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
آخر نشاط في المصدر
٢٥ فبراير ٢٠٢٦ في ١٦:٣٤
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٧٧٤
التفرعات
٧٣

خيارات التثبيت

يُحدَّد 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