Open24
Hono に入門したい
このスクラップについて
Hono に入門したい衝動を抑えきれないので Getting Started を読んで入門する過程を記録していく。
Getting Started
プロジェクト作成
コマンド
npm create hono@latest hello-hono
コンソール出力
create-hono version 0.12.0
✔ Using target directory … hello-hono
? Which template do you want to use? cloudflare-workers
✔ Cloning the template
? Do you want to install project dependencies? yes
? Which package manager do you want to use? npm
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd hello-hono
サーバー起動
コマンド
npm run dev
動作確認
curl http://localhost:8787
コンソール出力
Hello Hono!
JSON 出力
src/index.ts
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => {
return c.text("Hello Hono!");
});
app.get("/api/hello", (c) => {
return c.json({
ok: true,
message: "Hello Hono!",
});
});
export default app;
動作確認
curl http://localhost:8787/api/hello
コンソール出力
{"ok":true,"message":"Hello Hono!"}
Request and Response
src/index.ts
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => {
return c.text("Hello Hono!");
});
app.get("/api/hello", (c) => {
return c.json({
ok: true,
message: "Hello Hono!",
});
});
app.get("/posts/:id", (c) => {
const page = c.req.query("page");
const id = c.req.param("id");
c.header("X-Message", "Hi!");
return c.text(`You want see ${page} of ${id}`);
});
app.post("/posts", (c) => c.text("Created!", 201));
app.delete("/posts/:id", (c) => c.text(`${c.req.param("id")} is deleted!`));
export default app;
動作確認
curl -v http://localhost:8787/posts/POST_ID
コンソール出力
* Trying 127.0.0.1:8787...
* Connected to localhost (127.0.0.1) port 8787 (#0)
> GET /posts/POST_ID HTTP/1.1
> Host: localhost:8787
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 33
< Content-Type: text/plain; charset=UTF-8
< x-message: Hi!
<
* Connection #0 to host localhost left intact
You want see undefined of POST_ID
動作確認
curl -X POST -v http://localhost:8787/posts
コンソール出力
* Trying 127.0.0.1:8787...
* Connected to localhost (127.0.0.1) port 8787 (#0)
> POST /posts HTTP/1.1
> Host: localhost:8787
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 201 Created
< Content-Length: 8
< Content-Type: text/plain; charset=UTF-8
<
* Connection #0 to host localhost left intact
Created!
動作確認
curl -X DELETE -v http://localhost:8787/posts/POST_ID
コンソール出力
* Trying 127.0.0.1:8787...
* Connected to localhost (127.0.0.1) port 8787 (#0)
> DELETE /posts/POST_ID HTTP/1.1
> Host: localhost:8787
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 19
< Content-Type: text/plain;charset=UTF-8
<
* Connection #0 to host localhost left intact
POST_ID is deleted!
次は Return HTML
OpenAPI をやりたい
まだ基礎も固まっていないが OpenAPI の機能を使用してみたい。
プロジェクト作成
コマンド
npm create hono@latest hono-openapi
コンソール出力
create-hono version 0.12.0
✔ Using target directory … hono-openapi
? Which template do you want to use? nodejs
✔ Cloning the template
? Do you want to install project dependencies? yes
? Which package manager do you want to use? npm
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd hono-openapi
サーバー起動
コマンド
cd hono-openapi
npm run dev
インストール
コマンド
npm i zod @hono/zod-openapi
コーディング
src/index.ts
import { serve } from "@hono/node-server";
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
const ParamsSchema = z.object({
id: z
.string()
.min(3)
.openapi({
param: {
name: "id",
in: "path",
},
example: "121212",
}),
});
const UserSchema = z
.object({
id: z.string().openapi({
example: "123",
}),
name: z.string().openapi({
example: "John Doe",
}),
age: z.number().openapi({
example: 42,
}),
})
.openapi("User");
const ErrorSchema = z.object({
code: z.number().openapi({
example: 400,
}),
message: z.string().openapi({
example: "Bad Request",
}),
});
const route = createRoute({
method: "get",
path: "/users/{id}",
request: {
params: ParamsSchema,
},
responses: {
200: {
content: {
"application/json": {
schema: UserSchema,
},
},
description: "Retrieve the user",
},
400: {
content: {
"application/json": {
schema: ErrorSchema,
},
},
description: "Returns an error",
},
},
});
const app = new OpenAPIHono();
app.openapi(
route,
(c) => {
const { id } = c.req.valid("param");
return c.json(
{
id,
age: 20,
name: "Ultra-man",
},
200
);
},
(result, c) => {
if (!result.success) {
return c.json(
{
code: 400,
message: "Validation Error",
},
400
);
}
}
);
app.doc("/doc", {
openapi: "3.0.0",
info: {
version: "1.0.0",
title: "My API",
},
});
const port = 3000;
console.log(`Server is running on port ${port}`);
serve({
fetch: app.fetch,
port,
});
API ドキュメント
コマンド
curl http://localhost:3000/doc | jq
コンソール出力
{
"openapi": "3.0.0",
"info": {
"version": "1.0.0",
"title": "My API"
},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "123"
},
"name": {
"type": "string",
"example": "John Doe"
},
"age": {
"type": "number",
"example": 42
}
},
"required": [
"id",
"name",
"age"
]
}
},
"parameters": {}
},
"paths": {
"/users/{id}": {
"get": {
"parameters": [
{
"schema": {
"type": "string",
"minLength": 3,
"example": "121212"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "Retrieve the user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"400": {
"description": "Returns an error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "number",
"example": 400
},
"message": {
"type": "string",
"example": "Bad Request"
}
},
"required": [
"code",
"message"
]
}
}
}
}
}
}
}
}
}
Swagger UI
コマンド
npm install @hono/swagger-ui
src/index.ts(追記)
app.get('/ui', swaggerUI({ url: '/doc' }))
http://localhost:3000/ui にアクセスする。
Swagger UI が表示された
次は OpenAPI TypeScript を試してみよう
CORS 対応
今回は GET だけなので大丈夫そうだが下記ミドルウェアで簡単に対応できそうだ。
下記の記事も参考になりそう。
バキバキ必要だった、もう API とフロントを分ける場合は無条件に必要だと考えておこう。
Remix プロジェクト作成
コマンド
npx create-remix@latest hello-openapi-ts
コンソール出力
remix v2.11.2 💿 Let's build a better website...
◼ Directory: Using hello-openapi-ts as project directory
◼ Using basic template See https://remix.run/guides/templates for more
✔ Template copied
git Initialize a new git repository?
Yes
deps Install dependencies with npm?
Yes
✔ Dependencies installed
✔ Git initialized
done That's it!
Enter your project directory using cd ./hello-openapi-ts
Check out README.md for development and deploy instructions.
Join the community at https://rmx.as/discord
OpenAPI TypeScript セットアップ
コマンド
npm i -D openapi-typescript typescript
tsconfig.json の内容を確認する。
tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler",
"noUncheckedIndexedAccess": true
}
}
型ファイル生成
コマンド
npx openapi-typescript http://localhost:3000/doc -o app/schema.d.ts
app/schema.d.ts
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/users/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Retrieve the user */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
/** @description Returns an error */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @example 400 */
code: number;
/** @example Bad Request */
message: string;
};
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
User: {
/** @example 123 */
id: string;
/** @example John Doe */
name: string;
/** @example 42 */
age: number;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;
いいね。
openapi-fetch インストール
コマンド
npm i openapi-fetch
コーディング
app/routes/_index.tsx
import type { MetaFunction } from "@remix-run/node";
import createClient from "openapi-fetch";
import { useEffect } from "react";
import { paths } from "~/schema";
export const meta: MetaFunction = () => {
return [
{ title: "New Remix App" },
{ name: "description", content: "Welcome to Remix!" },
];
};
const client = createClient<paths>({ baseUrl: "http://localhost:3000/" });
export default function Index() {
useEffect(() => {
client
.GET("/users/{id}", {
params: {
path: {
id: "123",
},
},
})
.then(({ data, error }) => {
if (data) {
console.info({ data });
} else if (error) {
console.info({ error });
}
});
}, []);
return (
<main className="container mx-auto">
<h1 className="my-4 text-2xl">Hello OpenAPI TypeScript</h1>
</main>
);
}
エディタの補完がバチバチに効くのが嬉しい。
openapi-react-query を使う?
コマンド
npm i openapi-react-query
なんか TanStack Query を直接使う方が良い気がしてきた。
ただコードは参考になりそう。
結局 openapi-react-query を使う
app/root.tsx
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import "./tailwind.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
app/routes/_index.tsx
import type { MetaFunction } from "@remix-run/node";
import createFetchClient from "openapi-fetch";
import createClient from "openapi-react-query";
import { paths } from "~/schema";
export const meta: MetaFunction = () => {
return [
{ title: "New Remix App" },
{ name: "description", content: "Welcome to Remix!" },
];
};
const fetchClient = createFetchClient<paths>({
baseUrl: "http://localhost:3000/",
});
const $api = createClient(fetchClient);
export default function Index() {
const { data, error, isLoading } = $api.useQuery("get", "/users/{id}", {
params: {
path: {
id: "123",
},
},
});
return (
<main className="container mx-auto">
<h1 className="my-4 text-2xl">Hello OpenAPI TypeScript</h1>
{isLoading && <p className="mb-4">Loading...</p>}
{data && (
<dl className="mb-4">
<dt>id</dt>
<dd>{data.id}</dd>
<dt>age</dt>
<dd>{data.age}</dd>
<dt>name</dt>
<dd>{data.name}</dd>
</dl>
)}
{error && <p role="alert">{error.message}</p>}
</main>
);
}
良い塩梅です。