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

> Learn to add new CRUD endpoints in Hono.js with OpenAPI. Define entities DTOs use cases repositories controllers and routes for automatic OpenAPI spec generation.

- Repository: [Lucas Zanoni⠀⠀⠀⠀⠀ ⠀╱|、 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ (˚ˎ 。7 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ |、˜〵 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ じしˍ,)ノ/tcc](https://github.com/castrozan/tcc)
- Tags: how-to-guide
- Published: 2026-02-27

---

**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.

```typescript
// 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/main/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.

```typescript
// 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/main/CreateProjectDto.ts)](https://github.com/castrozan/tcc/blob/master/src/application/dtos/projects/CreateProjectDto.ts) • [[`UpdateProjectDto.ts`](https://github.com/castrozan/tcc/blob/main/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.

```typescript
// 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/main/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.

```typescript
// 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/main/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.

```typescript
// 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/main/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.

```typescript
// 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`](https://github.com/castrozan/tcc/blob/main//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/main/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.

```typescript
// 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`](https://github.com/castrozan/tcc/blob/main/src/infrastructure/web/open-api/server.ts).
- **Automatic documentation**: Once registered in [`server.ts`](https://github.com/castrozan/tcc/blob/main/server.ts), the `fromHono` function exposes your endpoints at [`/openapi.json`](https://github.com/castrozan/tcc/blob/main//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`](https://github.com/castrozan/tcc/blob/main/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`](https://github.com/castrozan/tcc/blob/main//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.