🔥

Honoで始める軽量モジュール分割のフォルダ構成

に公開

本記事のサマリ

NestJSの美しいモジュール分割に慣れ親しんだ皆さん、Honoについて「軽量だけど綺麗に分割とかできないんじゃないの?」と考えていませんか?

私自身それでHonoを敬遠してきたのですが、一度しっかりと試してみようと思い、今回Honoでモジュール分割を試してみました。結論から言うと綺麗なモジュール構造を、ちゃんとシンプルに実現できます。

これが同じような疑問を持つ方のHonoへの入口になればと思います。本記事では、単一ファイルから段階的にモジュール化する実践的な手順をお伝えします。

(Githubに今回のコードも上げてますので、是非ご覧ください😉)

https://github.com/toto-inu/lab-202510-nestjs_vs_hono

なぜHonoなのか - どこでも動く軽量フレームワーク

Honoは、Yusuke Wada氏によって開発された軽量APIフレームワークです。

https://hono.dev/

最大の特徴は「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が最適解になるでしょう。

https://hono.dev/getting-started/basic

技術選択において最も重要なのは、チームの生産性とプロダクトの成長速度です。Honoなら、シンプルさを保ちながら、しっかりとした構造でアプリケーションを育てていけますね!

(DIとかも次回試してみたい...せっかく軽量なのに重くなるでしょうか...🤔)

株式会社StellarCreate | Tech blog📚

Discussion