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
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
noExplicitAnyis OFF —anyis 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:
import { z } from "zod";
import type { AnyZodObject, RouteParameter } from "./types";
// or mixed:
import { MetaGenerator, type MetaInput, type O } from "./types";
Naming:
- Classes:
PascalCase— suffixedEndpointorException(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/hasprefix (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():
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:
// 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:
buildRequest({ method, path })— plain object for itty-router (addjson: () => ({...})for body)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:
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:
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