chanfana
Version:
OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!
274 lines (215 loc) • 11.8 kB
Markdown
# 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).