chanfana
Version:
OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!
138 lines (105 loc) • 6.27 kB
Markdown
# AGENTS.md — Chanfana
Chanfana is an OpenAPI 3/3.1 schema generator and request validator for Hono and itty-router on Cloudflare Workers. It uses Zod v4 for schemas and provides auto CRUD endpoints with D1 support.
## Commands
```bash
npm test # Run all tests (tsc + vitest)
npm test -- coverage-gaps.test.ts # Run a single test file by name
npm test -- --coverage # Run with Istanbul coverage report
npm run build # Build CJS + ESM to dist/
npm run lint # Lint via Biome (auto-fixes on failure)
```
Tests run inside `@cloudflare/vitest-pool-workers` with a real D1 binding. Config: `tests/vitest.config.mts`.
## Project Layout
```
src/
adapters/ # Router adapters: hono.ts, ittyRouter.ts
endpoints/ # Auto CRUD: create, read, update, delete, list
d1/ # D1-specific implementations + SQL utilities (base.ts)
zod/ # OpenAPI registry merger
openapi.ts # Core handler: route registration, schema generation, doc UI
route.ts # Base OpenAPIRoute class (validation, lifecycle, error handling)
parameters.ts # Query/path param coercion (string → number/boolean/BigInt/Date)
exceptions.ts # ApiException hierarchy (12 exception classes, codes 7000–7012)
types.ts # Shared TypeScript types
index.ts # Barrel re-exports (export * from every module)
tests/integration/ # All tests (no unit test directory)
docs/ # VitePress documentation
skills/ # AI coding skills (write-endpoints)
```
## Code Style
**Formatter/Linter**: Biome (`biome.json`). Key settings:
- 2-space indent, 120 char line width, double quotes, always semicolons, trailing commas on multiline
- `noExplicitAny` is OFF — `any` is used deliberately throughout
**TypeScript** (`tsconfig.json`): `strict: true`, `verbatimModuleSyntax: true` (requires `import type` for type-only imports), target ES2022, bundler module resolution.
**Imports**: Biome auto-organizes. External packages first (alphabetical), then relative imports. No blank lines between groups. Use `import type` for type-only:
```typescript
import { z } from "zod";
import type { AnyZodObject, RouteParameter } from "./types";
// or mixed:
import { MetaGenerator, type MetaInput, type O } from "./types";
```
**Naming**:
- Classes: `PascalCase` — suffixed `Endpoint` or `Exception` (`CreateEndpoint`, `NotFoundException`)
- Functions/methods/variables: `camelCase` (`getValidatedData`, `coerceInputs`)
- Module-level constants: `SCREAMING_SNAKE_CASE` (`HIJACKED_METHODS`)
- User-facing config keys: `snake_case` (`docs_url`, `openapi_url`, `default_message`)
- Booleans: `is`/`has` prefix (`isVisible`, `isRoute`, `includesPath`)
- Unused params: `_` prefix (`_args`, `_e`, `_oldObj`)
**Types**: Return types are generally inferred. Parameters are always explicitly typed. Class properties always have explicit types or initializers. `@ts-expect-error` is used when needed (e.g., `_meta` in endpoint subclasses).
**Error handling**: Exception class hierarchy rooted at `ApiException extends Error`. Each has `buildResponse()` and `static schema()`. Pattern in `execute()`:
```typescript
try {
resp = await this.handle(...args);
} catch (e) {
if (this.params?.raiseOnError) throw e;
const errorResponse = formatChanfanaError(e);
if (errorResponse) return errorResponse;
throw e; // unknown error: re-throw
}
```
**Async**: Always `async/await`, never `.then()` chains. Use `for...of` for iteration, never `for...in`.
**Exports**: All named, no default exports. Barrel file `src/index.ts` uses `export *`.
## Zod v4 Rules (Critical)
All code must use Zod v4 syntax. Common mistakes:
```typescript
// WRONG (v3) → CORRECT (v4)
z.string().email() → z.email()
z.string().uuid() → z.uuid()
z.string().datetime() → z.iso.datetime()
z.string().date() → z.iso.date()
z.string().url() → z.url()
z.nativeEnum(X) → z.enum([...])
obj.strict() → z.strictObject({...})
{ message: "..." } → { error: "..." }
z.ZodTypeAny → z.ZodType
```
## Testing Patterns
Tests live in `tests/integration/`. Framework: Vitest with `describe`/`it` (not `test`).
**Two request-building approaches**:
1. `buildRequest({ method, path })` — plain object for itty-router (add `json: () => ({...})` for body)
2. `new Request(url, { method, body, headers })` — for Hono or when body/headers needed
**Test endpoint classes** are defined inline at the top of test files, before `describe` blocks. Named descriptively with `Endpoint` suffix (`FalsyDefaultsEndpoint`, `ThrowNotFoundEndpoint`).
**D1 tests** use `import { env } from "cloudflare:test"` and pass `env` as second arg to `router.fetch()`. Setup with raw SQL in `beforeEach`.
**Standard assertions**:
```typescript
expect(request.status).toBe(200);
expect(resp.success).toBe(true);
expect(resp.result).toEqual({ ... });
expect(resp.errors[0].code).toBe(7001);
```
## Architecture Quick Reference
**OpenAPIRoute lifecycle**: `execute()` → reset caches → `handle()` → catch errors → auto-JSON-wrap response
**Auto endpoints** (`CreateEndpoint`, `ReadEndpoint`, etc.) require a `_meta` property with `model.schema` (Zod), `model.primaryKeys`, and `model.tableName` (for D1). Optional `tags` for OpenAPI grouping. Support `before()`/`after()` hooks.
**Router adapters** (`fromHono`, `fromIttyRouter`) return a Proxy that intercepts route registration to capture OpenAPI metadata, then delegates to the underlying router.
**D1 endpoints** extend the base CRUD classes with SQL generation. Use parameterized queries exclusively. `d1/base.ts` provides `validateSqlIdentifier()`, `buildSafeFilters()`, `buildPrimaryKeyFilters()`, `handleDbError()`.
## Changesets
This project uses `@changesets/cli`. Add a changeset for user-facing changes:
```bash
npx changeset # Interactive prompt
```
For internal-only changes (tests, CI), use an empty changeset (frontmatter only, no package entry).
## Key Resources
- Docs: https://chanfana.pages.dev
- Source: https://github.com/cloudflare/chanfana
- Detailed coding skill: `skills/write-endpoints/SKILL.md`
- Zod v4: https://zod.dev/v4