この記事はHono Advent Calendar 2025 最終日の記事です。今回は自分が作ったeslint-pluginについて書く。とりあえず作ったものを見て!!そしてLGTMならStarください!!!
きっかけ
Honoは非常に柔軟で軽量なWebフレームワークだけど、自由度が高いゆえにチーム開発や規模が大きくなった際にコードの書き方が統一されにくいという課題に直面することがある。特にAIにコーディングをさせているとこの傾向は顕著だ。これは自分がHono Conference 2025での発表内容で登場したプロジェクトを進めているときの経験談からきている*1。
そこで思いついたのがESLintでガードレールを設けること。
そして自作する前にさらっと調べてみたところ、まだ誰かが作って世界に公開している様子はなさげ。ってことで「お、自分で作るか」となった。
インストールと設定
まずはインストールから*2。
npm install -D eslint-plugin-hono@alpha
ESLintのフラット設定(eslint.config.js)を使用する場合の設定例は以下の通り。
import globals from "globals"; import pluginJs from "@eslint/js"; import tseslint from "typescript-eslint"; import hono from "eslint-plugin-hono"; export default [ { plugins: { hono: hono, }, }, pluginJs.configs.recommended, ...tseslint.configs.recommended, ...hono.configs.recommended, { files: ["**/*.{ts,tsx,cts,mts}"], languageOptions: { parser: tseslint.parser, parserOptions: { ecmaVersion: "latest", sourceType: "module", project: "./tsconfig.json", }, globals: globals.node, }, }, ];
自作したルールの紹介
ここからは、eslint-plugin-hono が提供するルールのうち、特に自分でも気に入っている4つのルールについて紹介する。それぞれの意図と Good/Bad パターンを解説する。
route-grouping
ルート定義を整理整頓するためのルール。Honoでは app.get や app.post を自由に書ける。けど、パスごと、あるいはインスタンスごとに定義が散らばっていると可読性が下がると自分は思っている。なので、このルールは以下をチェックする。
- 同じパス (
/api/v1) に対するメソッド (GET,POST) はまとめて書く。 GET->POST->PUT->DELETEのような一貫した順序を強制する。
👎 Not LGTM
const app = new Hono(); // 同じパスなのに定義が離れている app.get('/path1', (c) => c.text('get')); app.get('/path2', (c) => c.text('get')); app.post('/path1', (c) => c.text('post')); // 同じrouteに対しては必ず決まった順番で app.post('/path3', (c) => c.text('post')); app.get('/path3', (c) => c.text('get'));

👍 LGTM
const app = new Hono(); // パスごとにまとまり、GET -> POST の順になっている app.get('/path1', (c) => c.text('get')); app.post('/path1', (c) => c.text('post')); app.get('/path2', (c) => c.text('get')); app.get('/path3', (c) => c.text('get')); app.post('/path3', (c) => c.text('post'));

global-middleware-placement
「グローバルミドルウェアはルート定義よりも前に記述すべき」というルール。
基本的にapp.use('*', ...) のようなグローバルミドルウェアは同じファイル内であればどこでも自由に書ける。が、無秩序にミドルウェアが書かれていくのは個人的に好きではない。特にグローバルミドルウェアは全てのルートに対する副作用だと考えているので、最初にその影響があることを頭に入れてからソースを読み進めたい。
なので、とあるHonoインスタンスへのグローバルミドルウェアは、そのインスタンスが定義された直後かつ、get, postなどの定義が始まる前に書かれていてほしい。これによって可読性が上がると考えてのルール。
👎 Not LGTM
import { serve } from "@hono/node-server"; import { Hono } from "hono"; import { logger } from "hono/logger"; const app = new Hono(); const book = new Hono(); app.get('/', (c) => c.text('Hello')); book.get('/', (c) => c.text('Book Home')); // いろいろ定義があって... // 不意にここでミドルウェアを書く app.use('*', logger()); book.use('*', logger()); serve(app);

👍 LGTM
import { serve } from "@hono/node-server"; import { Hono } from "hono"; import { logger } from "hono/logger"; const app = new Hono(); const book = new Hono(); app.use("*", logger()); app.get("/", (c) => c.text("Hello")); book.use("*", logger()); book.get("/", (c) => c.text("Book Home")); serve(app);

param-name-mismatch
個人的に一番気に入っているルール。
ルート定義のパスパラメータ(例: :id)と、ハンドラ内で取得する際のパラメータ名(c.req.param('id'))が一致しているかをチェックする。
単純なタイポや、仕様変更時の修正漏れを防ぐのに役立つはず。
👎 Not LGTM
import { Hono } from "hono"; const app = new Hono(); // 定義は :postId だが、取得しようとしているのは 'id' app.get("/posts/:postId", (c) => { const id = c.req.param("id"); return c.text(`Post ID is ${id}`); });

👍 LGTM
import { Hono } from "hono"; const app = new Hono(); app.get("/posts/:postId", (c) => { const id = c.req.param("postId"); // ここを正しくした return c.text(`Post ID is ${id}`); });

prefer-http-exception
HTTPのエラーレスポンスを返す際に、標準の Error ではなく Hono の HTTPException を使うよう促すルール。結構Hono初心者向けかも。
throw new Error('Not Found') としても、処理が終わるわけではなく、ましてや404がレスポンスされるわけではない。実際にはInternal Server Errorで終了する。HTTPException を使うことで、Honoが適切にレスポンスを生成してくれるので、そっちへ誘導する。
👎 Not LGTM
import { serve } from "@hono/node-server"; import { Hono } from "hono"; const app = new Hono(); app.get("/posts/:postId", (c) => { const id = c.req.param("postId"); if (id === "1") { // 400ではなくInternal Server Errorになる throw new Error("Bad Request"); } return c.text(`Post ID is ${id}`); }); serve(app);

👍 LGTM
import { HTTPException } from 'hono/http-exception'; // ステータスコードとメッセージを明示できる throw new HTTPException(404, { message: 'Not Found' }); throw new HTTPException(401, { message: 'Unauthorized' });

ただこのルールは現時点ではNot Found'のようにErrorに特定の文言を渡している場合にしか検知できないので、改善の余地はまだまだある状態...
おわりに
eslint-plugin-hono は、Honoを使った開発をより安全により快適にするためのツールで、AIや人間、誰が書いても整頓されたコードになってくれるのを期待する。
現状はまだこのルールたちが予期せぬ動きをしないことを実践でガリガリ使って試し切れてる自信がないので、Alpha版として公開している。なので、ぜひ使ってみて、バグ報告や機能要望などを リポジトリ に送ってほしい。そしてこの記事を読んでみた結果だったり、実際に使ってみたりした結果、応援の気持ちが芽生えたらぜひStarをください!
Happy Hono Coding!! 🔥
*1:Hono Conference 2025の登壇内容はこちら。https://blog.inorinrinrin.com/entry/2025/10/18/162046
*2:現状はAlpha版として公開しているため、タグをalphaと指定する必要がある