このページは英語の原文を翻訳したものです。内容に相違がある場合は英語版が優先されます。
チュートリアル:書店 API を構築する
小さな書店サービスを最初から最後まで構築します。ルーティング、検証、セキュリティ、OpenAPI、Hey API の型付き SDK、そしてコントラクトテストです。最後には、本番に近い形の DaloyJS アプリが手元に残ります。
- 01プロジェクトを作成pnpm add @daloyjs/core zod
- 02buildApp ファクトリールート · Zod · セキュリティフック
- 03サーバーを起動Node で serve(app)
- 04OpenAPI 仕様generateOpenAPI(app)
- 05型付き SDKHey API openapi-ts
- 06コントラクトテストrunContractTests(app)
1. プロジェクトを作成する
bash
mkdir bookstore && cd bookstore
pnpm init
pnpm add @daloyjs/core zod
pnpm add -D typescript @types/node @hey-api/openapi-ts prettierjson
// package.json を以下の内容に置き換える
{
"name": "bookstore",
"type": "module",
"scripts": {
"dev": "node --watch src/server.ts",
"test": "node --test tests/**/*.test.ts",
"typecheck": "tsc --noEmit",
"gen:openapi": "node scripts/dump-openapi.ts",
"gen:client": "openapi-ts",
"gen": "pnpm gen:openapi && pnpm gen:client"
}
}json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"types": ["node"],
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "openapi-ts.config.ts"]
}ini
# .npmrc
auto-install-peers=true
strict-peer-dependencies=true
prefer-frozen-lockfile=true
verify-store-integrity=true2. 共有の buildApp ファクトリーを作る
サーバー、コード生成、テストで同じ App を共有し、仕様、クライアント、テストを常に一致させます:
ts
// src/build-app.ts
import { z } from "zod";
import { App, requestId, secureHeaders, cors, rateLimit, bearerAuth, NotFoundError, timingSafeEqual } from "@daloyjs/core";
export const BookSchema = z.object({
id: z.string(),
title: z.string(),
year: z.number().int().optional(),
});
export function buildApp() {
const books = new Map<string, z.infer<typeof BookSchema>>([
["1", { id: "1", title: "Foundation", year: 1951 }],
["2", { id: "2", title: "Dune", year: 1965 }],
]);
const app = new App({ bodyLimitBytes: 64 * 1024, requestTimeoutMs: 5_000 });
app.use(requestId());
app.use(secureHeaders());
app.use(cors({ origin: ["http://localhost:5173"] }));
app.use(rateLimit({ windowMs: 60_000, max: 120 })); // keyGenerator か trustProxyHeaders を設定しない限りグローバルに適用
app.get(
"/books/:id",
{
operationId: "getBookById",
tags: ["Books"],
request: { params: z.object({ id: z.string() }) },
responses: {
200: {
description: "Found",
body: BookSchema,
examples: { default: { id: "1", title: "Foundation", year: 1951 } },
},
404: { description: "Not found" },
},
},
async ({ params }) => {
const book = books.get(params.id);
if (!book) throw new NotFoundError(`book ${params.id} not found`);
return { status: 200, body: book };
},
);
app.post(
"/books",
{
operationId: "createBook",
tags: ["Books"],
hooks: bearerAuth({ validate: (t) => timingSafeEqual(t, "demo-token") }),
request: { body: BookSchema.omit({ id: true }) },
responses: {
201: { description: "Created", body: BookSchema },
401: { description: "Unauthorized" },
422: { description: "Validation error" },
},
},
async ({ body }) => {
const id = String(books.size + 1);
const book = { id, ...body };
books.set(id, book);
return { status: 201, body: book };
},
);
return app;
}3. サーバーを起動する
ts
// src/server.ts
import { buildApp } from "./build-app.js";
import { serve } from "@daloyjs/core/node";
const app = buildApp();
const { port } = serve(app, { port: 3000 });
console.log(`bookstore listening on http://localhost:${port}`);bash
pnpm dev
curl http://localhost:3000/books/1
# {"id":"1","title":"Foundation","year":1951}
curl -X POST http://localhost:3000/books \
-H "authorization: Bearer demo-token" \
-H "content-type: application/json" \
-d '{"title":"Hyperion","year":1989}'
# {"id":"3","title":"Hyperion","year":1989}4. OpenAPI 仕様を生成する
ts
// scripts/dump-openapi.ts
import { mkdir, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
import { generateOpenAPI } from "@daloyjs/core/openapi";
import { buildApp } from "../src/build-app.js";
const app = buildApp();
const out = "./generated/openapi.json";
const doc = generateOpenAPI(app, {
info: { title: "Bookstore API", version: "1.0.0" },
securitySchemes: { bearer: { type: "http", scheme: "bearer" } },
});
await mkdir(dirname(out), { recursive: true });
await writeFile(out, JSON.stringify(doc, null, 2));
console.log(`wrote ${out}`);bash
pnpm gen:openapi5. Hey API の型付き SDK を生成する
ts
// openapi-ts.config.ts
import { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
input: "./generated/openapi.json",
output: { path: "./generated/client", postProcess: ["prettier"] },
plugins: ["@hey-api/client-fetch", "@hey-api/typescript", "@hey-api/sdk"],
});bash
pnpm gen
# generated/client/{client.gen.ts, sdk.gen.ts, types.gen.ts, index.ts}6. 任意の TS の呼び出し元で SDK を使う
ts
import { client } from "../generated/client/client.gen.js";
import { getBookById } from "../generated/client/sdk.gen.js";
client.setConfig({ baseUrl: "http://localhost:3000" });
const { data } = await getBookById({ path: { id: "1" } });
console.log(data?.title); // string | undefined、完全に型付け7. テストを追加する
ts
// tests/books.test.ts
import test from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../src/build-app.js";
import { runContractTests } from "@daloyjs/core/contract";
test("contract is clean", async () => {
const report = await runContractTests(buildApp());
assert.equal(report.ok, true, JSON.stringify(report.issues, null, 2));
});
test("GET /books/:id returns 200", async () => {
const res = await buildApp().request("/books/1");
assert.equal(res.status, 200);
});
test("POST /books rejects without token", async () => {
const res = await buildApp().request("/books", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ title: "Hyperion" }),
});
assert.equal(res.status, 401);
});bash
pnpm test作ったもの
- 型付けされ、検証され、保護された HTTP API。
- 本物の OpenAPI 3.1 ドキュメントと自動生成された型付き SDK。両者は常に同期しています。
- CI で食い違いを防ぐコントラクトテスト。
- pnpm と厳格な
.npmrcで強化したインストール手順。
続けてセキュリティ、マルチユーザー認可のチュートリアル(英語)、Adapters(英語)、API リファレンス(英語)をお読みください。
最終更新: