「Hono」というCloudflare Workers向けのフレームワークを作っています。
以前もYAPCの発表とZennの記事で紹介したものです。
あらためて、さかのぼってみると「Initial Commit」が去年の12月15日でそれから現在405コミット。頑張っています。これは僕個人だけのものではなく、コントビューターの方のかいもあってです。ちなみに、そういうのも考慮して、個人リポジトリでやっていましたが、ある時から「honojs」オーガナイゼーションに切り替えました。 現在のバージョンは「v1.4.5」。 APIで紆余曲折ありつつも、安定してきました。また、使ってくれる人もだんだんと増えています。 今回は「現時点での」という前置き付きで、Honoの紹介をしましょう。 ピックアップしたら40個あったので、一気に書いていきます。
1. Getting Started
Honoを使ってCloudflare Workersのアプリケーションを作ってみます。 といっても簡単です。
まずWranglerというCLIでプロジェクトの雛形を作ります。
mkdir hono-example
cd hono-example
npx wrangler init -y
Honoはnpmからインストールできます。
npm init -y
npm i hono
Wranglerが作った雛形ではsrc/index.ts
がソースコードなので、それをまるごと編集しちゃいます。
これがHonoを使った最初のコードになります。
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => c.text("Hello! Hono!"));
app.fire();
開発サーバーを立ち上げてみましょう。
npx wrangler dev
http://127.0.0.1:8787/
にアクセスすると「Hello! Hono!」が見えるでしょう。
デプロイまでやっちゃいましょう。Cloudflareアカウントがあれば以下のコマンドでデプロイできます。
npx wrangler publish ./src/index.ts
インストールからデプロイまでが5ステップでできました! ほんと簡単なので、是非試してみてください!
2. Starter template
さきほどは、Wranglerとnpmコマンド使って1からプロジェクトを作りましたが、 コマンド一発でテストスクリプトも含んだプロジェクトを作れるStarter templateがあります。こちらも使ってみてください。
npx create-cloudflare my-app https://github.com/honojs/hono-minimal
ちなみに、Cloudflareの公式ドキュメントにも載せてもらってます。
3. Wrangler2
ここまで見てきた通り、開発にはWranglerの最新、「Wrangler2」を使うことをおすすめします。以前は、Miniflareとesbuildを組み合わせる方法を好んで使っていましたが、Wrangler2はその2つを内包しつつ更に便利なCLIになっています。
4. Module Workers
Cloudflare Workersには従来からのService WorkersモードとModule Workersモードがあります。
Module Workersモードでは変数やKVへの参照をローカルスコープで扱えたり、リソースへのimport
が使えたりします。
HonoでModule Workersを使うためにはfire()
の代わりにexport default app
を使います。
const app = new Hono();
app.get("/", (c) => c.text("Hono!"));
export default app;
5. Ultrafast
Honoでは「Ultrafast」を謳っています。Honoの他にも優秀なCloudflare向けのフレームワーク・ルーターがいくつかありますが、それらに比べてだいぶ速いです。 以下は、少々複雑なルーティングのアプリケーションを使ったベンチマークスコアです。
hono - trie-router(default) x 389,510 ops/sec ±3.16% (85 runs sampled)
hono - regexp-router x 452,290 ops/sec ±2.64% (84 runs sampled)
itty-router x 206,013 ops/sec ±3.39% (90 runs sampled)
sunder x 323,131 ops/sec ±0.75% (97 runs sampled)
worktop x 191,218 ops/sec ±2.70% (91 runs sampled)
Fastest is hono - regexp-router
✨ Done in 43.56s.
ほら、速いでしょ?
6. 依存0
Honoは他のライブラリに全く依存していません。 インストールは最小限で済みます。
7. ファイルサイズ
Honoのファイルサイズは非常に小さいです。 1MB制限のあるCloudflare Workersにとって、これは嬉しいことでしょう。 ミドルウェアを使わない最小構成だと、9.5KBです。
Honoはビルトインミドルウェアが非常に豊富ですが、ミドルウェアは「使ったら初めて読み込む」という仕組みになっているので安心してください。
8. TypeScript
HonoはTypeScriptで書かれているし、TypeScriptで書くことを推奨します。
Wranglerを使えば、tsconfig.json
を置かなくともゼロコンフィグで.ts
を動かせます。
これはすごい。
Honoではパスパラメータの値がそのままリテラルタイプになったりします。
9. DX
Wranglerだとゼロコンフィグで始められ、少ないステップで開発からデプロイまでできます。
デプロイ後には、wrangler tail
コマンドも使えます。
それに加えて、Honoを使えば、最小限の記述で、アプリケーションを作ることができます。
TypeScriptを活用したエディタでの補完もDX=Developer Experienceを向上させています。
10. 基本的なルーティング
さて、実装の話題です。
ルーティングは大抵のことができます。
まず、HTTPメソッドと特定のパスでのルーティング。
app.get("/", (c) => c.text("GET /"));
app.post("/", (c) => c.text("POST /"));
app.put("/", (c) => c.text("PUT /"));
app.delete("/", (c) => c.text("DELETE /"));
ワイルドカード。
app.get("/wild/*/card", (c) => {
return c.text("GET /wild/*/card");
});
全てのHTTPメソッドを受け付けるにはall
を使います。
// Any HTTP methods
app.all("/hello", (c) => c.text("Any Method /hello"));
パスパラメータ、つまり/post/123/comment/456
の「123」と「456」を取りたければこう書きます。
app.get('/post/:id/comment/:comment_id', (c) => {
const id = c.req.param('id')
const id = c.req.param('comment_id')
...
})
もしくはこうやれば一気に取れる。
app.get('/posts/:id/comment/:comment_id', (c) => {
const { id, comment_id } = c.req.param()
...
})
パスパラメータを正規表現で指定できます。
app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => {
const { date, title } = c.req.param()
...
})
また、ルーティングをチェインすることなんてのもできます。
app
.get("/endpoint", (c) => {
return c.text("GET /endpoint");
})
.post((c) => {
return c.text("POST /endpoint");
})
.delete((c) => {
return c.text("DELETE /endpoint");
});
11. ルーティング順序
ルーティングはいかようにも書けてしまいます。そこで、どれが優先されるかのルールがあります。 登録した順と、スラッシュの数(階層の深さ)によってスコアリングされて実行順が決まるのです。
app.get("/api/*", "c"); // score 1.1 <--- `/*` is special wildcard
app.get("/api/:type/:id", "d"); // score 3.2
app.get("/api/posts/:id", "e"); // score 3.3
app.get("/api/posts/123", "f"); // score 3.4
app.get("/*/*/:id", "g"); // score 3.5
app.get("/api/posts/*/comment", "h"); // score 4.6 - not match
app.get("*", "a"); // score 0.7
app.get("*", "b"); // score 0.8
この条件で、以下にアクセスします。
GET /api/posts/123
すると、マッチするのは「c, d, e, f, b, a, b
」なのでそれをスコア順にソートすれば「a, b, c, d, e, f, g
」になります。
このスコアリングはハンドラをミドルウェアと組み合わせて使う場合、重要になるので、必要に応じて参照してください。
12. グルーピング
大きなアプリケーションを作ったり、「v1
」「v2
」とバージョニングしたWeb APIを作るのに便利です。
const api = new Hono();
api.get("/", (c) => c.text("List Books")); // GET /v1
api.get("/post/:id", (c) => {
// GET /v1/post/:id
const id = c.req.param("id");
return c.text("Get Book: " + id);
});
api.post("/post", (c) => c.text("Create Book")); // POST /v1/post
const app = new Hono();
app.route("/v1", api);
13. Slashの扱い
末尾にスラッシュがある場合の扱いと、ない場合の扱い方をstrict
パラメータで指定できます。
デフォルトはtrue
、つまり、区別しません。
false
にするとどちらも同じパスとして扱います。
const app = new Hono({ strict: false }); // Default is true
app.get("/hello", (c) => c.text("/hello or /hello/"));
14. async/await
ハンドラは「async/await
」をサポートしています。
Promiseを返すfetch
も使えます。
app.get("/fetch-url", async (c) => {
const response = await fetch("https://example.com/");
return c.text(`Status is ${response.status}`);
});
15. Response
ユーザーへのレスポンスはコアAPIのResponse
オブジェクトを返せばいいのですが、HonoではContextに便利メソッドを生やしてるのでそれを使ってください。
app.get("/say", (c) => {
return c.text("Hello!");
});
とかけば、Content-Type:text/plain
が自動的に付きます。JSON、HTMLに関しても同じく適切なContent-Typeを設定して返却します。
app.get("/api", (c) => {
return c.json({ message: "Hello!" });
});
app.get("/page", (c) => {
return c.html("<h1>Hello! Hono!</h1>");
});
ステータスコードとヘッダを指定したければ、こう書きます。
app.post("/post", (c) => {
return c.text("Created!", 201, {
"X-Custom": "foo",
});
});
また以下のようにreturn
する前に指定していくこともできます。
app.get("/post", (c) => {
// Set headers
c.header("X-Custom", "foo");
c.header("Content-Type", "text/plain");
// Set HTTP status code
c.status(201);
// Return the response body
return c.body("Created!");
});
16. MiddlewareとHandler
ミドルウェアとハンドラという概念があります。 ほぼ同じものなのですが、違いは以下の通りです。
- ハンドラ -
Response
オブジェクトを必ず返す。1度のディスパッチにつき1つのハンドラが呼ばれます。 - ミドルウェア - 何も返しません。ハンドラがディスパッチする前後に呼ばれ、
Request
オブジェクトの中身を見たり、Response
をいじったりします。特定のパスのみならずワイルドカードを使って、複数のパスにマッチさせることができるので、共通の処理を書きます。await next()
を使って次のミドルウェアを実行していきます。
具体例を交えて解説します。
ミドルウェアはapp.use
やapp.get
、app.post
、app.put
…とともに、パスを指定して登録します。
今回はビルトインミドルウェアを使っています。
// match any method, all routes
app.use("*", logger());
// specify path
app.use("/posts/*", cors());
// specify method and path
app.post("/posts/*", basicAuth(), bodyParse());
以下は、c.text()
でResponse
を返すのでこれがハンドラになります。
app.post("/posts", (c) => c.text("Created!", 201));
全体のイメージとしてはこんな感じです。
logger() -> cors() -> basicAuth() -> bodyParse() -> *handler*
ハンドラを包むようにミドルウェアが実行されます。
17. カスタムミドルウェア
自分でミドルウェアを書けます。await next()
がディスパッチのタイミングなので、その前後に処理を挟めば、x-response-time
ヘッダを付加するミドルウェアはこのように書けます。
app.use("*", async (c, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
c.header("x-Response-time", `${ms}`);
});
18. ビルトインミドルウェア
Honoには予めたくさんビルトインミドルウェアが備わっています。 現在、以下があります。
- Basic Authentication
- Bearer Authentication
- Cookie parsing / serializing
- CORS
- ETag
- GraphQL Server
- JWT Authentication
- Logger
- Mustache template engine (Only for Cloudflare Workers)
- JSON pretty printing
- Serving static files (Only for Cloudflare Workers)
- Body Parse
- Powered-by
これらは、例えばhono/basic-auth
というパスでimportできて、そのタイミングで初めて読み込まれます。
また、なるべく外部のライブラリに依存しないようできています。
ポータブルなんですね。
ではこれらの中からいくつか見ていきましょう。
19. Basic Auth
Cloudflare Workersでベーシック認証を実装するのは案外面倒ですが、これを使えば一発です。
import { Hono } from "hono";
import { basicAuth } from "hono/basic-auth";
const app = new Hono();
app.use(
"/auth/*",
basicAuth({
username: "hono",
password: "acoolproject",
})
);
app.get("/auth/page", (c) => {
return c.text("You are authorized");
});
app.fire();
20. Bearer Auth
API tokenなどをリクエストヘッダに込めて、それを受け付けてVerifyするような認証もミドルウェアで提供しています。
import { Hono } from "hono";
import { bearerAuth } from "hono/bearer-auth";
const app = new Hono();
const token = "honoisacool";
app.use("/auth/*", bearerAuth({ token }));
app.get("/auth/page", (c) => {
return c.text("You are authorized");
});
app.fire();
21. JWT Auth
JWTの認証もあります。
import { Hono } from "hono";
import { jwt } from "hono/jwt";
const app = new Hono();
app.use(
"/auth/*",
jwt({
secret: "it-is-very-secret",
})
);
app.get("/auth/page", (c) => {
return c.text("You are authorized");
});
app.fire();
22. CORS
Cloudflare WorkersをWeb APIにして外部のフロントから呼び出す、というユースケースが多くて、CORSの実装についての話題が多いですが、これもミドルウェアでやりましょう。 細かいオプション設定でもできます。
const app = new Hono();
app.use("/api/*", cors());
app.use(
"/api2/*",
cors({
origin: "http://example.com",
allowHeaders: ["X-Custom-Header", "Upgrade-Insecure-Requests"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
maxAge: 600,
credentials: true,
})
);
app.all("/api/abc", (c) => {
return c.json({ success: true });
});
app.all("/api2/abc", (c) => {
return c.json({ success: true });
});
23. ETag
ETagも勝手につけてくれます。
const app = new Hono();
app.use("/etag/*", etag());
app.get("/etag/abc", (c) => {
return c.text("Hono is cool");
});
app.fire();
24. Serve Static
Cloudflare Workersにはファイルシステムという概念がなく、画像やCSS、JSなどの静的ファイルをサーブするのには「Workers Sites」という仕組みを利用しなくてはいけなく、少々面倒です。
そこで、wrangler.toml
さえ設定してしまえば、特定のディレクトリ以下に置いたファイルをそのまま配信できるミドルウェアを作りました。よく使います。
import { Hono } from "hono";
import { serveStatic } from "hono/serve-static";
const app = new Hono();
app.use("/static/*", serveStatic({ root: "./" }));
app.get("/", (c) => c.text("This is Home! You can access: /static/hello.txt"));
app.fire();
例: https://github.com/honojs/examples/tree/master/serve-static
ちなみにこれらのファイルはKVに置かれるので、1MBの制限とは別の扱いです。
25. Mustache
MustacheというHTMLを出力するためのテンプレートエンジンも使えます。 テンプレートファイルに記述するので、コードとViewを分けることができます。 ちなみに、これもKVの仕組みを使っています。
import { Hono } from "hono";
import { mustache } from "hono/mustache";
const app = new Hono();
app.use("*", mustache());
app.get("/", (c) => {
return c.render(
"index",
{ name: "Hono", title: "Hono mustache example" }, // Parameters
{ footer: "footer", header: "header" } // Partials
);
});
app.fire();
Module Workersモードではこう書きます。
import { Hono } from "hono";
import { mustache } from "hono/mustache.module"; // <---
const app = new Hono();
app.use("*", mustache());
// ...
export default app;
それぞれのテンプレートファイルは以下の通りです。 Partialという仕組みを使ってヘッダとヘッダを分けています。
index.mustache:
{{> header}}
<h1>Hello! {{name}}</h1>
{{> footer}}
header.mustache:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{{title}}</title>
</head>
</body>
footer.mustache:
</body>
</html>
例: https://github.com/honojs/examples/tree/master/mustache-template
26. GraphQL
Honoを使ってGraphQLサーバーも立てられます。 実際、「Ramen API」というプロジェクトでも使っています。
graphql
ライブラリを別途インストールさえすれば、GraphQLサーバーになるのです!
import { Hono } from "hono";
import { graphqlServer } from "hono/graphql-server";
import { buildSchema } from "graphql";
export const app = new Hono();
const schema = buildSchema(`
type Query {
hello: String
}
`);
const rootValue = {
hello: () => "Hello Hono!",
};
app.use(
"/graphql",
graphqlServer({
schema,
rootValue,
})
);
app.fire();
27. JSX
これはまだリリースされておらず、実装段階ですが、将来組み込まれる可能性があります。 Mustacheミドルウェアのように、ちょっとしたHTMLをHonoで出力したい時にJSXのシンタックスを使えるってものです。 これはあくまでもSSRするためだけにJSXを使っているだけで、Reactでもありません。 仮想DOMは扱わず、全て文字列です。
以下は「こんな風に書けるだろう」という例です。
import { Hono } from "hono";
import { h, jsx } from "hono/jsx";
export const app = new Hono();
app.use("*", jsx());
const Layout = (props: { children?: string; link: string }) => {
return (
<html>
<body>
{props.children}
<footer>
<a href={props.link}>{props.link}</a>
</footer>
</body>
</html>
);
};
const Top = (props: { message: string }) => {
return (
<Layout link="https://github.com/honojs/hono">
<h1>{props.message}</h1>
</Layout>
);
};
app.get("/", (c) => {
const message = "Hello! Hono!";
return c.render(<Top message={message} />);
});
export default app;
繰り返しますがこのミドルウェアは「ちょっとした」HTML向けです。 よりファットなアプリケーションではReactなりRemixを使いましょう。
ミドルウェアの解説は以上です。
28. Not Foundとエラーハンドリング
Honoではユーザー自身がNot Foundとエラーの扱いを指定できます。
app.notFound((c) => {
return c.text("Custom 404 Message", 404);
});
エラーハンドリング用のハンドラでは、第一引数にThrowされたErrorオブジェクトが渡ってきます。
app.onError((err, c) => {
console.error(`${err}`);
return c.text("Custom Error Message", 500);
});
29. 2つのルーター
Honoでは内部で使うルーターが2種類あって、どちらを使うかを指定できます。
デフォルトは「TrieRouter
」です。
import { RegExpRouter } from "hono/router/reg-exp-router";
const app = new Hono({ router: new RegExpRouter() });
30. TrieRouter
Honoのルーターは速いです。
TrieRouterはTrie木という構造を使っているので、登録されたパスを頭からなめる方法よりも速くなります。 また、適切な箇所でキャッシュを利用しています。
31. RegExpRouter
TriRouterより速いのがRegExpRouterです。
これは@usualomaさんが実装してくれました。
登録されたルーティングを予めひとつの大きな正規表現にして、リクエストが来たら、マッチさせるという仕組みです。
これはPerlのRouter::Boomで使われいた手法です。
ベンチマークをすると、Cloudflare Workersのみならず、他のNodeのフレームワークで使われいてるルーターでも最速レベルです。マルチマッチしない実装だと、fastifyなんかで使われいてるfind-my-way
と比べてもいい勝負、もしくは速いのですごいです。
32. テスト
Cloudflare Workersのいいところはテストが簡単に書けることです。
Miniflareの仮想環境が優秀だからでしょう。
それに加えて、jest-environment-miniflare
というjestのenvironmentを使います。
設定についてはStarter templateを参照してください。
さて書き方ですが、Honoですとapp.request
を使います。
const app = new Hono();
app.get("/hello", (c) => c.text("hello!"));
というアプリがあったとして、/hello
へのRequestの結果が200かどうかを試験するにはこう書けます。
test("GET /hello is ok", async () => {
const res = await app.request("http://localhost/hello");
expect(res.status).toBe(200);
});
簡単でしょ?
33. Bindings
Cloudflare Workersではいわゆるexport HOO=FOO
で設定するような環境変数の概念がありません。
その代わり、wrangler.toml
に書いたりして登録します。
Module Workersモードの場合、その変数が環境にバインドされます。
また、以下で説明するKVやR2へのアクセスもバインドされたオブジェクトを経由します。
Honoではc.env
からそれらにアクセスできます。
例えば、よくあるユースケースとして、ユーザー/パスを変数に入れて、それをBasic認証で参照させるにはこう書きます。
export interface Bindings {
USERNAME: string;
PASSWORD: string;
}
const api = new Hono<Bindings>();
api.post("/posts", async (c, next) => {
const auth = basicAuth({
username: c.env.USERNAME,
password: c.env.PASSWORD,
});
await auth(c, next);
});
ジェネリクスを渡すと補完が効いて便利です。
34. KVと一緒に使う
Cloudflare Workersには「KV」という非常に素朴なkey-valueストアがあります。 それを使ってみましょう。
といっても、Service Workersモードではグローバルな名前空間を参照することになるので、特に「Honoだからこう」ということはありません。
declare let BOOKS: KVNamespace;
const book = await BOOKS.get(key);
一方、Module Workersモードでは上記したBindingsの概念を使うことになります。
export interface Bindings {
BOOKS: KVNamespace;
}
const api = new Hono<Bindings>();
api.post("/book/:key", async (c, next) => {
const key = c.req.param("key");
const book = await c.env.BOOKS.get(key);
//...
});
このように、Module Workersだとローカルスコープで参照します。
35. R2と一緒に使う
先日BetaになったR2と一緒に使ってみます。といっても、これもKVと同じでc.env
にオブジェクトを生やすだけです。
interface Bindings {
BUCKET: R2Bucket;
}
app.get("/:key", async (c) => {
const key = c.req.param("key");
const object = await c.env.BUCKET.get(key);
if (!object) return c.notFound();
data = await object.arrayBuffer();
contentType = object.httpMetadata.contentType;
return c.body(data, 200, {
"Content-Type": contentType,
});
});
R2とKVを使ったアプリを以前作ったので、そちらも参考にしてください。
36. 実用的な例
これらを踏まえて、実用的なアプリを書いていくと、以下のようなコードを拡張していくことになるでしょう。
import { Hono } from "hono";
import { cors } from "hono/cors";
import { basicAuth } from "hono/basic-auth";
import { prettyJSON } from "hono/pretty-json";
import { getPosts, getPost, createPost, Post } from "./model";
const app = new Hono();
app.get("/", (c) => c.text("Pretty Blog API"));
app.use("*", prettyJSON());
app.notFound((c) => c.json({ message: "Not Found", ok: false }, 404));
export interface Bindings {
USERNAME: string;
PASSWORD: string;
}
const api = new Hono<Bindings>();
api.use("/posts/*", cors());
api.get("/posts", (c) => {
const { limit, offset } = c.req.query();
const posts = getPosts({ limit, offset });
return c.json({ posts });
});
api.get("/posts/:id", (c) => {
const id = c.req.param("id");
const post = getPost({ id });
return c.json({ post });
});
api.post(
"/posts",
async (c, next) => {
const auth = basicAuth({
username: c.env.USERNAME,
password: c.env.PASSWORD,
});
await auth(c, next);
},
async (c) => {
const post = await c.req.json<Post>();
const ok = createPost({ post });
return c.json({ ok });
}
);
app.route("/api", api);
export default app;
37. Fastly Compute@Edgeでも動く
ちなみに、Cloudflare WorkersはWebスタンダードのAPIを使っており、その点ではFastly Compute@Edgeも同じです。 なのでHonoは一部の機能(Serve StaticミドルウェアなどKVを使うもの)を除いてはFastly Compute@Edgeでも動きます。以下が、それようのStarter templateです。
38. Deno対応??
実行環境は違えど、DenoもWeb標準のAPIを扱います。 なので、工夫をすればDenoでも動きます。 現に、Denoifyというトランスコンパラを使って吐き出したコードが動くことを確認しました!
正式に対応するか謎ですが、その可能性もありますね。 メンテナスコストがかかりそうなので、躊躇しているところです。
39. Contributor
Honoは僕だけのプロジェクトではないです。 特に@metrueさんと@usualomaさんの貢献がでかいです。@metrueさんは主にミドルウェアの実装を。 @usualomaさんはRegExpRouterを始め、パフォーマンスやリファクタリングに関わるところを実装しています。 またIssue上で議論をすることがあって、ハンドラとミドルウェアについてと、ルーティングルールについてを「濃く」やりとりしました。楽しいですね。
40. 参考プロジェクト
最後に、参考プロジェクト一覧です。
APIのデザインについてはExpressとKoaにインスパイアされている部分が大きいです。
例えばcompose.ts
はKoaのコードがベースになっています。
itty-router、Sunder、Worktopは他のCloudflare Workersのルーター・フレームワークです。 itty-routerはたった35行なのがすごい。 Worktopは元Cloudflareの天才、lukeedが作っていて完成度が高いです。
TrieRouterの実装にあたってはgoblinというGoのルーター、RegExpRouterはPerlのRouter::Boomを参考にしています。
- express - https://github.com/expressjs/express
- koa - https://github.com/koajs/koa
- itty-router - https://github.com/kwhitley/itty-router
- Sunder - https://github.com/SunderJS/sunder
- goblin - https://github.com/bmf-san/goblin
- worktop - https://github.com/lukeed/worktop
- Router::Boom - https://github.com/tokuhirom/Router-Boom
おまけ
急にTwitterでメンション飛んできて、なんだと思ったら、Wrangler2のメイン開発者@threepointoneがHonoを取り上げてくれた!Wrangler2をずっとウォッチしてて、この人すごいなーって思っていたからすごい嬉しい!
loading...
まとめ
以上、Honoについて40個のことをピックアップしてみました。 Hono、わりといい出来だし、Cloudflare Workersも楽しいので使ってみてください!