How to Add New CRUD Endpoints in Hono.js with OpenAPI: A Complete Guide

To add new CRUD endpoints in Hono.js with OpenAPI, define your domain entity, create DTOs for type safety, implement use-cases for business logic, build a repository for persistence, wire everything together in a Hono controller, and register the route in the server file where chanfana automatically generates the OpenAPI specification.

The castrozan/tcc repository demonstrates a production-ready pattern for building type-safe REST APIs using Hono.js (a lightweight web framework optimized for edge runtimes) and chanfana (a helper that introspects Hono routes to produce OpenAPI 3 documentation). This guide walks through the exact six-layer architecture used in the codebase to add a complete Project resource with Create, Read, Update, and Delete operations.

Step 1: Define the Domain Entity

Start by modeling your business object in the domain layer. This entity represents the core data structure independent of any framework or database.

// src/domain/entities/projects/Project.ts
export class Project {
  constructor(
    public readonly id: string,
    public name: string,
    public description?: string,
  ) {}
}

Reference: [Project.ts](https://github.com/castrozan/tcc/blob/master/src/domain/entities/projects/Project.ts)

Step 2: Create DTOs for OpenAPI Documentation

Data Transfer Objects (DTOs) define the shape of incoming requests and outgoing responses. These interfaces ensure type safety across layers and provide metadata for OpenAPI generation.

// src/application/dtos/projects/CreateProjectDto.ts
export interface CreateProjectDto {
  name: string;
  description?: string;
}

// src/application/dtos/projects/UpdateProjectDto.ts
export interface UpdateProjectDto {
  name?: string;
  description?: string;
}

Reference: [CreateProjectDto.ts](https://github.com/castrozan/tcc/blob/master/src/application/dtos/projects/CreateProjectDto.ts) • [UpdateProjectDto.ts](https://github.com/castrozan/tcc/blob/master/src/application/dtos/projects/UpdateProjectDto.ts)

Step 3: Implement Use-Cases

Use-cases encapsulate business logic and orchestrate between the domain and infrastructure layers. Each operation (Create, Find, Update, Delete) receives its own use-case class following the Single Responsibility Principle.

// src/application/use-cases/projects/CreateProjectUseCase.ts
import { IProjectRepository } from '@/domain/interfaces/projects/IProjectRepository';
import { CreateProjectDto } from '@/application/dtos/projects/CreateProjectDto';
import { Project } from '@/domain/entities/projects/Project';
import { v4 as uuid } from 'uuid';

export class CreateProjectUseCase {
  constructor(private repo: IProjectRepository) {}

  async execute(dto: CreateProjectDto): Promise<Project> {
    const project = new Project(uuid(), dto.name, dto.description);
    await this.repo.save(project);
    return project;
  }
}

Create analogous classes for FindAllProjectsUseCase, FindProjectByIdUseCase, UpdateProjectUseCase, and DeleteProjectUseCase following the same pattern.

Reference: [CreateProjectUseCase.ts](https://github.com/castrozan/tcc/blob/master/src/application/use-cases/projects/CreateProjectUseCase.ts)

Step 4: Add Repository Interface and Implementation

Define a repository interface in the domain layer to abstract persistence details, then provide a concrete implementation in the infrastructure layer.

// src/domain/interfaces/projects/IProjectRepository.ts
import { Project } from '@/domain/entities/projects/Project';

export interface IProjectRepository {
  save(project: Project): Promise<void>;
  findAll(): Promise<Project[]>;
  findById(id: string): Promise<Project | null>;
  update(id: string, data: Partial<Project>): Promise<void>;
  delete(id: string): Promise<void>;
}

For the concrete implementation, follow the existing Professional repository pattern in src/infrastructure/database/sqlite/ to create a SQLite-backed repository that implements IProjectRepository.

Reference: [IProjectRepository.ts](https://github.com/castrozan/tcc/blob/master/src/domain/interfaces/projects/IProjectRepository.ts)

Step 5: Build the Hono Controller

Controllers act as HTTP adapters, translating Hono's Context into DTOs and invoking the appropriate use-case.

// src/presentation/controllers/projects/CreateProjectController.ts
import { Context } from 'hono';
import { CreateProjectUseCase } from '@/application/use-cases/projects/CreateProjectUseCase';
import { CreateProjectDto } from '@/application/dtos/projects/CreateProjectDto';

export class CreateProjectController {
  constructor(private useCase: CreateProjectUseCase) {}

  async handle(c: Context) {
    const dto: CreateProjectDto = await c.req.json();
    const entity = await this.useCase.execute(dto);
    return c.json(entity, 201);
  }
}

Create corresponding controllers for listing, retrieving by ID, updating, and deleting projects. Use the existing src/presentation/controllers/professionals/* files as blueprints for consistent error handling and response formatting.

Reference: [CreateProjectController.ts](https://github.com/castrozan/tcc/blob/master/src/presentation/controllers/projects/CreateProjectController.ts)

Step 6: Register Routes and Generate OpenAPI Spec

The final step wires your controllers to HTTP routes in the Hono application. The chanfana library (fromHono) introspects these registrations to automatically build the OpenAPI 3 specification.

// src/infrastructure/web/open-api/server.ts
import { fromHono } from 'chanfana';
import { Hono } from 'hono';
import { CreateProjectController } from '@/presentation/controllers/projects/CreateProjectController';
import { ListProjectsController } from '@/presentation/controllers/projects/ListProjectsController';
import { GetProjectController } from '@/presentation/controllers/projects/GetProjectController';
import { UpdateProjectController } from '@/presentation/controllers/projects/UpdateProjectController';
import { DeleteProjectController } from '@/presentation/controllers/projects/DeleteProjectController';
import { ProjectUseCases } from '@/application/use-cases/projects';

const app = new Hono();

// ----- Projects CRUD -----
app.post(
  '/projects',
  new CreateProjectController(ProjectUseCases.create).handle,
);
app.get(
  '/projects',
  new ListProjectsController(ProjectUseCases.list).handle,
);
app.get(
  '/projects/:id',
  new GetProjectController(ProjectUseCases.getById).handle,
);
app.put(
  '/projects/:id',
  new UpdateProjectController(ProjectUseCases.update).handle,
);
app.delete(
  '/projects/:id',
  new DeleteProjectController(ProjectUseCases.delete).handle,
);

// OpenAPI generation
const openapi = fromHono(app, {
  info: {
    title: 'tcc API',
    version: '1.0.0',
  },
});

export { app, openapi };

When you run the server and navigate to /openapi.json, chanfana exposes the new /projects paths alongside existing endpoints, complete with method definitions and path parameters.

Reference: [server.ts](https://github.com/castrozan/tcc/blob/master/src/infrastructure/web/open-api/server.ts)

Full Working Example (Minimal Setup)

Here is a condensed, copy-pasteable implementation that follows the exact architecture used in the castrozan/tcc repository. This example assumes you have already configured dependency injection or barrel exports for your use-cases.

// 1. Domain Entity
// src/domain/entities/projects/Project.ts
export class Project {
  constructor(
    public readonly id: string,
    public name: string,
    public description?: string,
  ) {}
}

// 2. DTO
// src/application/dtos/projects/CreateProjectDto.ts
export interface CreateProjectDto {
  name: string;
  description?: string;
}

// 3. Use-Case
// src/application/use-cases/projects/CreateProjectUseCase.ts
import { IProjectRepository } from '@/domain/interfaces/projects/IProjectRepository';
import { CreateProjectDto } from '@/application/dtos/projects/CreateProjectDto';
import { Project } from '@/domain/entities/projects/Project';
import { v4 as uuid } from 'uuid';

export class CreateProjectUseCase {
  constructor(private repo: IProjectRepository) {}
  
  async execute(dto: CreateProjectDto): Promise<Project> {
    const project = new Project(uuid(), dto.name, dto.description);
    await this.repo.save(project);
    return project;
  }
}

// 4. Controller
// src/presentation/controllers/projects/CreateProjectController.ts
import { Context } from 'hono';
import { CreateProjectUseCase } from '@/application/use-cases/projects/CreateProjectUseCase';

export class CreateProjectController {
  constructor(private useCase: CreateProjectUseCase) {}
  
  async handle(c: Context) {
    const dto = await c.req.json();
    const result = await this.useCase.execute(dto);
    return c.json(result, 201);
  }
}

// 5. Server Registration (excerpt)
// src/infrastructure/web/open-api/server.ts
import { Hono } from 'hono';
import { fromHono } from 'chanfana';
import { CreateProjectController } from '@/presentation/controllers/projects/CreateProjectController';
import { CreateProjectUseCase } from '@/application/use-cases/projects/CreateProjectUseCase';
import { SqliteProjectRepository } from '@/infrastructure/database/sqlite/SqliteProjectRepository';

const app = new Hono();
const projectRepo = new SqliteProjectRepository();
const createProjectUseCase = new CreateProjectUseCase(projectRepo);

app.post('/projects', new CreateProjectController(createProjectUseCase).handle);

export const openapi = fromHono(app, {
  info: { title: 'tcc API', version: '1.0.0' }
});

Summary

  • Domain-driven layers separate entities, DTOs, use-cases, and controllers, making the codebase testable and framework-agnostic.
  • Hono.js provides the HTTP routing layer optimized for edge environments, while chanfana introspects those routes to generate OpenAPI 3 specifications automatically.
  • Six-step workflow: Define the entity in src/domain/entities/, create DTOs in src/application/dtos/, implement use-cases in src/application/use-cases/, add repository interfaces in src/domain/interfaces/, build controllers in src/presentation/controllers/, and register routes in src/infrastructure/web/open-api/server.ts.
  • Automatic documentation: Once registered in server.ts, the fromHono function exposes your endpoints at /openapi.json without manual YAML or JSON editing.

Frequently Asked Questions

How does chanfana generate OpenAPI specs from Hono routes?

The fromHono function in src/infrastructure/web/open-api/server.ts inspects the Hono application's registered routes, extracting HTTP methods, paths, and handler metadata. It then constructs a valid OpenAPI 3 document that reflects your endpoint structure, available at the /openapi.json endpoint by default.

Can I customize the OpenAPI documentation for specific endpoints?

Yes. The repository includes a decorator pattern in src/presentation/decorators/ that allows you to attach metadata such as summary, tags, requestBody schemas, and responses directly to controller classes. When chanfana processes the routes, it merges this metadata into the generated specification.

What database does the repository use for the repository pattern?

The castrozan/tcc repository implements the repository pattern using SQLite for persistence. Concrete implementations like SqliteProjectRepository (following the existing Professional repository template in src/infrastructure/database/sqlite/) handle SQL queries while the domain layer remains agnostic to the database technology.

Is the controller pattern compatible with Cloudflare Workers?

Yes. Hono.js is specifically designed for edge runtimes including Cloudflare Workers, and the controller pattern used in src/presentation/controllers/ accepts Hono's Context object directly. The repository's architecture ensures that business logic in use-cases remains free of platform-specific code, making the application portable across Node.js, Deno, and edge environments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →