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