UNPKG

chanfana

Version:

OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!

274 lines (215 loc) 11.8 kB
# Defining Endpoints The `OpenAPIRoute` class is the cornerstone of building APIs with Chanfana. It provides a structured and type-safe way to define your API endpoints, including their schemas and logic. This guide will delve into the details of using `OpenAPIRoute` to create robust and well-documented endpoints. ## Understanding the `OpenAPIRoute` Class `OpenAPIRoute` is an abstract class that serves as the base for all your API endpoint classes in Chanfana. To create an endpoint, you will extend this class and implement its properties and methods. **Key Components of an `OpenAPIRoute` Class:** * **`schema` Property:** This is where you define the OpenAPI schema for your endpoint. It's an object that specifies the structure of the request (body, query, params, headers) and the possible responses. The `schema` is crucial for both OpenAPI documentation generation and request validation. * **`handle(...args: any[])` Method:** This **asynchronous** method contains the core logic of your endpoint. It's executed when a valid request is received. The arguments passed to `handle` depend on the router adapter you are using (e.g., Hono's `Context` object). You are expected to return a `Response` object, a Promise that resolves to a `Response`, or a plain JavaScript object (which Chanfana will automatically convert to a JSON response). * **`getValidatedData<S = any>()` Method:** This **asynchronous** method is available within your `handle` method. It allows you to access the validated request data. It returns a Promise that resolves to an object containing the validated `body`, `query`, `params`, and `headers` based on the schemas you defined in the `schema.request` property. TypeScript type inference is used to provide type safety based on your schema definition. ## Basic Endpoint Structure Here's the basic structure of an `OpenAPIRoute` class: ```typescript import { OpenAPIRoute } from 'chanfana'; import { z } from 'zod'; import { type Context } from 'hono'; class MyEndpoint extends OpenAPIRoute { schema = { // Define your OpenAPI schema here (request and responses) request: { // ... request schema (optional) }, responses: { // ... response schema (required) }, }; async handle(c: Context) { // Implement your endpoint logic here // Access validated data using this.getValidatedData() // Return a Response, Promise<Response>, or a plain object } } ``` ## Defining the `schema` The `schema` property is where you define the OpenAPI contract for your endpoint. Let's break down its components: ### Request Schema (`request`) The `request` property is an optional object that defines the structure of the incoming request. It can contain the following properties, each being a Zod schema: * **`body`:** Schema for the request body. Typically used for `POST`, `PUT`, and `PATCH` requests. You'll often use `contentJson` to define JSON request bodies. * **`query`:** Schema for query parameters in the URL. Use `z.object({})` to define the structure of query parameters. * **`params`:** Schema for path parameters in the URL path. Use `z.object({})` to define the structure of path parameters. * **`headers`:** Schema for HTTP headers. Use `z.object({})` to define the structure of headers. **Example: Request Schema with Body and Query Parameters** ```typescript import { OpenAPIRoute, contentJson } from 'chanfana'; import { z } from 'zod'; import { type Context } from 'hono'; class ExampleEndpoint extends OpenAPIRoute { schema = { request: { body: contentJson(z.object({ name: z.string().min(3), email: z.string().email(), })), query: z.object({ page: z.number().int().min(1).default(1), pageSize: z.number().int().min(1).max(100).default(20), }), }, responses: { // ... response schema }, }; async handle(c: Context) { const data = await this.getValidatedData<typeof this.schema>(); // data.body will be of type { name: string, email: string } // data.query will be of type { page: number, pageSize: number } console.log("Validated Body:", data.body); console.log("Validated Query:", data.query); return { message: 'Request Validated!' }; } } ``` ### Response Schema (`responses`) The `responses` property is a **required** object that defines the possible responses your endpoint can return. It's structured as a dictionary where keys are HTTP status codes (e.g., "200", "400", "500") and values are response definitions. Each response definition should include: * **`description`:** A human-readable description of the response. * **`content`:** (Optional) Defines the response body content. You'll often use `contentJson` to define JSON response bodies. **Example: Response Schema with Success and Error Responses** ```typescript import { OpenAPIRoute, contentJson, InputValidationException } from 'chanfana'; import { z } from 'zod'; import { type Context } from 'hono'; class AnotherEndpoint extends OpenAPIRoute { schema = { responses: { "200": { description: 'Successful operation', ...contentJson(z.object({ status: z.string().default("success"), data: z.object({ id: z.number() }), })), }, ...InputValidationException.schema(), "500": { description: 'Internal Server Error', ...contentJson(z.object({ status: z.string().default("error"), message: z.string(), })), }, }, }; async handle(c: Context) { // ... your logic ... const success = Math.random() > 0.5; if (success) { return { status: "success", data: { id: 123 } }; } else { throw new Error("Something went wrong!"); // Example of throwing an error } } } ``` ## Implementing the `handle` Method The `handle` method is where you write the core logic of your API endpoint. It's an asynchronous method that receives arguments depending on the router adapter. **Inside the `handle` method, you typically:** 1. **Access Validated Data:** Use `this.getValidatedData<typeof this.schema>()` to retrieve the validated request data. TypeScript will infer the types of `data.body`, `data.query`, `data.params`, and `data.headers` based on your schema. 2. **Implement Business Logic:** Perform the operations your endpoint is designed for (e.g., database interactions, calculations, external API calls). 3. **Return a Response:** * **Return a `Response` object directly:** You can construct a `Response` object using the built-in `Response` constructor or helper functions from your router framework (e.g., `c.json()` in Hono). * **Return a Promise that resolves to a `Response`:** If your logic is asynchronous, return a Promise that resolves to a `Response`. * **Return a plain JavaScript object:** Chanfana will automatically convert a plain JavaScript object into a JSON response with a `200 OK` status code. You can customize the status code and headers if needed by returning a `Response` object instead. **Example: `handle` Method Logic** ```typescript import { OpenAPIRoute, contentJson } from 'chanfana'; import { z } from 'zod'; import { type Context } from 'hono'; class UserEndpoint extends OpenAPIRoute { schema = { request: { params: z.object({ userId: z.string(), }), }, responses: { "200": { description: 'User details retrieved', ...contentJson(z.object({ id: z.string(), name: z.string(), email: z.string(), })), }, // ... error responses }, }; async handle(c: Context) { const data = await this.getValidatedData<typeof this.schema>(); const userId = data.params.userId; // Simulate fetching user data (replace with actual database/service call) const user = { id: userId, name: `User ${userId}`, email: `user${userId}@example.com`, }; return { ...user }; // Return a plain object, Chanfana will convert to JSON } } ``` ## Accessing Validated Data with `getValidatedData()` The `getValidatedData<S = any>()` method is crucial for accessing the validated request data within your `handle` method. **Key features of `getValidatedData()`:** * **Type Safety:** By using `getValidatedData<typeof this.schema>()`, you get strong TypeScript type inference. The returned `data` object will have properties (`body`, `query`, `params`, `headers`) that are typed according to your schema definitions. This significantly improves code safety and developer experience. * **Asynchronous Operation:** `getValidatedData()` is an asynchronous method because it performs request validation. You need to `await` its result before accessing the validated data. * **Error Handling:** If the request validation fails, `getValidatedData()` will throw a `ZodError` exception. Chanfana automatically catches this exception and returns a `400 Bad Request` response. You typically don't need to handle validation errors explicitly within your `handle` method unless you want to customize the error response further. **Example: Using `getValidatedData()`** ```typescript import { type Context } from 'hono'; async handle(c: Context) { const data = await this.getValidatedData<typeof this.schema>(); const userName = data.body.name; // TypeScript knows data.body.name is a string const pageNumber = data.query.page; // TypeScript knows data.query.page is a number // ... use validated data in your logic ... } ``` ## Example: A Simple Greeting Endpoint Let's put it all together with a simple greeting endpoint that takes a name as a query parameter and returns a personalized greeting. ```typescript import { Hono, type Context } from 'hono'; import { fromHono, OpenAPIRoute, contentJson } from 'chanfana'; import { z } from 'zod'; export type Env = { // Example bindings, use your own DB: D1Database BUCKET: R2Bucket } export type AppContext = Context<{ Bindings: Env }> class GreetingEndpoint extends OpenAPIRoute { schema = { request: { query: z.object({ name: z.string().min(1).describe("Name to greet"), }), }, responses: { "200": { description: 'Greeting message', ...contentJson(z.object({ greeting: z.string(), })), }, }, }; async handle(c: AppContext) { const data = await this.getValidatedData<typeof this.schema>(); const name = data.query.name; return { greeting: `Hello, ${name}! Welcome to Chanfana.` }; } } const app = new Hono<{ Bindings: Env }>(); const openapi = fromHono(app); openapi.get('/greet', GreetingEndpoint); export default app; ``` This example demonstrates the basic structure of an `OpenAPIRoute`, defining a schema for query parameters and responses, and implementing the endpoint logic in the `handle` method. --- In the next sections, we will explore request validation and response definition in more detail, along with the various parameter types Chanfana provides. Let's start with [**Request Validation in Detail**](./request-validation.md).