Honoで始める軽量モジュール分割のフォルダ構成
本記事のサマリ
NestJSの美しいモジュール分割に慣れ親しんだ皆さん、Honoについて「軽量だけど綺麗に分割とかできないんじゃないの?」と考えていませんか?
私自身それでHonoを敬遠してきたのですが、一度しっかりと試してみようと思い、今回Honoでモジュール分割を試してみました。結論から言うと綺麗なモジュール構造を、ちゃんとシンプルに実現できます。
これが同じような疑問を持つ方のHonoへの入口になればと思います。本記事では、単一ファイルから段階的にモジュール化する実践的な手順をお伝えします。
(Githubに今回のコードも上げてますので、是非ご覧ください😉)
なぜHonoなのか - どこでも動く軽量フレームワーク
Honoは、Yusuke Wada氏によって開発された軽量APIフレームワークです。
最大の特徴は「Write Once, Run Anywhere」。プロジェクトを初期化する際の体験を見てみましょう:
npm create hono@latest my-app
この時、以下のようなテンプレート選択画面が表示されます:
✔ Which template do you want to use?
nodejs (従来のサーバー)
cloudflare-workers (エッジ環境)
aws-lambda (サーバーレス)
vercel (エッジ関数)
nextjs (Next.js統合)
bun (Bunランタイム)
deno (Denoランタイム)
同じコードが、これらすべての環境で動作します。NestJSでは不可能な、真のマルチプラットフォーム対応です。
特にVercel Edge FunctionsやCloudflare Workersなどのエッジ環境では、起動の速さとバンドルサイズの小ささが重要になります。NestJSは重すぎてそもそも動作しませんが、Honoなら完璧に動作します。
モジュール分割の旅路 - 3ステップでNestJS風の整理された構造へ
「NestJSのモジュール分割は綺麗だけど、もっとシンプルにできないの?」という疑問に答えます。Honoなら段階的に、そして最小限の変更で実現できるのです。
ステップ1: 単一ファイルからスタート
// app.ts
import { Hono } from 'hono'
const app = new Hono()
// ユーザー関連API
app.get('/api/users', async (c) => {
return c.json({ users: [] })
})
app.post('/api/users', async (c) => {
const user = await c.req.json()
return c.json({ id: 1, ...user }, 201)
})
// 投稿関連API
app.get('/api/posts', async (c) => {
return c.json({ posts: [] })
})
app.post('/api/posts', async (c) => {
const post = await c.req.json()
return c.json({ id: 1, ...post }, 201)
})
export default app
ステップ2: ルーター分離(最小限の変更)
まずはルーターを分離します。たった2つのファイル追加で完了です。
// routes/users.ts
import { Hono } from 'hono'
const users = new Hono()
users.get('/', async (c) => {
return c.json({ users: [] })
})
users.post('/', async (c) => {
const user = await c.req.json()
return c.json({ id: 1, ...user }, 201)
})
export { users }
// routes/posts.ts
import { Hono } from 'hono'
const posts = new Hono()
posts.get('/', async (c) => {
return c.json({ posts: [] })
})
posts.post('/', async (c) => {
const post = await c.req.json()
return c.json({ id: 1, ...post }, 201)
})
export { posts }
// app.ts(更新)
import { Hono } from 'hono'
import { users } from './routes/users'
import { posts } from './routes/posts'
const app = new Hono()
app.route('/api/users', users)
app.route('/api/posts', posts)
export default app
ステップ3: モジュール構造への進化(NestJS風の整理)
ここからが本番です。NestJSのような、ドメインごとにまとまったモジュール構造を作りましょう。
目指す構造:
src/
├── modules/
│ ├── users/
│ │ ├── userController.ts
│ │ ├── userService.ts
│ │ └── index.ts
│ └── posts/
│ ├── postController.ts
│ ├── postService.ts
│ └── index.ts
└── app.ts
まず、各モジュールのサービスを作成:
// modules/users/userService.ts
export class UserService {
async getUsers() {
// DBアクセスやビジネスロジック
return [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
}
async createUser(userData: any) {
// ユーザー作成ロジック
return { id: 3, ...userData }
}
async getUserById(id: string) {
// ユーザー取得ロジック
return { id, name: 'John' }
}
}
次に、コントローラーを作成:
// modules/users/userController.ts
import { Hono } from 'hono'
import { UserService } from './userService'
const userService = new UserService()
export const userController = new Hono()
userController.get('/', async (c) => {
const users = await userService.getUsers()
return c.json({ users })
})
userController.post('/', async (c) => {
const userData = await c.req.json()
const user = await userService.createUser(userData)
return c.json(user, 201)
})
userController.get('/:id', async (c) => {
const id = c.req.param('id')
const user = await userService.getUserById(id)
return c.json(user)
})
モジュールのエントリーポイント:
// modules/users/index.ts
export { userController } from './userController'
export { UserService } from './userService'
同様にpostsモジュールも作成:
// modules/posts/postService.ts
export class PostService {
async getPosts() {
return [{ id: 1, title: 'First Post' }]
}
async createPost(postData: any) {
return { id: 2, ...postData }
}
}
// modules/posts/postController.ts
import { Hono } from 'hono'
import { PostService } from './postService'
const postService = new PostService()
export const postController = new Hono()
postController.get('/', async (c) => {
const posts = await postService.getPosts()
return c.json({ posts })
})
postController.post('/', async (c) => {
const postData = await c.req.json()
const post = await postService.createPost(postData)
return c.json(post, 201)
})
// modules/posts/index.ts
export { postController } from './postController'
export { PostService } from './postService'
最後に、アプリケーションのエントリーポイントで全てを統合:
// app.ts
import { Hono } from 'hono'
import { userController } from './modules/users'
import { postController } from './modules/posts'
const app = new Hono()
app.route('/api/users', userController)
app.route('/api/posts', postController)
export default app
完成です! これでNestJSのような、ドメインごとに綺麗に整理されたモジュール構造になりました。各モジュールは独立しており、テストも保守も容易です。純粋なTypeScriptのクラスとHonoのシンプルなルーティングだけで実現できました。
DIがないので、正直この分割がいるかはわかりませんが、ルーティングをしっかり分割することは大事ですね。
まとめ - シンプルさという最強の武器
Honoが提供するのは、NestJSの整理された構造を、圧倒的にシンプルな方法で実現する選択肢です。
- 段階的な成長:単一ファイル → ルーター分離 → サービス層追加
- 最小限の変更:複雑な設定やボイラープレート不要
- 真のマルチプラットフォーム:同じコードがあらゆる環境で動作
- 圧倒的な軽量性:エッジ環境でも快適に動作
「整理されたコードを書きたいけど、NestJSは重すぎる」と感じている方には、Honoが最適解になるでしょう。
技術選択において最も重要なのは、チームの生産性とプロダクトの成長速度です。Honoなら、シンプルさを保ちながら、しっかりとした構造でアプリケーションを育てていけますね!
(DIとかも次回試してみたい...せっかく軽量なのに重くなるでしょうか...🤔)
株式会社StellarCreate(stellar-create.co.jp)のエンジニアブログです。 プロダクト指向のフルスタックエンジニアを目指す方募集中です! カジュアル面談で気軽に雑談しましょう!→ recruit.stellar-create.co.jp/
Discussion