以下の内容はhttps://blog.inorinrinrin.com/entry/2025/12/25/084537より取得しました。


Honoのコードを整頓された状態にするためのeslint-pluginをリリースした

この記事はHono Advent Calendar 2025 最終日の記事です。今回は自分が作ったeslint-pluginについて書く。とりあえず作ったものを見て!!そしてLGTMならStarください!!!

github.com

きっかけ

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.getapp.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と指定する必要がある




以上の内容はhttps://blog.inorinrinrin.com/entry/2025/12/25/084537より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14