This article is Day 12 of the Solo SaaS Development - Design, Implementation, and Operations Advent Calendar 2025.
Yesterday's article covered "Why I Migrated from MPA to SPA." Today, I'll explain why I migrated from Next.js Route Handler to Hono and the benefits it brought.
π― Why I Migrated from Route Handler
Route Handler makes it easy to create APIs, but as the project grew, several challenges became apparent.
Route Handler Challenges
1. Directory Structure Constraints
With Route Handler, the directory structure under app/api/ directly maps to URL paths.
app/api/
βββ users/
β βββ route.ts β GET /api/users
β βββ [id]/
β βββ route.ts β GET /api/users/123
βββ contents/
β βββ route.ts β GET /api/contents
β βββ [id]/
β βββ route.ts β GET /api/contents/456
β βββ comments/
β βββ route.ts β GET /api/contents/456/comments
As endpoints increase, the app/api/ directory becomes bloated. When trying to share utility functions or validation logic, it becomes unclear where to place files.
2. Code Duplication
Each route.ts file tends to have similar validation and error handling code.
// app/api/users/route.ts
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Validation
if (!body.name || typeof body.name !== 'string') {
return NextResponse.json({ error: 'Invalid name' }, { status: 400 });
}
// Processing...
} catch (error) {
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
}
// app/api/contents/route.ts
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Similar validation...
if (!body.title || typeof body.title !== 'string') {
return NextResponse.json({ error: 'Invalid title' }, { status: 400 });
}
// Processing...
} catch (error) {
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
}
3. Manual API Documentation Management
Creating OpenAPI documentation requires writing definition files separately from the implementation. When the implementation changes, the documentation needs to be updated too, making it prone to divergence.
π₯ Why I Chose Hono
Hono is a lightweight and fast web framework.
The following points were decisive factors.
1. Directory Structure Freedom
When integrating Hono with Next.js, you only need minimal connection code in app/api/, while the API logic can be freely organized in server/api/.
app/api/
βββ [[...route]]/
βββ route.ts # Hono connection only (a few lines)
server/api/
βββ index.ts # Hono app main
βββ routes/
β βββ users.ts # User-related APIs
β βββ contents.ts # Content-related APIs
β βββ admin.ts # Admin APIs
βββ middleware/
βββ auth.ts # Auth middleware
βββ error.ts # Error handling
You can organize files by feature and place shared processing in appropriate locations.
2. Automatic Documentation with Zod OpenAPI
Using @hono/zod-openapi, you can define request/response types with Zod schemas while automatically generating OpenAPI documentation.
import { createRoute, z } from '@hono/zod-openapi';
// Request/Response schema definitions
const CreateUserSchema = z.object({
name: z.string().min(1).openapi({ example: 'John Doe' }),
email: z.string().email().openapi({ example: 'john@example.com' }),
});
const UserResponseSchema = z.object({
id: z.string().openapi({ example: 'user_123' }),
name: z.string(),
email: z.string(),
createdAt: z.string().datetime(),
});
// Route definition (types and docs generated simultaneously)
const createUserRoute = createRoute({
method: 'post',
path: '/users',
request: {
body: {
content: { 'application/json': { schema: CreateUserSchema } },
},
},
responses: {
201: {
description: 'User created successfully',
content: { 'application/json': { schema: UserResponseSchema } },
},
400: {
description: 'Validation error',
},
},
});
Since implementation and documentation are always in sync, there's no worry about divergence.
3. Middleware for Shared Processing
Authentication and error handling can be defined as middleware and reused.
// server/api/middleware/auth.ts
import { createMiddleware } from 'hono/factory';
export const authMiddleware = createMiddleware(async (c, next) => {
const session = await getSession(c.req.header('Authorization'));
if (!session) {
return c.json({ error: 'Unauthorized' }, 401);
}
c.set('user', session.user);
await next();
});
// server/api/index.ts
import { OpenAPIHono } from '@hono/zod-openapi';
import { authMiddleware } from './middleware/auth';
const app = new OpenAPIHono();
// Apply middleware to routes requiring auth
app.use('/users/*', authMiddleware);
app.use('/contents/*', authMiddleware);
// Public APIs without middleware
app.route('/public', publicRoutes);
β‘ Implementation
Integration with Next.js
To integrate Hono with Next.js, use the hono/vercel adapter.
// app/api/[[...route]]/route.ts
import { handle } from 'hono/vercel';
import { app } from '@/server/api';
export const GET = handle(app);
export const POST = handle(app);
export const PUT = handle(app);
export const DELETE = handle(app);
[[...route]] is a catch-all segment that routes all requests under /api/* to Hono. Just these few lines complete the Next.js connection.
Separating Auth API
When using authentication libraries like Better Auth, you can separate the auth endpoints.
app/api/
βββ [[...route]]/ # Hono proxy (main API)
βββ auth/ # Better Auth (auth only)
β βββ [...all]/
βββ webhooks/ # Webhooks (Stripe, etc.)
βββ stripe/
By separating endpoints based on their nature, each part can be managed independently.
Error Handling
With Hono, error handling can be defined in one place.
// server/api/index.ts
import { OpenAPIHono } from '@hono/zod-openapi';
const app = new OpenAPIHono();
// Zod validation error handling
app.onError((err, c) => {
if (err instanceof z.ZodError) {
return c.json({
error: 'Validation Error',
details: err.errors,
}, 400);
}
// Other errors
console.error(err);
return c.json({ error: 'Internal Server Error' }, 500);
});
// 404 handling
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404);
});
Error handling that was scattered across files in Route Handler is now centralized in one place.
π Migration Benefits
Here's a summary of the benefits from migrating Route Handler to Hono.
| Item | Before (Route Handler) | After (Hono) |
|---|---|---|
| Directory Structure | Constrained by URL structure | Freely organizable |
| Validation | Manual implementation | Declarative with Zod |
| Type Safety | Manual type definitions | Auto-inferred from Zod |
| API Documentation | Manual management | Auto-generated |
| Error Handling | Duplicated across files | Centralized in middleware |
Improved Development Efficiency
- Easier endpoint additions: When adding new routes, existing schemas and middleware can be reused
- Early type error detection: Request/response types are inferred from Zod schemas
- No documentation updates needed: Documentation is automatically updated when implementation changes
OpenAPI Documentation Usage
OpenAPI documentation can be automatically generated from defined routes.
// server/api/index.ts
app.doc('/doc', {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
},
});
Accessing /api/doc returns the OpenAPI specification JSON. This JSON can be imported into tools like Swagger UI or Apidog for endpoint listing and request testing.
Improved Testability
Since Hono applications are framework-independent, tests are easier to write.
import { app } from '@/server/api';
describe('Users API', () => {
it('should create a user', async () => {
const res = await app.request('/users', {
method: 'POST',
body: JSON.stringify({ name: 'Test', email: 'test@example.com' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status).toBe(201);
});
});
π‘ Migration Tips
1. Migrate Gradually
You don't need to migrate everything at once. You can implement new endpoints with Hono while gradually migrating existing Route Handlers.
2. Consider Separating Auth and Webhooks
When libraries provide dedicated handlers, like Better Auth or Stripe Webhooks, it's an option to maintain them as separate endpoints rather than forcing integration with Hono.
β Summary
Here's what improved from migrating Route Handler to Hono.
Solved Challenges:
- Directory structure constraints β Free organization in
server/api/ - Code duplication β Middleware and schema reuse
- Manual documentation management β Auto-generated with Zod OpenAPI
Benefits Gained:
- Improved type safety (auto-inference from Zod)
- Improved development efficiency (easier endpoint additions)
- Improved testability (framework-independent)
Hono works well with Next.js, resolving Route Handler challenges while enabling more structured API development.
Tomorrow's article will cover "Vercel Optimization."
Other articles in this series
- 12/11: Why I Migrated from MPA to SPA: App Router Refactoring in Practice
- 12/13: Vercel Optimization: Reducing Build Time and Improving Response
Top comments (0)