Implementing the backend of a mobile app using Hono and Cloudflare Workers

2023-12-18

This article is the 18th day of the Hono Advent Calendar 2023

https://qiita.com/advent-calendar/2023/hono

Background

In my university class, I'm developing an ios app as a team. The app needed a backend, so I decided to use the hottest technology stack, Hono and Cloudflare Workers, to implement the backend. It's an MVP, so I didn't do anything fancy.

Tech Stack

Needless to say.

This too.

I needed a database, so I used it.

It's a TypeScript ORM that also supports D1. It's hot.

As a requirement of this app, there was a photo posting, so I used it as a storage location.

Setup

Wrangler

Since I'm using Cloudflare Workers, I need to set up Wrangler. First of all,

npm install -g wrangler

and then,

wrangler login

If you can log in successfully, create a project with

wrangler init [project name]

D1, Drizzle setup

I referred to the following articles.

D1 setup is done with

wrangler d1 create [database name]

and then add the following to wrangler.toml.

[[ d1_databases ]]
binding = "DB" 
database_name = "<your-database-name>"
database_id = "<your-id>"

I referred to the following articles for the drizzle setup (I omitted it because it's a bit off topic).

I'm lazy to type wrangler d1 migrations apply dbtest, so I added the following two to package.json.

"scripts": {
		"deploy": "wrangler deploy",
		"dev": "wrangler dev",
		"start": "wrangler dev",
+		"generate": "drizzle-kit generate:sqlite --out migrations --schema src/db/schema.ts",
+		"migration": "wrangler d1 migrations apply dbtest"
	},

R2 setup

I referred to yusukebe's article for the R2 setup. https://yusukebe.com/posts/2022/r2-beta/

As usual,

wrangler r2 bucket create [bucket name]

to create a bucket and then add it to wrangler.toml.

[[ r2_buckets ]]
binding = 'BUCKET'
bucket_name = '<your-bucket-name>'
preview_bucket_name = '<your-bucket-name>'

Hono setup

Setup of Hono is done with

npm create hono@latest

and then select

cloudflare-workers

from the options.

実装について

I'll just introduce the parts that stand out, as long as they meet the requirements.

First, the implementation of photo posting.

app.put('/post', async (c) => {
	const data = await c.req.json<PostData>();
	const base64 = data.key;
	if (!base64) return c.notFound();

	const type = detectType(base64);
	if (!type) return c.notFound();

	const body = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));

	let key;

	key = (await sha256(body)) + '.' + type?.suffix;

	if (c.env) {
		const bucket = c.env.BUCKET;
		await bucket.put(key, body, { httpMetadata: { contentType: type.mimeType } });
	} else {
		throw new Error('c.env is undefined');
	}

	const db = drizzle(c.env.DB);
	await db
		.insert(posts)
		.values({
			userId: data.userId,
			title: data.title,
			key: key,
			createdAt: data.createdAt,
			latitude: data.latitute,
			longitude: data.longitude,
			isPublic: data.isPublic,
			isFriendsOnly: data.isFriendsOnly,
			isPrivate: data.isPrivate,
			orientation: data.orientation,
		})
		.execute();
    const result = await db.select().from(posts).where(eq(posts.key, key)).all();
    return c.json({ id: result[0].id, userId: data.userId, title: data.title, key: key });
});

To explain in detail,

The photo data comes in base64, so I referred to yusukebe's code and saved it in the bucket.

const body = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));

	let key;

	key = (await sha256(body)) + '.' + type?.suffix;

	if (c.env) {
		const bucket = c.env.BUCKET;
		await bucket.put(key, body, { httpMetadata: { contentType: type.mimeType } });
	} else {
		throw new Error('c.env is undefined');
	}

It's a complete copy. Thank you for yusukebe.

Then, I saved it in the database with the help of drizzle.

const db = drizzle(c.env.DB);
    await db
        .insert(posts)
        .values({
            userId: data.userId,
            title: data.title,
            key: key,
            createdAt: data.createdAt,
            latitude: data.latitute,
            longitude: data.longitude,
            isPublic: data.isPublic,
            isFriendsOnly: data.isFriendsOnly,
            isPrivate: data.isPrivate,
            orientation: data.orientation,
        })
        .execute();

所感

I was able to experience the ease of development using Hono and Cloudflare Workers. As a beginner, I was able to code intuitively and deploy easily, so I think it's perfect for beginners. It's nice to be able to deploy the code I wrote immediately and check it out, and it also helps to keep my motivation up. It's very helpful for me, who is easily bored. In conclusion, Hono and Cloudflare Workers are the best.

モバイルアプリのバックエンドをHono+Cloudflare Workersで実装した話

2023-12-18

この記事はHono Advent Calendar 2023の18日目の記事です。

https://qiita.com/advent-calendar/2023/hono

経緯

