chanfana

repository·main·Indexed 20 days ago

https://github.com/cloudflare/chanfana

An OpenAPI 3 and 3.1 schema generator and validator designed for lightweight routers such as Hono and itty-router, optimized for Cloudflare Workers. It provides tools for defining type-safe endpoints using Zod, extracting schemas via CLI, and implementing advanced patterns like nested routers, custom serializers, and endpoint interceptors.

Tokens
65.4K
Snippets
151
Records
206
Agent score
72%

What's inside chanfana

  1. What is Chanfana?

    main

    Chanfana (formerly itty-router-openapi) is a TypeScript library designed to bring OpenAPI capabilities to web APIs. It is optimized for modern JavaScript runtimes, specifically Cloudflare Workers, but works in any environment.

    Chanfana allows you to:

    • Define API contracts using TypeScript classes and Zod schemas.
    • Automatically generate OpenAPI v3 and v3.1 compliant schemas from your code.
    • Enforce request validation against your defined schemas.
    • Serve interactive documentation via Swagger UI and ReDoc.
    • Extend existing router applications like Hono and itty-router.
  2. Overview of Chanfana features

    main

    Chanfana is an OpenAPI 3 and 3.1 schema generator and validator designed for web frameworks like Hono and itty-router. It provides several core capabilities for building type-safe APIs:

    • OpenAPI Schema Generation: Automatically generates OpenAPI v3 & v3.1 compliant schemas from TypeScript API endpoint definitions.
    • Automatic Request Validation: Enforces API contracts by validating incoming requests against defined schemas.
    • Class-Based Endpoints: Allows organizing API logic using structured classes for better reusability.
    • Auto CRUD Endpoints: Reduces boilerplate by automatically generating endpoints for common CRUD operations.
    • TypeScript Type Inference: Provides automatic type inference for request parameters to ensure a type-safe developer experience.
    • Router Adapters: Supports seamless integration with popular routers such as Hono and itty-router.
  3. Overview of D1 Endpoints in Chanfana

    main

    Chanfana provides specialized endpoint classes that integrate directly with Cloudflare D1, Cloudflare's serverless SQL database. These classes extend the standard auto CRUD endpoints to provide built-in logic for database interactions while retaining features like schema generation and validation.

    The available D1-specific endpoint classes are:

    • D1CreateEndpoint: Handles resource creation in D1.
    • D1ReadEndpoint: Handles resource retrieval from D1.
    • D1UpdateEndpoint: Handles resource updates in D1.
    • D1DeleteEndpoint: Handles resource deletion from D1.
    • D1ListEndpoint: Handles listing resources with D1-optimized filtering and pagination.
  4. Define input types with Zod schemas

    main

    Chanfana uses native Zod schemas to define input types for API endpoints. This approach provides TypeScript type safety, built-in validation, and automatic OpenAPI documentation generation for request bodies, query parameters, path parameters, and headers.

    Key benefits include:

    • Type safety: Full TypeScript inference for validated data.
    • Validation: Built-in Zod validation logic.
    • OpenAPI generation: Automatic schema documentation.
    • Flexibility: Access to all Zod features.
    import { OpenAPIRoute, contentJson } from 'chanfana';
    import { z } from 'zod';
    
    class MyEndpoint extends OpenAPIRoute {
        schema = {
            request: {
                params: z.object({ id: z.uuid() }),
                query: z.object({ search: z.string().optional() }),
                headers: z.object({ 'X-Custom-Header': z.string() }),
                body: contentJson(z.object({ name: z.string() }))
            },
            responses: {
                '200': { description: 'Success' }
            }
        };
    
        async handle(c) {
            const data = await this.getValidatedData<typeof this.schema>();
            // data.params.id, data.query.search, etc. are typed
        }
    }
  5. Use Middleware and Interceptors in Chanfana

    main

    Chanfana integrates with the middleware capabilities of your underlying router (Hono or itty-router).

    Router Middleware

    You can apply middleware for authentication, logging, or request modification. When using Hono, you can pass middleware directly into the Chanfana route registration method: openapi.get('/path', middleware, EndpointClass);

    Endpoint Interceptors

    Chanfana's auto endpoints (e.g., CreateEndpoint, ReadEndpoint, UpdateEndpoint, DeleteEndpoint, ListEndpoint) provide lifecycle methods that act as interceptors. You can override these to perform actions before or after the core logic:

    • before: Executes before the core logic (useful for authorization or data validation).
    • after: Executes after the core logic (useful for post-processing or logging).

    Refer to the specific documentation for each auto-endpoint type to see the full list of available lifecycle methods.

    import { Hono } from 'hono';
    import { fromHono, OpenAPIRoute } from 'chanfana';
    
    const authMiddleware = async (c, next) => {
        const apiKey = c.req.header('X-API-Key');
        if (apiKey !== 'valid-api-key') {
            return c.json({ success: false, message: 'Unauthorized' }, 401);
        }
        await next();
    };
    
    class ProtectedEndpoint extends OpenAPIRoute {
        schema = { responses: { "200": { description: 'Protected resource' } } };
        async handle(c: any) { return { message: 'Protected data' }; }
    }
    
    const app = new Hono();
    const openapi = fromHono(app);
    
    // Apply middleware to a specific route
    openapi.get('/protected', authMiddleware, ProtectedEndpoint);
    
    export default app;
  6. Define the `Meta` object for Auto Endpoints

    main

    To use auto endpoints, you must define a Meta object for each endpoint. This object describes your data model, including its schema, primary keys, and database table name. The Meta object is assigned to the endpoint class via the _meta property.

    MetaInput Structure

    type MetaInput = {
        model: Model;
        fields?: AnyZodObject; // Optional, defaults to model.schema
        pathParameters?: Array<string>; // Optional, to explicitly define path parameters
        tags?: Array<string>; // Optional, OpenAPI tags for grouping operations
    };
    
    type Model = {
        tableName: string; // Required, the database table name
        schema: AnyZodObject; // Zod schema defining the data model
        primaryKeys: Array<string>; // Array of primary key field names
        serializer?: (obj: object, context?: SerializerContext) => object; // Optional serializer function
        serializerSchema?: AnyZodObject; // Optional schema for serialized output
    };

    Key Properties

    • model.tableName (required): The database table name (used by D1 endpoints for SQL generation).
    • model.schema (required): A Zod schema defining the data model structure for validation and schema generation.
    • model.primaryKeys (required): An array of strings representing the primary key fields. Used by ReadEndpoint, UpdateEndpoint, and DeleteEndpoint to identify resources.
    • model.serializer (optional): A function to transform data before it is sent in a response. It receives the object and a SerializerContext (which includes filters and options like pagination/ordering).
    • model.serializerSchema (optional): A Zod schema for the serialized output. If provided, it documents the structure of the response after the serializer has run.
    • fields (optional): A Zod schema representing all possible fields. Defaults to model.schema. Use this if your database table has more columns than you want to expose via the API.
    • pathParameters (optional): An array of strings to explicitly define which URL parameters correspond to the model's primary keys. This is essential for nested routes.
    • tags (optional): OpenAPI tags for grouping operations in documentation (e.g., Swagger UI).
    type MetaInput = {
        model: Model;
        fields?: AnyZodObject;
        pathParameters?: Array<string>;
        tags?: Array<string>;
    };
    
    type Model = {
        tableName: string;
        schema: AnyZodObject;
        primaryKeys: Array<string>;
        serializer?: (obj: object, context?: SerializerContext) => object;
        serializerSchema?: AnyZodObject;
    };
  7. Use Zod v4 syntax for Chanfana v3

    main

    Chanfana v3 requires Zod v4 syntax. Using Zod v3 syntax for certain validation methods will result in errors or deprecation warnings.

    FeatureZod v3 (Incorrect)Zod v4 (Correct)
    Emailz.string().email()z.email()
    UUIDz.string().uuid()z.uuid()
    Datetimez.string().datetime()z.iso.datetime()
    Datez.string().date()z.iso.date()
    URLz.string().url()z.url()
    IP v4z.string().ip({ version: 'v4' })z.ipv4()
    Strict Objectz.object({}).strict()z.strictObject({})
    Enumz.nativeEnum(MyEnum)z.enum(['option1', 'option2'])
  8. How MultiException manages multiple errors

    main

    The MultiException class is used to group multiple ApiException instances together, allowing you to report several errors (like multiple validation failures) in a single response.

    Key properties:

    • errors: An Array<ApiException> containing the grouped exceptions.
    • status: The highest HTTP status code among the contained exceptions (defaults to 400).
    • isVisible: Defaults to true, but becomes false if any contained exception has isVisible: false.
    • buildResponse(): Returns a combined array of error objects from all contained exceptions.
    import { MultiException, InputValidationException, OpenAPIRoute, contentJson } from 'chanfana';
    import { z } from 'zod';
    import { type Context } from 'hono';
    
    class ValidateMultipleFieldsEndpoint extends OpenAPIRoute {
        schema = {
            request: {
                body: contentJson(z.object({
                    field1: z.string().min(5),
                    field2: z.number().positive(),
                    field3: z.boolean(),
                })),
            },
            responses: {
                ...InputValidationException.schema(), // Document HTTP 400 error
            },
        };
    
        async handle(c: Context) {
            const data = await this.getValidatedData<typeof this.schema>();
            const field1 = data.body.field1;
            const field2 = data.body.field2;
            const field3 = data.body.field3;
    
            const errors: ApiException[] = [];
    
            if (field1.length > 10) {
                errors.push(new InputValidationException("Field 1 is too long", ['body', 'field1']));
            }
            if (field2 > 100) {
                errors.push(new InputValidationException("Field 2 is too large", ['body', 'field2']));
            }
    
            if (errors.length > 0) {
                throw new MultiException(errors);
            }
    
            return { message: 'Validation successful' };
        }
    }
  9. How Chanfana adapters work

    main

    Chanfana is router-agnostic. It uses Adapters to bridge its OpenAPI functionality (schema generation, validation, and documentation) with specific JavaScript web routers.

    Adapters provide:

    1. Router Integration: Seamlessly connecting OpenAPI features to the router's mechanism.
    2. Request Handling Abstraction: Allowing Chanfana's core logic to remain independent of router-specific request/response details.
    3. Middleware Compatibility: Ensuring Chanfana works alongside the target router's existing middleware ecosystem.

    Currently, Chanfana supports two main adapters:

    • Hono Adapter (fromHono)
    • Itty Router Adapter (fromIttyRouter)
  10. Prevent SQL injection in D1 endpoints

    main

    D1 endpoints in Chanfana include built-in security utilities to prevent SQL injection. All SQL queries use parameterized statements, and all identifiers (table names, column names) are validated before use. To maintain security, follow these best practices:

    1. Always use parameterized queries: Never interpolate user input directly into SQL strings.
    2. Validate all identifiers: Use validateTableName and validateColumnName for any dynamic table or column names.
    3. Use buildSafeFilters: For building WHERE clauses from user-provided filter conditions.
    4. Define constraintsMessages: Map database constraint violations to user-friendly error messages.
    5. Use logging: Pass a logger to handleDbError to track database errors for debugging.
  11. How `OpenAPIRoute` works as the building block for APIs

    main

    The OpenAPIRoute class is the primary way to define API endpoints in Chanfana. Instead of writing raw route handlers, you extend this class to create structured, schema-driven endpoints.

    Key components of an OpenAPIRoute subclass:

    • schema property: Defines the OpenAPI contract, including request (body, query, params, headers) and responses (status codes and content).
    • handle() method: The core logic of your endpoint. It only executes if the incoming request passes validation.
    • Data Access: Use this.getValidatedData() to access data that has been parsed and transformed by Zod, or this.getUnvalidatedData() to see the raw input before defaults are applied.
    class MyEndpoint extends OpenAPIRoute {
      schema = {
        request: { /* ... */ },
        responses: { /* ... */ }
      };
    
      async handle() {
        // Implementation logic here
      }
    }
  12. Distinguish between `getValidatedData()` and `getUnvalidatedData()`

    main

    When using Zod schemas with .default() values, getValidatedData() will always include those default values in the returned object, even if the client did not send the field. This can make it difficult to perform partial updates (e.g., PATCH requests) where you only want to update fields explicitly provided by the user.

    To distinguish between a field being 'absent' and a field being 'sent with a default value', use getUnvalidatedData(). This method returns the raw request data before Zod applies any transformations or defaults.

    class UpdateUser extends OpenAPIRoute {
      schema = {
        request: {
          body: contentJson(z.object({
            name: z.string().optional(),
            status: z.string().default('active'),
          })),
        },
      };
    
      async handle() {
        const validated = await this.getValidatedData();
        // validated.body = { status: 'active' } - default applied even if not sent
    
        const raw = await this.getUnvalidatedData();
        // raw.body = {} - shows what was actually in the request
    
        if ('status' in raw.body) {
          // User explicitly sent status field
        }
      }
    }