はじめに
この記事では、NestJS でデコレータが多用される理由を整理します。
NestJS を初めて触ると、次のような書き方が多いことに気付きます。
@Controller()
@Injectable()
@Module()
@Get()
@Post()
Java の Spring を触ったことがある人なら、アノテーションに近い書き方だと感じるかもしれません。
一方で、TypeScript のクラスやメソッドに @Controller() や @Get() が付いているのを見ると、最初は少し不思議に見えます。
なぜ NestJS は、関数呼び出しや設定ファイルではなく、デコレータを中心に設計されているのでしょうか。
先に結論
NestJS のデコレータは、クラスやメソッドにメタデータを付けるために使われます。
NestJS はそのメタデータを読み取り、次のような情報として扱います。
- このクラスは Controller である
- このクラスは Provider として管理できる
- このクラスは Module である
- このメソッドは GET
/users/:idを処理する - このルートには Guard や Pipe を適用する
つまり、デコレータは単なる飾りではありません。
NestJS に対して、クラスやメソッドの役割を宣言するための仕組みです。
Controller はルーティング用のメタデータを持つ
たとえば、次の Controller を考えます。
@Controller("users")
export class UserController {}
この @Controller("users") によって、NestJS はこのクラスを Controller として扱います。
さらに、ルートの基準パスが /users であることも分かります。
もしデコレータがなければ、次のコードはただの TypeScript のクラスです。
export class UserController {}
クラス名に Controller と付いていても、それは人間が読むための名前にすぎません。
フレームワークが確実に判断するには、「このクラスは Controller である」という情報が必要です。
その情報を付けるために @Controller() が使われます。
メソッドデコレータで HTTP ルートを表す
Controller の中では、メソッドにもデコレータを付けます。
@Controller("users")
export class UserController {
@Get(":id")
findById() {
return "user";
}
}
@Get(":id") によって、NestJS は次の情報を得ます。
- HTTP メソッドは GET
- パスは
/users/:id - 実行するハンドラは
findById()
NestJS は起動時に Controller やメソッドのメタデータを読み取り、リクエストとハンドラの対応関係を作ります。
公式ドキュメントでも、デコレータはクラスと必要なメタデータを結び付け、Nest がルーティングマップを作れるようにするものとして説明されています。
Gin のようにルーターへ直接登録する設計と比べる
NestJS のデコレータは、ルート定義をメソッドの近くに宣言するための仕組みです。
この特徴は、Gin のようにルーターへ直接登録するフレームワークと比べると分かりやすくなります。
Gin では、HTTP メソッド、パス、ハンドラをルーターに直接渡します。
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "user",
})
})
r.Run()
この書き方では、GET /users/:id というルートと、実行するハンドラを r.GET() に直接登録しています。
Gin のパッケージドキュメントでも、r.GET("/ping", handler) のようにルーターへエンドポイントを登録する例が示されています。
NestJS では、同じ対応関係を次のように表現します。
@Controller("users")
export class UserController {
@Get(":id")
findById() {
return "user";
}
}
違いを整理すると、次のとおりです。
| 観点 | NestJS | Gin |
|---|---|---|
| ルート定義 |
@Controller() / @Get()
|
r.GET() / r.POST()
|
| 書き方 | クラスやメソッドにメタデータを付ける | ルーターに関数を登録する |
| DI | Nest の IoC コンテナを使う | 自分で構造体などに依存を渡すことが多い |
| 構造 | Module / Controller / Provider をフレームワークに宣言する | router / handler / service などを自分で設計する |
Gin でも、Controller に近い形でハンドラを分けることはできます。
type UserHandler struct {
userService *UserService
}
func NewUserHandler(userService *UserService) *UserHandler {
return &UserHandler{
userService: userService,
}
}
func (h *UserHandler) FindByID(c *gin.Context) {
c.JSON(200, gin.H{
"message": "user",
})
}
ただし、ルート登録は別に書きます。
userService := NewUserService()
userHandler := NewUserHandler(userService)
r := gin.Default()
users := r.Group("/users")
{
users.GET("/:id", userHandler.FindByID)
}
r.Run()
Group() を使うと、共通の URL プレフィックスごとにルートをまとめられます。
Gin のルーティングドキュメントでも、API バージョン管理、共通ミドルウェア、コード整理に使えるものとして説明されています。
つまり、Gin では「このメソッドは GET のハンドラである」という情報をメソッドに付けるのではありません。
users.GET("/:id", userHandler.FindByID) のように、ルーターへ直接登録します。
この違いを見ると、NestJS のデコレータは、Gin でいうルート登録の情報をクラスやメソッドの近くに宣言しているものだと理解しやすくなります。
Provider と DI でもメタデータが使われる
NestJS では、Service や Repository のようなクラスを Provider として扱います。
@Injectable()
export class UserService {}
@Injectable() は、このクラスが Nest の IoC コンテナで管理できる Provider であることを示します。
Controller 側では、コンストラクタで依存オブジェクトを受け取ります。
@Controller("users")
export class UserController {
constructor(private readonly userService: UserService) {}
}
この書き方によって、UserController は UserService に依存していることが明確になります。
依存オブジェクトの生成や受け渡しは NestJS のランタイムが担当します。
ただし、@Injectable() を付けるだけで、どこからでも自動的に注入できるわけではありません。
通常は Module の providers に登録され、その Module のスコープ内で解決できる状態になっている必要があります。
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
このように、NestJS では Controller、Provider、Module の関係もデコレータで宣言します。
Module はアプリケーション構造を表す
NestJS では @Module() も重要です。
@Module({
imports: [],
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
@Module() は、アプリケーションの構造を NestJS に伝えるためのメタデータを持ちます。
たとえば、次のような情報です。
- この Module が持つ Controller
- この Module が持つ Provider
- 他の Module から使う Module
- 外部へ公開する Provider
NestJS はこれらの情報をもとに、依存関係の解決やアプリケーション全体の構成を管理します。
デコレータが多く見える理由は、NestJS がアプリケーションの構造をコード上に宣言させる設計だからです。
なぜ設定ファイルではなくデコレータなのか
同じことは、設定ファイルでも表現できます。
たとえば、次のような設定を別ファイルに書く設計も考えられます。
{
path: "/users/:id",
method: "GET",
controller: UserController,
handler: "findById"
}
この形でもルーティングは作れます。
ただし、Controller の実装とルート定義が離れます。
NestJS は、クラスやメソッドのすぐ近くに役割を宣言します。
@Get(":id")
findById() {
return "user";
}
この書き方では、メソッドを読んだときに「これは GET /users/:id のハンドラである」とすぐ分かります。
つまり、NestJS のデコレータ文化は、設定をコードの近くに置き、フレームワークが読み取れる形で宣言するためのものです。
Spring のアノテーションとは何が違うのか
NestJS の書き方は Spring によく似ています。
たとえば、対応関係としては次のように見えます。
| 役割 | Spring | NestJS |
|---|---|---|
| Controller | @Controller |
@Controller() |
| Service | @Service |
@Injectable() |
| Module/設定 |
@Configuration など |
@Module() |
| GET ルート | @GetMapping |
@Get() |
Spring を触ったことがある人ほど、NestJS のデコレータを「Java のアノテーションと同じもの」と見たくなるかもしれません。
ただし、見た目は似ていても、仕組みは別物です。
Spring では、Java のアノテーションを Spring コンテナや Spring MVC が読み取ります。
@Controller や @Service は、Spring が管理するコンポーネントであることを示す stereotype annotation です。
Spring の公式ドキュメントでも、@Component を汎用的な Spring 管理コンポーネントとし、@Repository、@Service、@Controller はそれぞれより具体的な用途の特殊化として説明されています。
たとえば、Spring MVC では次のように書きます。
@Controller
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public String findById(@PathVariable String id) {
return "user";
}
}
ここでは、次の情報を Spring に伝えています。
- このクラスは Controller である
-
/usersを基準パスにする -
GET /users/{id}をfindById()に割り当てる -
UserServiceをコンストラクタで注入する
Spring の @GetMapping は、@RequestMapping(method = RequestMethod.GET) を読みやすくした合成アノテーションです。
一方、NestJS は TypeScript のデコレータ構文を使います。
@Controller("users")
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(":id")
findById(@Param("id") id: string) {
return "user";
}
}
このコードでは、次の情報を NestJS に伝えています。
- このクラスは Controller である
-
/usersを基準パスにする -
GET /users/:idをfindById()に割り当てる -
UserServiceをコンストラクタで注入する
NestJS の公式ドキュメントでも、デコレータはクラスと必要なメタデータを結び付け、Nest がリクエストと Controller を対応させるルーティングマップを作れるようにするものとして説明されています。
大きな違いは、NestJS では Module が依存関係の境界として強く出てくる点です。
通常は Module 側で Controller と Provider を登録します。
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
@Injectable() は「このクラスは Provider として扱える」という印です。
ただし、それだけでアプリ全体のどこからでも使えるという意味ではありません。
通常は Module の providers に登録され、必要に応じて exports され、使う側の Module から imports されます。
整理すると、次の違いがあります。
| 観点 | Spring | NestJS |
|---|---|---|
| 仕組み | Java のアノテーション | TypeScript のデコレータ |
| 読み取る主体 | Spring コンテナや Spring MVC | NestJS ランタイム |
| クラス登録 | コンポーネントスキャンや設定クラスなど | Module の controllers / providers など |
| ルート定義 |
@RequestMapping / @GetMapping
|
@Controller() / @Get()
|
| 依存関係の境界 | ApplicationContext や設定 | Module |
アノテーションやデコレータは、AOP や横断的関心事の文脈で語られることがあります。
たとえば、認証、ログ、トランザクション、バリデーションのような処理は、アプリケーション本体の処理に横から差し込まれることがあります。
そのため、「既存のクラスを大きく変えずに機能を追加するための仕組み」と説明されることもあります。
ただし、NestJS がデコレータを多用している理由をそこだけで理解すると、少しずれます。
NestJS のデコレータの中心的な役割は、フレームワークに「このクラスやメソッドは何者か」を伝えることです。
つまり、既存機能を壊さずに拡張するためというより、まずはフレームワークがアプリケーション構造を読み取るための印です。
もちろん、Guard、Pipe、Interceptor のように、処理の前後へ機能を差し込む仕組みにもデコレータは使われます。
しかし、その場合でも基本は同じです。
デコレータが直接ビジネス処理を実行しているのではなく、NestJS に読み取らせるためのメタデータを付けています。
デコレータは「処理本体」ではなく「宣言」
@Get() や @Injectable() を見ると、それ自体が処理を実行しているように見えるかもしれません。
しかし、重要なのは、デコレータが処理本体ではないという点です。
@Get(":id") は、HTTP リクエストを直接処理する関数ではありません。
「このメソッドは GET :id のハンドラである」という情報を付けるためのものです。
実際にリクエストを受け取り、該当するハンドラを呼び出すのは NestJS のランタイムです。
@Injectable() も同じです。
それ自体が Service の処理を書く場所ではありません。
「このクラスは Provider として扱える」という情報を NestJS に渡すための宣言です。
このように見ると、NestJS のデコレータは、処理を書くためというより、フレームワークに読み取らせる情報を書くためのものだと分かります。
まとめ
NestJS がデコレータを多用しているのは、クラスやメソッドの役割をフレームワークに伝えるためです。
デコレータによって、NestJS は次のような情報を読み取れます。
- Controller とルーティング
- Provider と DI
- Module の構成
- Guard、Pipe、Interceptor などの適用対象
Spring のアノテーションに似た見た目ですが、NestJS では TypeScript のデコレータを使ってメタデータを付与し、それをランタイムが読み取っています。
そのため、NestJS のデコレータは「便利な装飾」ではなく、アプリケーション構造を宣言するための中心的な仕組みです。
この視点で読むと、@Controller()、@Injectable()、@Module() が多く出てくる理由を理解しやすくなります。