私が通っている大学の授業の一環として、iosアプリのチーム開発を行っています。そのアプリでバックエンドが必要になり、せっかくなので今アツい技術スタックを使ってみたい!!!ということで、HonoとCloudflare Workersを使ってバックエンドを実装しました。といってもMVPなので、大層なことはしていません。

使用スタック

言わずもがなですね。

これもそう。

データベースが欲しかったので使いました。

D1にも対応しているTypeScriptのORMです。アツい。

今回のアプリの要件として、写真の投稿があったので、その保存先として使いました。

セットアップ

Wrangler

Cloudflare Workersを使っているので、Wranglerのセットアップが必要。 とりま、

npm install -g wrangler

して、

wrangler login

をする。無事にログインできたら、

wrangler init [project name]

でプロジェクトを作成します。

D1,Drizzleのセットアップ

私は、以下の記事を参考にしました。

D1のセットアップは、

wrangler d1 create [database name]

で出来ちゃいます。 あとはwrangler.tomlに、追加するだけ。

[[ d1_databases ]]
binding = "DB" 
database_name = "<your-database-name>"
database_id = "<your-id>"

drizzle周りのセットアップは参考の記事に書いてある通りです(本題から若干外れるので割愛)。

migrationのコマンドを打つのが面倒なので、package.jsonに以下の二つを追加しました。

"scripts": {
		"deploy": "wrangler deploy",
		"dev": "wrangler dev",
		"start": "wrangler dev",
+		"generate": "drizzle-kit generate:sqlite --out migrations --schema src/db/schema.ts",
+		"migration": "wrangler d1 migrations apply dbtest"
	},

R2のセットアップ

R2のセットアップは、yusukebeさんの記事を参考にしました。 https://yusukebe.com/posts/2022/r2-beta/

例の如く、

wrangler r2 bucket create [bucket name]

でバケットを作成して、

wrangler.tomlに追加します。

[[ r2_buckets ]]
binding = 'BUCKET'
bucket_name = '<your-bucket-name>'
preview_bucket_name = '<your-bucket-name>'

Honoのセットアップ

Honoのセットアップは、

npm create hono@latest

を叩いて、選択肢のうち

cloudflare-workers

を選択します。そうすれば色々生成されるので、src/に色々書いていけばおkです。

実装について

要件に沿って実装していけばいいので、特出するところだけ紹介します。

まず、写真の投稿の実装ですが、

app.put('/post', async (c) => {
	const data = await c.req.json<PostData>();
	const base64 = data.key;
	if (!base64) return c.notFound();

	const type = detectType(base64);
	if (!type) return c.notFound();

	const body = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));

	let key;

	key = (await sha256(body)) + '.' + type?.suffix;

	if (c.env) {
		const bucket = c.env.BUCKET;
		await bucket.put(key, body, { httpMetadata: { contentType: type.mimeType } });
	} else {
		throw new Error('c.env is undefined');
	}

	const db = drizzle(c.env.DB);
	await db
		.insert(posts)
		.values({
			userId: data.userId,
			title: data.title,
			key: key,
			createdAt: data.createdAt,
			latitude: data.latitute,
			longitude: data.longitude,
			isPublic: data.isPublic,
			isFriendsOnly: data.isFriendsOnly,
			isPrivate: data.isPrivate,
			orientation: data.orientation,
		})
		.execute();
    const result = await db.select().from(posts).where(eq(posts.key, key)).all();
    return c.json({ id: result[0].id, userId: data.userId, title: data.title, key: key });
});

という感じです。

詳しく説明すると、

写真のデータはbase64で来るので、yusukebeさんのコードを参考にして、バケットに保存しています。

const body = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));

	let key;

	key = (await sha256(body)) + '.' + type?.suffix;

	if (c.env) {
		const bucket = c.env.BUCKET;
		await bucket.put(key, body, { httpMetadata: { contentType: type.mimeType } });
	} else {
		throw new Error('c.env is undefined');
	}

まるパクリですね。ありがとうございます。

あとは、drizzleの恩恵に預かって、データベースに保存しています。

const db = drizzle(c.env.DB);
    await db
        .insert(posts)
        .values({
            userId: data.userId,
            title: data.title,
            key: key,
            createdAt: data.createdAt,
            latitude: data.latitute,
            longitude: data.longitude,
            isPublic: data.isPublic,
            isFriendsOnly: data.isFriendsOnly,
            isPrivate: data.isPrivate,
            orientation: data.orientation,
        })
        .execute();

所感

モバイルアプリじゃなくてもいいやんというツッコミはさておき、HonoとCloudflare Workersを使ってみて、開発体験の良さを実感しました。初心者の私が直感的にコーディングできて、デプロイも簡単なので、入門には最適だと思います。書いたコードを即座にデプロイして、動作確認ができるのは気持ちがよく、モチベーションの維持にも繋がると思います。何かと飽き性な筆者には、とてもありがたいです。結論、HonoとCloudflare Workersは最高です。