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!

plugins/blog/src/api/modules/categories/categories.module.ts
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.

plugins/blog/src/api/modules/categories/categories.module.ts
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.

plugins/blog/src/api/modules/categories/routes/get.route.ts
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!

plugins/blog/src/config.api.ts
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:

plugins/blog/src/api/modules/categories/categories.module.ts
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!

plugins/blog/src/api/modules/categories/routes/get_by_id.route.ts
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!

plugins/blog/src/api/modules/categories/routes/search.route.ts
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!

plugins/blog/src/api/modules/categories/routes/create.route.ts
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);
  },
});