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. Theschema
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 tohandle
depend on the router adapter you are using (e.g., Hono'sContext
object). You are expected to return aResponse
object, a Promise that resolves to aResponse
, or a plain JavaScript object (which Chanfana will automatically convert to a JSON response).getValidatedData<S = any>()
Method: This asynchronous method is available within yourhandle
method. It allows you to access the validated request data. It returns a Promise that resolves to an object containing the validatedbody
,query
,params
, andheaders
based on the schemas you defined in theschema.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:
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 forPOST
,PUT
, andPATCH
requests. You'll often usecontentJson
to define JSON request bodies.query
: Schema for query parameters in the URL. Usez.object({})
to define the structure of query parameters.params
: Schema for path parameters in the URL path. Usez.object({})
to define the structure of path parameters.headers
: Schema for HTTP headers. Usez.object({})
to define the structure of headers.
Example: Request Schema with Body and Query Parameters
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 usecontentJson
to define JSON response bodies.
Example: Response Schema with Success and Error Responses
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',
content: contentJson(z.object({
status: z.string().default("success"),
data: z.object({ id: z.number() }),
})),
},
...InputValidationException.schema(),
"500": {
description: 'Internal Server Error',
content: 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:
- Access Validated Data: Use
this.getValidatedData<typeof this.schema>()
to retrieve the validated request data. TypeScript will infer the types ofdata.body
,data.query
,data.params
, anddata.headers
based on your schema. - Implement Business Logic: Perform the operations your endpoint is designed for (e.g., database interactions, calculations, external API calls).
- Return a Response:
- Return a
Response
object directly: You can construct aResponse
object using the built-inResponse
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 aResponse
. - 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 aResponse
object instead.
- Return a
Example: handle
Method Logic
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',
content: 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 returneddata
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 toawait
its result before accessing the validated data. - Error Handling: If the request validation fails,
getValidatedData()
will throw aZodError
exception. Chanfana automatically catches this exception and returns a400 Bad Request
response. You typically don't need to handle validation errors explicitly within yourhandle
method unless you want to customize the error response further.
Example: Using getValidatedData()
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.
import { Hono, type Context } from 'hono';
import { fromHono, OpenAPIRoute } 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',
content: 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.