Restful API
Learn how to create and organize API routes in your VitNode plugins with modules, handlers, and parameter validation.
Building a RESTful API in VitNode is like assembling LEGO blocks - you create modules, add routes to them, and connect everything together. Let's dive into this architectural adventure!
Setting Up Your API Structure
Create Module
Think of modules as containers for related API endpoints. They help organize your routes logically - perfect for keeping your sanity intact!
import { buildModule } from '@vitnode/core/api/lib/module';
import { CONFIG_PLUGIN } from '@/config';
export const categoriesModule = buildModule({
pluginId: CONFIG_PLUGIN.id,
name: 'categories',
routes: [], // We'll populate this soon!
});
Nested Modules
Want to create a module hierarchy? VitNode's got your back! Nested modules are perfect for complex APIs.
import { buildModule } from '@vitnode/core/api/lib/module';
import { CONFIG_PLUGIN } from '@/config';
import { postsModule } from './posts/posts.module';
export const categoriesModule = buildModule({
pluginId: CONFIG_PLUGIN.id,
name: 'categories',
routes: [],
modules: [postsModule],
});
This creates a beautiful structure: /api/{plugin_id}/categories/posts/*
Craft Routes
Now for the fun part - creating actual endpoints! Each route is a small but mighty function that handles HTTP requests.
import { z } from '@hono/zod-openapi';
import { buildRoute } from '@vitnode/core/api/lib/route';
import { CONFIG_PLUGIN } from '@/config';
export const getCategoriesRoute = buildRoute({
...CONFIG_PLUGIN,
route: {
method: 'get',
path: '/',
responses: {
200: {
content: {
'application/json': {
schema: z.object({
categories: z.array(
z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
}),
),
}),
},
},
description: 'Successfully retrieved categories',
},
},
},
handler: c => {
// Your business logic goes here
return c.json({
categories: [
{ id: '1', name: 'Technology', description: 'All things tech' },
{ id: '2', name: 'Lifestyle' },
],
});
},
});
Connect Everything
The final step is connecting your modules to your plugin's API configuration. It's like plugging in the last cable!
import { buildApiPlugin } from '@vitnode/core/api/lib/plugin';
import { CONFIG_PLUGIN } from '@/config';
import { categoriesModule } from './api/modules/categories/categories.module';
export const blogApiPlugin = () => {
return buildApiPlugin({
...CONFIG_PLUGIN,
modules: [categoriesModule],
});
};
Don't forget to add your routes to the module:
import { buildModule } from '@vitnode/core/api/lib/module';
import { CONFIG_PLUGIN } from '@/config';
import { getCategoriesRoute } from './routes/get.route';
export const categoriesModule = buildModule({
pluginId: CONFIG_PLUGIN.id,
name: 'categories',
routes: [getCategoriesRoute],
});
Working with Parameters
Path Parameters
Path parameters are perfect when you need to identify specific resources. They're like the ID card of your API endpoints!
import { z } from '@hono/zod-openapi';
import { buildRoute } from '@vitnode/core/api/lib/route';
import { HTTPException } from 'hono/http-exception';
import { CONFIG_PLUGIN } from '@/config';
export const getCategoryByIdRoute = buildRoute({
...CONFIG_PLUGIN,
route: {
method: 'get',
path: '/{id}', // Dynamic path parameter
request: {
params: z.object({
id: z.string().openapi({
description: 'Unique identifier for the category',
example: 'tech-category-123',
}),
}),
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
}),
},
},
description: 'Category details retrieved successfully',
},
404: {
description: 'Category not found',
},
},
},
handler: c => {
const { id } = c.req.valid('param'); // Extract the path parameter
// Simulate database lookup
if (id === 'nonexistent') {
throw new HTTPException(404);
}
return c.json({
id,
name: `Category ${id}`,
description: 'A fantastic category for amazing content',
});
},
});
Query Parameters
Query parameters are your best friends for filtering, searching, and pagination. They make your API flexible and user-friendly!
import { z } from '@hono/zod-openapi';
import { buildRoute } from '@vitnode/core/api/lib/route';
import { CONFIG_PLUGIN } from '@/config';
export const searchCategoriesRoute = buildRoute({
...CONFIG_PLUGIN,
route: {
method: 'get',
path: '/search',
request: {
query: z.object({
search: z.string().optional().openapi({
description: 'Search term to filter categories',
example: 'technology',
}),
}),
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
categories: z.array(
z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
}),
),
total: z.number(),
}),
},
},
description: 'Search results with pagination info',
},
},
},
handler: c => {
const { search } = c.req.valid('query');
// Your search logic here
const mockResults = [
{ id: '1', name: 'Technology', description: 'Tech-related posts' },
{ id: '2', name: 'Lifestyle' },
];
const filteredResults = search
? mockResults.filter(cat =>
cat.name.toLowerCase().includes(search.toLowerCase()),
)
: mockResults;
const paginatedResults = filteredResults.slice(offset, offset + limit);
return c.json({
categories: paginatedResults,
total: filteredResults.length,
});
},
});
Request Body (JSON Payload)
When you need to send complex data (creating, updating), request bodies are your go-to solution. Perfect for forms and JSON payloads!
import { z } from '@hono/zod-openapi';
import { buildRoute } from '@vitnode/core/api/lib/route';
import { CONFIG_PLUGIN } from '@/config';
const createCategorySchema = z.object({
name: z.string().min(1).max(100).openapi({
description: 'Name of the category',
example: 'Web Development',
}),
description: z.string().optional().openapi({
description: 'Optional description for the category',
example: 'Everything about building websites and web applications',
}),
color: z
.string()
.regex(/^#[0-9A-F]{6}$/i)
.optional()
.openapi({
description: 'Hex color code for the category',
example: '#3B82F6',
}),
});
export const createCategoryRoute = buildRoute({
...CONFIG_PLUGIN,
route: {
method: 'post',
path: '/',
request: {
body: {
content: {
'application/json': {
schema: createCategorySchema,
},
},
description: 'Category data to create',
required: true,
},
},
responses: {
201: {
content: {
'application/json': {
schema: z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
color: z.string().optional(),
createdAt: z.string(),
}),
},
},
description: 'Category created successfully',
},
400: {
description: 'Invalid input data',
},
},
},
handler: async c => {
const data = c.req.valid('json');
// Simulate category creation
const newCategory = {
id: `cat_${Date.now()}`,
...data,
createdAt: new Date().toISOString(),
};
return c.json(newCategory, 201);
},
});