Skip to main content

write-endpoints

Comprehensive guide for building OpenAPI endpoints with chanfana - schema definition, request validation, CRUD operations, D1 database integration, and exception handling

来源信息

仓库
cloudflare/chanfana
最近来源活动
2026年2月25日 16:34
检测到的 SKILL.md 语言
英语
星标
768
Forks
71

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
write-endpoints
description
Comprehensive guide for building OpenAPI endpoints with chanfana - schema definition, request validation, CRUD operations, D1 database integration, and exception handling
# Writing OpenAPI Endpoints with Chanfana ## When to Use Use this skill when: - Building OpenAPI endpoints with chanfana for Cloudflare Workers - Defining request/response schemas with Zod v4 - Creating CRUD auto endpoints (Create, Read, Update, Delete, List) - Integrating with Cloudflare D1 databases - Implementing error handling with exception classes ## Part 1: Fundamentals ### Quick Start with Hono ```typescript import { Hono, type Context } from 'hono'; import { fromHono, OpenAPIRoute, contentJson } from 'chanfana'; import { z } from 'zod'; export type Env = { DB: D1Database; }; export type AppContext = Context<{ Bindings: Env }>; class HelloEndpoint extends OpenAPIRoute { schema = { responses: { "200": { description: 'Successful response', ...contentJson(z.object({ message: z.string() })), }, }, }; async handle(c: AppContext) { return { message: 'Hello, Chanfana!' }; } } const app = new Hono<{ Bindings: Env }>(); const openapi = fromHono(app); openapi.get('/hello', HelloEndpoint); export default app; ``` ### Quick Start with itty-router ```typescript import { Router } from 'itty-router'; import { fromIttyRouter, OpenAPIRoute, contentJson } from 'chanfana'; import { z } from 'zod'; class HelloEndpoint extends OpenAPIRoute { schema = { responses: { "200": { description: 'Successful response', ...contentJson(z.object({ message: z.string() })), }, }, }; async handle(request: Request, env, ctx) { return { message: 'Hello, Chanfana!' }; } } const router = Router(); const openapi = fromIttyRouter(router); openapi.get('/hello', HelloEndpoint); router.all('*', () => new Response("Not Found.", { status: 404 })); export const fetch = router.handle; ``` ### Schema Definition Define request validation for body, query, params, and headers: ```typescript import { OpenAPIRoute, contentJson } from 'chanfana'; import { z } from 'zod'; class CreateUserEndpoint extends OpenAPIRoute { schema = { request: { body: contentJson(z.object({ username: z.string().min(3).max(20), password: z.string().min(8), email: z.email(), fullName: z.string().optional(), })), query: z.object({ notify: z.boolean().optional().default(true), }), params: z.object({ orgId: z.uuid(), }), headers: z.object({ 'X-API-Key': z.string(), }), }, responses: { "200": { description: 'User created successfully', ...contentJson(z.object({ id: z.uuid(), username: z.string(), email: z.email(), })), }, "400": { description: 'Validation error', ...contentJson(z.object({ success: z.literal(false), errors: z.array(z.object({ code: z.number(), message: z.string(), })), })), }, }, }; async handle(c) { const data = await this.getValidatedData<typeof this.schema>(); // data.body, data.query, data.params, data.headers are all typed return { id: crypto.randomUUID(), username: data.body.username, email: data.body.email }; } } ``` ### Zod v4 Syntax (CRITICAL) Chanfana v3 uses Zod v4. Use the correct syntax: ```typescript // WRONG - Zod v3 syntax (deprecated) z.string().email() z.string().uuid() z.string().datetime() z.string().date() z.string().url() z.string().ip({ version: "v4" }) z.object({}).strict() z.nativeEnum(MyEnum) // CORRECT - Zod v4 syntax z.email() z.uuid() z.iso.datetime() z.iso.date() z.url() z.ipv4() z.strictObject({}) z.enum(['option1', 'option2']) ``` ### Common Zod Types for APIs Use native Zod schemas for all parameter types: ```typescript import { z } from 'zod'; // String with constraints const nameSchema = z.string() .min(3) .max(50) .describe("User's name") .openapi({ example: 'John Doe' }); // Number with range const priceSchema = z.number() .min(0) .describe('Product price') .openapi({ example: 99.99 }); // Integer const ageSchema = z.number() .int() .min(0) .max(120) .describe("User's age"); // Boolean with default const isActiveSchema = z.boolean() .default(true) .describe('User active status'); // Date/time (ISO 8601) const createdAtSchema = z.iso.datetime() .describe('Creation timestamp') .openapi({ example: '2024-01-20T10:30:00Z' }); // Date only (YYYY-MM-DD) const birthDateSchema = z.iso.date() .describe('Birth date') .openapi({ example: '1990-05-15' }); // Email, UUID const emailSchema = z.email().describe('Email address'); const userIdSchema = z.uuid().describe('User ID'); // Enumeration const statusSchema = z.enum(['pending', 'processing', 'shipped', 'delivered']) .default('pending') .describe('Order status'); // Array const tagsSchema = z.array(z.string()).openapi({ description: 'Tags', }); // Object const addressSchema = z.object({ street: z.string().describe('Street address'), city: z.string().describe('City'), zipCode: z.string().describe('Zip code'), }); // Regex pattern const phoneSchema = z.string() .regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number format') .describe('Phone number'); // IP addresses const ipv4Schema = z.ipv4(); const ipv6Schema = z.ipv6(); const ipSchema = z.union([z.ipv4(), z.ipv6()]); // Hostname (regex pattern) const hostnameSchema = z.string().regex( /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/ ); ``` ### Validated Data Access Always use `await` with `getValidatedData()`: ```typescript class MyEndpoint extends OpenAPIRoute { async handle(c) { // CORRECT - with await and type annotation const data = await this.getValidatedData<typeof this.schema>(); // Type-safe access const username = data.body.username; const page = data.query.page; const userId = data.params.userId; const apiKey = data.headers['X-API-Key']; return { success: true }; } } ``` ### Using getUnvalidatedData() for Partial Updates In Zod v4, optional fields with `.default()` always have values in validated data. Use `getUnvalidatedData()` to detect what was actually sent: ```typescript class UpdateUser extends OpenAPIRoute { schema = { request: { body: contentJson(z.object({ name: z.string().optional(), status: z.enum(['active', 'inactive']).default('active'), })), }, }; async handle() { const validated = await this.getValidatedData<typeof this.schema>(); // validated.body.status is 'active' even if not sent const raw = await this.getUnvalidatedData(); // raw.body = {} if nothing was sent // Check what was actually sent const updates: Record<string, any> = {}; if ('name' in raw.body) updates.name = validated.body.name; if ('status' in raw.body) updates.status = validated.body.status; return { updated: updates }; } } ``` ## Part 2: CRUD Auto Endpoints ### Meta Object Definition All auto endpoints require a `_meta` property: ```typescript import { z } from 'zod'; // Define the model schema const UserSchema = z.object({ id: z.uuid(), username: z.string().min(3).max(20), email: z.email(), role: z.enum(['user', 'admin']), createdAt: z.iso.datetime(), }); // Define the meta object const userMeta = { model: { schema: UserSchema, // Required: Zod schema for the model primaryKeys: ['id'], // Required: Array of primary key fields tableName: 'users', // Required for D1 endpoints serializer: (user: any) => { // Optional: Transform output const { passwordHash, ...safe } = user; return safe; }, serializerSchema: UserSchema.omit({ passwordHash: true }), // Optional: Schema for serialized output }, pathParameters: ['id'], // Optional: Explicit path params for nested routes tags: ['Users'], // Optional: OpenAPI tags for grouping operations }; ``` ### CreateEndpoint ```typescript import { CreateEndpoint, type O } from 'chanfana'; class CreateUser extends CreateEndpoint { _meta = userMeta; // Optional: Pre-processing hook async before(data: O<typeof this._meta>): Promise<O<typeof this._meta>> { return { ...data, id: crypto.randomUUID(), createdAt: new Date().toISOString(), }; } // Required: Create logic async create(data: O<typeof this._meta>) { await db.users.insert(data); return data; } // Optional: Post-processing hook async after(data: O<typeof this._meta>): Promise<O<typeof this._meta>> { await sendWelcomeEmail(data.email); return data; } } // Register route openapi.post('/users', CreateUser); ``` ### ReadEndpoint ```typescript import { ReadEndpoint, type Filters, type O } from 'chanfana'; class GetUser extends ReadEndpoint { _meta = userMeta; async before(filters: Filters): Promise<Filters> { // Pre-fetch validation return filters; } async fetch(filters: Filters): Promise<O<typeof this._meta> | null> { const userId = filters.filters[0].value; return await db.users.findById(userId); } async after(data: O<typeof this._meta>): Promise<O<typeof this._meta>> { // Post-fetch processing return data; } } // Register route with path parameter openapi.get('/users/:id', GetUser); ``` ### ListEndpoint ```typescript import { ListEndpoint, type ListFilters, type ListResult, type O } from 'chanfana'; class ListUsers extends ListEndpoint { _meta = userMeta; // Configure filtering, search, and sorting filterFields = ['role', 'status']; // Exact match filtering searchFields = ['username', 'email']; // Full-text search (LIKE) orderByFields = ['createdAt', 'username']; // Available sort fields
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看