Next.js開発者で、APIルートの定義を簡素化しつつ、型安全性、バリデーション、ドキュメント作成を維持したいと考えているなら、nextjs-route-decorator
がまさに必要なツールかもしれません。この記事では、このTypeScriptベースのライブラリが提供する機能を探り、実践的な例を通じてその使い方を解説し、API開発をよりクリーンで効率的、かつ保守しやすくする方法を紹介します。
nextjs-route-decoratorとは?
nextjs-route-decorator
は、Next.jsのAPIルートをデコレーター中心のアプローチで定義することを目的としたTypeScriptライブラリです。NestJSやJavaのSpringフレームワークに慣れている方なら、デコレーターがもたらす宣言的なスタイルに親しみを感じるでしょう。ルーティング、バリデーション、ミドルウェアのために繰り返しボイラープレートコードを書く代わりに、このライブラリを使えばTypeScriptデコレーターを用いてAPIロジックを簡潔かつ読みやすく定義できます。
主な特徴
nextjs-route-decorator
が際立つポイントは以下の通りです:
- デコレーター構文:モダンなTypeScriptデコレーターを使用して、ルート、メソッド、パラメータ、ミドルウェアをクリーンかつ宣言的に定義します。
- Next.jsとのシームレスな統合:Next.jsのAPIルート向けに設計されており、フレームワークのファイルベースのルーティングシステムと自然に連携します。
-
型安全性とバリデーション:TypeScriptと
zod
(ピア依存関係)を活用し、リクエストパラメータ、クエリ文字列、ボディペイロードの実行時バリデーションを提供し、ランタイムエラーを削減します。 - Swaggerドキュメントの自動生成:ルートデコレーターからSwagger/OpenAPIドキュメントを自動生成し、APIドキュメントを最小限の労力で最新に保ちます。
- ミドルウェアサポート:CORS、ロギング、スロットリングなどのミドルウェアをデコレーターベースで簡単に統合できます。
なぜAPIルートにデコレーターを使うのか?
デコレーターに初めて触れる方は、最初は魔法のように感じるかもしれません。しかし実際には、クラスやメソッドにメタデータや動作を宣言的に追加する手段に過ぎません。nextjs-route-decorator
の場合、デコレーターを使うことでコントローラークラス上で直接APIルート、バリデーションルール、ミドルウェアを定義でき、繰り返し必要なセットアップコードを削減できます。
このアプローチが革新的な理由は以下の通りです:
- ボイラープレートの削減:冗長なルーティングロジックやミドルウェアの配線を書く必要がなく、デコレーターが代わりに処理します。
- 可読性の向上:API定義が簡潔で自己説明的になり、チームがコードを理解し保守しやすくなります。
- 一貫性:デコレーターにより、バリデーション、ミドルウェア、ドキュメントがAPI全体で一貫して適用されます。
それでは、実際の例を通じてその仕組みを見てみましょう。
導入手順:インストール
Next.jsプロジェクトでnextjs-route-decorator
を使用するには、ライブラリとそのピア依存関係をインストールする必要があります。以下のコマンドを実行してください:
npm install nextjs-route-decorator
プロジェクトに以下のピア依存関係が既にインストールされていることを確認してください:
-
next
(バージョン13.2以降) -
typescript
(バージョン5以降) zod
また、tsconfig.json
で実験的デコレーターサポートを有効にする必要があります:
{
"compilerOptions": {
// ...existing options...
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
例:ユーザー登録APIの構築
実践的な例として、ユーザー登録用のAPIルートを作成してみましょう。コントローラーを定義し、リクエストボディをバリデーションし、成功レスポンスを返すプロセスを、nextjs-route-decorator
を使って実装します。
ファイルの作成
以下のようにファイルを用意します:
./src/app/api/[…params]/user.schema.ts
import { z } from "zod";
export const UserObject = z.object({
id: z.string().default("1").describe("User ID"),
username: z.string().min(3, "Username must be at least 3 characters long"),
email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters long"),
});
export const RegisterUser = UserObject.omit({
id: true,
});
export const User = UserObject.omit({ password: true });
export const Users = z.array(User);
export type RegisterUser = z.infer<typeof RegisterUser>;
export type User = z.infer<typeof User>;
export type Users = z.infer<typeof Users>;
./src/app/api/[…params]/user.controller.ts
import {
Controller,
Post,
Body,
StatusCode,
Get,
inject,
Param,
Put,
Delete,
} from "nextjs-route-decorator";
import { z } from "zod";
import { RegisterUser, User, Users } from "./user.schema";
import { UserService } from "./user.service";
import { NextResponse } from "next/server";
@Controller("/user", {
name: "User Api Documentation",
description: "User management API",
})
export class UserController {
constructor(@inject(UserService) private userService: UserService) {}
@Get("/", {
response: {
200: Users,
},
info: {
id: "list-users",
summary: "Get all users",
description: "Get a list of all registered users",
},
})
async getUsers() {
const users = await this.userService.getUsers();
return users;
}
@Get("/:id", {
response: {
200: User,
},
})
async getUserById(@Param("id") id: string) {
const user = await this.userService.getUserById(id);
if (!user) {
return NextResponse.json({ message: "User not found" }, { status: 404 });
}
return user;
}
@Post("/", {
body: RegisterUser,
response: {
201: z.object({
success: z.boolean(),
message: z.string(),
}),
},
})
@StatusCode(201)
async register(@Body body: RegisterUser) {
this.userService.registerUser(body);
return {
success: true,
message: `User ${body.username} registered successfully!`,
};
}
@Put("/:id", {
path: User.pick({ id: true }),
body: RegisterUser,
response: {
200: z.object({
success: z.boolean(),
message: z.string(),
}),
},
})
async updateUser(@Param("id") id: string, @Body body: RegisterUser) {
await this.userService.updateUser(id, body);
return {
success: true,
message: `User ${id} updated successfully!`,
};
}
@Delete("/:id", {
path: User.pick({ id: true }),
response: {
200: z.object({
success: z.boolean(),
message: z.string(),
}),
},
})
async deleteUser(@Param("id") id: string) {
const deleted = await this.userService.deleteUser(id);
if (!deleted) {
return NextResponse.json({ message: "User not found" }, { status: 404 });
}
return {
success: true,
message: `User ${id} deleted successfully!`,
};
}
}
./src/app/api/[…params]/user.service.ts
import { injectable } from "nextjs-route-decorator";
import type { RegisterUser, Users, User } from "./user.schema";
@injectable()
export class UserService {
async getUsers(): Promise<Users> {
return [
{
id: "1",
username: "user1",
email: "user1@example.com",
},
{
id: "2",
username: "user2",
email: "user2@example.com",
},
];
}
async registerUser(user: RegisterUser): Promise<User> {
return {
id: Math.random().toString(36).substring(2, 15),
username: user.username,
email: user.email,
};
}
async getUserById(id: string): Promise<User | null> {
const users = await this.getUsers();
return users.find((user) => user.id === id) || null;
}
async updateUser(
id: string,
user: Partial<RegisterUser>
): Promise<User | null> {
const users = await this.getUsers();
const existingUser = users.find((u) => u.id === id);
if (existingUser) {
return { ...existingUser, ...user };
}
return null;
}
async deleteUser(id: string): Promise<boolean> {
const users = await this.getUsers();
const index = users.findIndex((user) => user.id === id);
if (index !== -1) {
users.splice(index, 1);
return true;
}
return false;
}
}
./src/app/api/[…params]/user.module.ts
import { Module } from "nextjs-route-decorator";
import { UserController } from "./user.controller";
@Module({
controllers: [UserController],
prefix: "/api",
})
export default class UserModule {}
-
./src/app/api/[…params]/route.ts
でルートを定義します:
import { RouterFactory } from "nextjs-route-decorator";
import UserModule from "./user.module";
export const { GET, POST, PUT, DELETE } = RouterFactory.create(UserModule, {
swagger: {
path: "/api/swagger",
info: {
title: "NextJS App Router API Documentation",
version: "1.0.0",
},
servers: [
{
url: "http://localhost:3000",
description: "ローカルサーバー",
},
...(process.env.APP_URL
? [
{
url: `https://${process.env.APP_URL}`,
description: "Vercelサーバー",
},
]
: []),
],
},
});
アプリを起動すると、http://localhost:3000/api/swagger
でSwagger UIにアクセスできます。このページでは、ルート、リクエストスキーマ、レスポンスの詳細を含む美しくインタラクティブなAPIドキュメントが表示され、すべてデコレーターから自動生成されます。
nextjs-route-decoratorを使用するメリット
次回のNext.jsプロジェクトでnextjs-route-decorator
を検討すべき理由は以下の通りです:
- クリーンなコードベース:デコレATORSにより冗長なルーティングやミドルウェアのセットアップが不要になり、API定義が簡潔で保守しやすくなります。
- 生産性の向上:バリデーションとSwaggerドキュメントの自動生成を備えたAPIエンドポイントを迅速にプロトタイプ化できます。
-
信頼性の向上:TypeScriptと
zod
による型安全性と実行時バリデーションでエラーリスクを低減します。 - シームレスな統合:Next.js向けに作られており、既存のワークフローに完璧に適合します。
- 拡張性:ネストされたモジュール、カスタムミドルウェア、カスタムデコレーターの追加やライブラリの拡張が容易です。
結論
nextjs-route-decorator
は、API開発プロセスを効率化したいNext.js開発者にとって強力なツールです。TypeScriptデコレーターを活用することで、APIルートに構造、明確さ、一貫性をもたらし、バリデーション、ミドルウェア、Swaggerドキュメント生成などの重要なタスクを処理します。
試してみる準備はできましたか?Next.jsプロジェクトにnextjs-route-decorator
をインストールし、デコレーターを使ったAPIルートの定義を始めてみてください。さらに高度な使い方については、プロジェクトのリポジトリで追加の例やカスタマイズオプションを確認できます。