NestJSでREST APIを構築する際、HTTPメソッドデコレータとリクエストデコレータを活用することで、クリーンで型安全なエンドポイントを実装できます。本記事では、@Get、@Post、@Put、@Deleteの各HTTPメソッドデコレータと、@Param、@Query、@Body、@Headersによるリクエストデータの取得方法、そしてDTOパターンを用いた堅牢なAPI設計を実践します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Node.js |
20以上 |
| npm |
10以上 |
| NestJS |
11.x |
| OS |
Windows / macOS / Linux |
| エディタ |
VS Code(推奨) |
事前に以下の準備を完了してください。
- NestJS CLIのインストール済み
- NestJSプロジェクトの作成済み
プロジェクトの作成方法はNestJS入門記事、Module・Controller・Providerの基本はアーキテクチャ解説記事を参照してください。
HTTPメソッドデコレータの基本#
NestJSは、RESTful APIの各操作に対応するHTTPメソッドデコレータを提供しています。これらのデコレータをControllerのメソッドに付与することで、特定のHTTPリクエストを処理するエンドポイントを定義します。
利用可能なHTTPメソッドデコレータ一覧#
| デコレータ |
HTTPメソッド |
主な用途 |
推奨ステータスコード |
@Get() |
GET |
リソースの取得 |
200 |
@Post() |
POST |
リソースの作成 |
201 |
@Put() |
PUT |
リソースの完全更新 |
200 |
@Patch() |
PATCH |
リソースの部分更新 |
200 |
@Delete() |
DELETE |
リソースの削除 |
200または204 |
@Options() |
OPTIONS |
通信オプションの取得 |
200 |
@Head() |
HEAD |
ヘッダー情報の取得 |
200 |
@All() |
全メソッド |
すべてのHTTPメソッドを処理 |
- |
ルートパスの指定方法#
HTTPメソッドデコレータには、オプションでパスを指定できます。このパスは@Controller()で指定したプレフィックスと組み合わされます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import { Controller, Get, Post, Put, Delete } from '@nestjs/common';
@Controller('articles')
export class ArticlesController {
// GET /articles
@Get()
findAll() {
return 'すべての記事を取得';
}
// GET /articles/featured
@Get('featured')
findFeatured() {
return '注目記事を取得';
}
// GET /articles/category/:categoryId
@Get('category/:categoryId')
findByCategory() {
return 'カテゴリ別の記事を取得';
}
}
|
リクエストデコレータによるデータ取得#
NestJSは、HTTPリクエストから様々なデータを取得するための専用デコレータを提供しています。これらを活用することで、Expressのreqオブジェクトに直接アクセスすることなく、必要なデータを型安全に取得できます。
リクエストデコレータ一覧#
graph LR
A[HTTPリクエスト] --> B{デコレータ}
B --> C["@Param() - URLパス"]
B --> D["@Query() - クエリ文字列"]
B --> E["@Body() - リクエストボディ"]
B --> F["@Headers() - ヘッダー"]
B --> G["@Ip() - IPアドレス"]
B --> H["@Req() - リクエスト全体"]
| デコレータ |
取得元 |
Express相当 |
使用例 |
@Param(key?) |
URLパスパラメータ |
req.params |
/users/:idのidを取得 |
@Query(key?) |
クエリ文字列 |
req.query |
?page=1&limit=10を取得 |
@Body(key?) |
リクエストボディ |
req.body |
JSONペイロードを取得 |
@Headers(name?) |
リクエストヘッダー |
req.headers |
Authorizationなどを取得 |
@Ip() |
クライアントIP |
req.ip |
IPアドレスを取得 |
@Req() |
リクエスト全体 |
req |
生のリクエストオブジェクト |
@Param - URLパスパラメータの取得#
@Param()デコレータは、URLパスに含まれる動的なパラメータを取得します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import { Controller, Get, Param } from '@nestjs/common';
@Controller('users')
export class UsersController {
// すべてのパラメータをオブジェクトとして取得
@Get(':id')
findOne(@Param() params: { id: string }) {
console.log(params.id);
return `ユーザーID: ${params.id}`;
}
// 特定のパラメータのみ取得
@Get(':userId/posts/:postId')
findUserPost(
@Param('userId') userId: string,
@Param('postId') postId: string,
) {
return `ユーザー${userId}の投稿${postId}を取得`;
}
}
|
パラメータの型変換が必要な場合は、組み込みのPipeを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { Controller, Get, Param, ParseIntPipe, ParseUUIDPipe } from '@nestjs/common';
@Controller('products')
export class ProductsController {
// 数値への変換(変換失敗時は400エラー)
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return `商品ID: ${id}(数値型)`;
}
// UUIDのバリデーション
@Get('uuid/:uuid')
findByUuid(@Param('uuid', ParseUUIDPipe) uuid: string) {
return `UUID: ${uuid}`;
}
}
|
@Query - クエリ文字列の取得#
@Query()デコレータは、URLのクエリ文字列(?key=value形式)からパラメータを取得します。一覧取得やフィルタリング、ページネーションで頻繁に使用されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import { Controller, Get, Query } from '@nestjs/common';
@Controller('articles')
export class ArticlesController {
// すべてのクエリパラメータを取得
// GET /articles?page=1&limit=10&sort=createdAt
@Get()
findAll(@Query() query: { page?: string; limit?: string; sort?: string }) {
return `ページ: ${query.page}, 件数: ${query.limit}, ソート: ${query.sort}`;
}
// 個別に取得(デフォルト値の設定)
@Get('search')
search(
@Query('keyword') keyword: string,
@Query('page') page: string = '1',
@Query('limit') limit: string = '20',
) {
return `キーワード「${keyword}」で検索(${page}ページ目、${limit}件)`;
}
}
|
型変換とバリデーションを行う場合は、DTOとValidationPipeを組み合わせます(後述)。
1
2
3
4
5
6
7
8
9
10
11
12
|
import { Controller, Get, Query, ParseIntPipe, DefaultValuePipe } from '@nestjs/common';
@Controller('posts')
export class PostsController {
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
return `${page}ページ目、${limit}件取得`;
}
}
|
@Body - リクエストボディの取得#
@Body()デコレータは、POST、PUT、PATCHリクエストのボディからデータを取得します。JSONペイロードの処理に使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
import { Controller, Post, Put, Patch, Body, Param } from '@nestjs/common';
interface CreateArticleDto {
title: string;
content: string;
tags: string[];
}
interface UpdateArticleDto {
title?: string;
content?: string;
tags?: string[];
}
@Controller('articles')
export class ArticlesController {
// リクエストボディ全体を取得
@Post()
create(@Body() createArticleDto: CreateArticleDto) {
return {
message: '記事を作成しました',
data: createArticleDto,
};
}
// 完全更新(PUT)
@Put(':id')
update(
@Param('id') id: string,
@Body() updateArticleDto: UpdateArticleDto,
) {
return {
message: `記事${id}を更新しました`,
data: updateArticleDto,
};
}
// 部分更新(PATCH)
@Patch(':id')
partialUpdate(
@Param('id') id: string,
@Body() updateArticleDto: UpdateArticleDto,
) {
return {
message: `記事${id}を部分更新しました`,
data: updateArticleDto,
};
}
// 特定のプロパティのみ取得
@Post('quick')
quickCreate(@Body('title') title: string) {
return `タイトル「${title}」で記事を作成`;
}
}
|
@Headers()デコレータは、HTTPリクエストヘッダーから値を取得します。認証トークンやカスタムヘッダーの処理に使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import { Controller, Get, Post, Headers } from '@nestjs/common';
@Controller('api')
export class ApiController {
// すべてのヘッダーを取得
@Get('headers')
getAllHeaders(@Headers() headers: Record<string, string>) {
return {
userAgent: headers['user-agent'],
contentType: headers['content-type'],
};
}
// 特定のヘッダーを取得
@Get('auth')
checkAuth(@Headers('authorization') authHeader: string) {
if (!authHeader) {
return { authenticated: false };
}
return {
authenticated: true,
token: authHeader.replace('Bearer ', ''),
};
}
// カスタムヘッダーの取得
@Post('webhook')
handleWebhook(
@Headers('x-webhook-signature') signature: string,
@Headers('x-webhook-timestamp') timestamp: string,
@Body() payload: unknown,
) {
return {
signature,
timestamp,
payload,
};
}
}
|
DTOパターンによる型定義#
DTO(Data Transfer Object)は、クライアントとサーバー間でやり取りされるデータの形式を定義するオブジェクトです。NestJSでは、DTOをクラスとして定義することで、バリデーションとドキュメンテーションの両方に活用できます。
なぜクラスでDTOを定義するのか#
TypeScriptのインターフェースはコンパイル時に消去されるため、ランタイムでの型チェックやバリデーションには使用できません。一方、クラスはランタイムでも存在するため、デコレータを使用したバリデーションが可能です。
graph TD
A[TypeScript Interface] -->|コンパイル| B[消去される]
C[TypeScript Class] -->|コンパイル| D[JavaScript Classとして残る]
D --> E[ランタイムバリデーション可能]
D --> F[リフレクション可能]
D --> G[Swaggerドキュメント生成可能]リクエストDTOの定義#
リクエストデータを受け取るためのDTOを定義します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// dto/create-article.dto.ts
export class CreateArticleDto {
title: string;
content: string;
summary: string;
tags: string[];
categoryId: number;
isPublished: boolean;
}
// dto/update-article.dto.ts
export class UpdateArticleDto {
title?: string;
content?: string;
summary?: string;
tags?: string[];
categoryId?: number;
isPublished?: boolean;
}
|
ページネーション用のクエリDTO#
一覧取得時のページネーションパラメータもDTOで定義すると便利です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// dto/pagination-query.dto.ts
export class PaginationQueryDto {
page?: number;
limit?: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
// dto/article-filter.dto.ts
export class ArticleFilterDto extends PaginationQueryDto {
keyword?: string;
categoryId?: number;
tags?: string[];
isPublished?: boolean;
}
|
レスポンスDTOの定義#
APIレスポンスの形式を統一するためのDTOも定義します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// dto/article-response.dto.ts
export class ArticleResponseDto {
id: number;
title: string;
content: string;
summary: string;
tags: string[];
categoryId: number;
isPublished: boolean;
createdAt: Date;
updatedAt: Date;
}
// dto/paginated-response.dto.ts
export class PaginatedResponseDto<T> {
data: T[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
}
|
完全なCRUD APIの実装例#
ここまでの内容を統合し、記事管理APIの完全な実装例を示します。
ディレクトリ構成#
src/
├── articles/
│ ├── dto/
│ │ ├── create-article.dto.ts
│ │ ├── update-article.dto.ts
│ │ ├── article-filter.dto.ts
│ │ └── article-response.dto.ts
│ ├── articles.controller.ts
│ ├── articles.service.ts
│ └── articles.module.ts
└── app.module.ts
DTOファイル#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// articles/dto/create-article.dto.ts
export class CreateArticleDto {
title: string;
content: string;
summary: string;
tags: string[];
categoryId: number;
}
// articles/dto/update-article.dto.ts
export class UpdateArticleDto {
title?: string;
content?: string;
summary?: string;
tags?: string[];
categoryId?: number;
isPublished?: boolean;
}
// articles/dto/article-filter.dto.ts
export class ArticleFilterDto {
page?: number;
limit?: number;
keyword?: string;
categoryId?: number;
sortBy?: 'createdAt' | 'updatedAt' | 'title';
sortOrder?: 'asc' | 'desc';
}
|
Serviceの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
// articles/articles.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { ArticleFilterDto } from './dto/article-filter.dto';
interface Article {
id: number;
title: string;
content: string;
summary: string;
tags: string[];
categoryId: number;
isPublished: boolean;
createdAt: Date;
updatedAt: Date;
}
@Injectable()
export class ArticlesService {
private articles: Article[] = [];
private idCounter = 1;
create(createArticleDto: CreateArticleDto): Article {
const article: Article = {
id: this.idCounter++,
...createArticleDto,
isPublished: false,
createdAt: new Date(),
updatedAt: new Date(),
};
this.articles.push(article);
return article;
}
findAll(filterDto: ArticleFilterDto) {
let result = [...this.articles];
const page = filterDto.page ?? 1;
const limit = filterDto.limit ?? 10;
// キーワード検索
if (filterDto.keyword) {
const keyword = filterDto.keyword.toLowerCase();
result = result.filter(
(article) =>
article.title.toLowerCase().includes(keyword) ||
article.content.toLowerCase().includes(keyword),
);
}
// カテゴリフィルタ
if (filterDto.categoryId) {
result = result.filter(
(article) => article.categoryId === filterDto.categoryId,
);
}
// ソート
const sortBy = filterDto.sortBy ?? 'createdAt';
const sortOrder = filterDto.sortOrder ?? 'desc';
result.sort((a, b) => {
const aValue = a[sortBy];
const bValue = b[sortBy];
if (sortOrder === 'asc') {
return aValue > bValue ? 1 : -1;
}
return aValue < bValue ? 1 : -1;
});
// ページネーション
const total = result.length;
const totalPages = Math.ceil(total / limit);
const offset = (page - 1) * limit;
const data = result.slice(offset, offset + limit);
return {
data,
meta: {
total,
page,
limit,
totalPages,
},
};
}
findOne(id: number): Article {
const article = this.articles.find((a) => a.id === id);
if (!article) {
throw new NotFoundException(`Article with ID ${id} not found`);
}
return article;
}
update(id: number, updateArticleDto: UpdateArticleDto): Article {
const articleIndex = this.articles.findIndex((a) => a.id === id);
if (articleIndex === -1) {
throw new NotFoundException(`Article with ID ${id} not found`);
}
this.articles[articleIndex] = {
...this.articles[articleIndex],
...updateArticleDto,
updatedAt: new Date(),
};
return this.articles[articleIndex];
}
remove(id: number): void {
const articleIndex = this.articles.findIndex((a) => a.id === id);
if (articleIndex === -1) {
throw new NotFoundException(`Article with ID ${id} not found`);
}
this.articles.splice(articleIndex, 1);
}
}
|
Controllerの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
// articles/articles.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
Headers,
HttpCode,
HttpStatus,
ParseIntPipe,
} from '@nestjs/common';
import { ArticlesService } from './articles.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { ArticleFilterDto } from './dto/article-filter.dto';
@Controller('articles')
export class ArticlesController {
constructor(private readonly articlesService: ArticlesService) {}
/**
* 記事を作成する
* POST /articles
*/
@Post()
@HttpCode(HttpStatus.CREATED)
create(
@Body() createArticleDto: CreateArticleDto,
@Headers('x-user-id') userId: string,
) {
console.log(`User ${userId} is creating an article`);
return this.articlesService.create(createArticleDto);
}
/**
* 記事一覧を取得する(フィルタリング・ページネーション対応)
* GET /articles?page=1&limit=10&keyword=nestjs
*/
@Get()
findAll(@Query() filterDto: ArticleFilterDto) {
return this.articlesService.findAll(filterDto);
}
/**
* 特定の記事を取得する
* GET /articles/:id
*/
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.articlesService.findOne(id);
}
/**
* 記事を更新する
* PUT /articles/:id
*/
@Put(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateArticleDto: UpdateArticleDto,
) {
return this.articlesService.update(id, updateArticleDto);
}
/**
* 記事を削除する
* DELETE /articles/:id
*/
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number) {
this.articlesService.remove(id);
}
}
|
Moduleの定義#
1
2
3
4
5
6
7
8
9
10
11
|
// articles/articles.module.ts
import { Module } from '@nestjs/common';
import { ArticlesController } from './articles.controller';
import { ArticlesService } from './articles.service';
@Module({
controllers: [ArticlesController],
providers: [ArticlesService],
exports: [ArticlesService],
})
export class ArticlesModule {}
|
レスポンスのカスタマイズ#
NestJSでは、レスポンスのステータスコードやヘッダーをカスタマイズするためのデコレータが用意されています。
@HttpCodeによるステータスコードの変更#
デフォルトでは、GETリクエストは200、POSTリクエストは201を返します。これを変更するには@HttpCode()デコレータを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import { Controller, Post, Delete, HttpCode, HttpStatus } from '@nestjs/common';
@Controller('examples')
export class ExamplesController {
// 202 Acceptedを返す(非同期処理の受付など)
@Post('async-task')
@HttpCode(HttpStatus.ACCEPTED)
createAsyncTask() {
return { message: 'タスクを受け付けました' };
}
// 204 No Contentを返す(ボディなし)
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove() {
// 何も返さない
}
}
|
カスタムヘッダーを追加するには@Header()デコレータを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import { Controller, Get, Header } from '@nestjs/common';
@Controller('files')
export class FilesController {
@Get('download')
@Header('Content-Type', 'application/octet-stream')
@Header('Content-Disposition', 'attachment; filename="data.csv"')
downloadFile() {
return 'id,name,email\n1,John,john@example.com';
}
@Get('no-cache')
@Header('Cache-Control', 'no-store, no-cache, must-revalidate')
getNoCacheData() {
return { timestamp: new Date().toISOString() };
}
}
|
@Redirectによるリダイレクト#
別のURLへリダイレクトするには@Redirect()デコレータを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import { Controller, Get, Redirect, Query } from '@nestjs/common';
@Controller('redirect')
export class RedirectController {
// 固定URLへのリダイレクト
@Get('old-page')
@Redirect('https://example.com/new-page', 301)
redirectToNewPage() {
// リダイレクト先は@Redirectで指定済み
}
// 動的なリダイレクト
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
redirectToDocs(@Query('version') version: string) {
if (version === '10') {
return { url: 'https://docs.nestjs.com/v10/' };
}
// デフォルトは@Redirectで指定したURLへ
}
}
|
REST API設計のベストプラクティス#
NestJSでREST APIを設計する際に押さえておくべきポイントをまとめます。
リソース指向のURL設計#
良い例:
GET /articles - 記事一覧取得
GET /articles/:id - 記事詳細取得
POST /articles - 記事作成
PUT /articles/:id - 記事更新
DELETE /articles/:id - 記事削除
避けるべき例:
GET /getArticles
POST /createArticle
POST /articles/delete/:id
適切なHTTPステータスコードの使用#
| 操作 |
成功時 |
失敗時 |
| 作成(POST) |
201 Created |
400 Bad Request, 409 Conflict |
| 取得(GET) |
200 OK |
404 Not Found |
| 更新(PUT/PATCH) |
200 OK |
400 Bad Request, 404 Not Found |
| 削除(DELETE) |
200 OK または 204 No Content |
404 Not Found |
一貫性のあるレスポンス形式#
成功時もエラー時も、一貫したレスポンス構造を維持します。
1
2
3
4
5
6
7
8
9
10
11
12
|
// 成功レスポンスの例
{
"data": { ... },
"meta": { "total": 100, "page": 1 }
}
// エラーレスポンスの例
{
"statusCode": 400,
"message": ["title should not be empty"],
"error": "Bad Request"
}
|
まとめ#
本記事では、NestJSにおけるREST APIエンドポイントの実装方法を解説しました。
学習した内容は以下のとおりです。
- HTTPメソッドデコレータ(
@Get、@Post、@Put、@Delete)によるエンドポイント定義
- リクエストデコレータ(
@Param、@Query、@Body、@Headers)によるデータ取得
- DTOパターンを使用した型安全なリクエスト・レスポンス設計
- レスポンスカスタマイズ(
@HttpCode、@Header、@Redirect)
- REST API設計のベストプラクティス
次のステップとして、Pipeを使用したバリデーションの実装や、Exception Filterによるエラーハンドリングの統一化を学ぶことで、より堅牢なAPIを構築できるようになります。
参考リンク#