NestJS完全マスターガイド 2025 - Node.jsエンタープライズ開発の決定版
TypeScriptファーストのNode.jsフレームワークNestJSを徹底解説。v11系の新機能、マイクロサービス構築、GraphQL統合、本番環境での運用まで、エンタープライズ開発に必要な全てを網羅します。
TypeScriptの高度な型機能を活用して、Conditional TypesやTemplate Literal Typesを駆使した型安全なAPIクライアントを実装する方法を徹底解説。zodによるランタイム検証も紹介します。
Typescript の型システムは、単なる型チェック以上の強力な機能を提供します。 本記事では、Conditional Types や Template Literal Types などの高度な型機能を活用し、 コンパイル時に型安全性を保証する api クライアントの実装方法を詳しく解説します。
Typescript の型システムは、javascript 開発における型安全性を飛躍的に向上させます。 特に、api クライアントの実装においては、コンパイル時にエラーを検出できることで、 実行時エラーを大幅に削減できます。
Mapped Types は、既存の型から新しい型を生成する強力な機能です。
// 基本的なMapped Type
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 使用例
interface User {
id: number;
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string; }
Mapped Types は、api レスポンスの型変換や、フォームデータの検証において特に有効です。 条件付きマッピングと組み合わせることで、より柔軟な型定義が可能になります。
Typescript には多くの組み込み Utility Types があり、これらを活用することで 型定義を簡潔かつ強力にできます。
Utility Type | 説明 | 使用例 | 活用シーン |
---|---|---|---|
Partial<T> | 全プロパティをオプショナルに | Partial<User> | 更新API |
Required<T> | 全プロパティを必須に | Required<Config> | 設定検証 |
Pick<T, K> | 特定のプロパティを抽出 | Pick<User, 'id' | 'name'> | レスポンス型 |
Omit<T, K> | 特定のプロパティを除外 | Omit<User, 'password'> | セキュリティ |
Record<K, T> | キーと値の型を定義 | Record<string, User> | マップ型 |
Conditional Types は、型レベルでの条件分岐を可能にする機能です。 api クライアントの実装において、エンドポイントごとに異なる型を返す場合に特に有効です。
// 基本構文: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
// 使用例
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
// エンドポイントごとに個別の型定義
interface GetUserResponse {
id: number;
name: string;
email: string;
}
interface GetPostsResponse {
posts: Array<{
id: number;
title: string;
content: string;
}>;
}
// APIクライアント
async function getUser(): Promise<GetUserResponse> {
// 実装
}
async function getPosts(): Promise<GetPostsResponse> {
// 実装
}
// エンドポイントと型をマッピング
type APIEndpoints = {
'/users/:id': {
response: {
id: number;
name: string;
email: string;
};
};
'/posts': {
response: {
posts: Array<{
id: number;
title: string;
content: string;
}>;
};
};
};
// Conditional Typesで型を推論
type APIResponse<T extends keyof APIEndpoints> =
APIEndpoints[T]['response'];
// 統一されたAPIクライアント
async function api<T extends keyof APIEndpoints>(
endpoint: T
): Promise<APIResponse<T>> {
// 実装
}
// エンドポイントごとに個別の型定義
interface GetUserResponse {
id: number;
name: string;
email: string;
}
interface GetPostsResponse {
posts: Array<{
id: number;
title: string;
content: string;
}>;
}
// APIクライアント
async function getUser(): Promise<GetUserResponse> {
// 実装
}
async function getPosts(): Promise<GetPostsResponse> {
// 実装
}
// エンドポイントと型をマッピング
type APIEndpoints = {
'/users/:id': {
response: {
id: number;
name: string;
email: string;
};
};
'/posts': {
response: {
posts: Array<{
id: number;
title: string;
content: string;
}>;
};
};
};
// Conditional Typesで型を推論
type APIResponse<T extends keyof APIEndpoints> =
APIEndpoints[T]['response'];
// 統一されたAPIクライアント
async function api<T extends keyof APIEndpoints>(
endpoint: T
): Promise<APIResponse<T>> {
// 実装
}
infer
キーワードを使用することで、Conditional Types 内で型を推論できます。
// 関数の戻り値の型を抽出
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Promiseの中身の型を抽出
type Unpromise<T> = T extends Promise<infer U> ? U : T;
// 実践例:APIレスポンスの型抽出
type ExtractData<T> = T extends { data: infer D } ? D : never;
interface APIResponse<T> {
data: T;
status: number;
message: string;
}
type UserData = ExtractData<APIResponse<User>>; // User型が抽出される
Template Literal Types は、文字列リテラル型を組み合わせて新しい型を生成する機能です。 api の url パターンを型安全に管理する際に非常に有効です。
// 基本構文
type Greeting<T extends string> = `Hello, ${T}!`;
type Message = Greeting<'World'>; // "Hello, World!"
// HTTPメソッドとパスの組み合わせ
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIPath = '/users' | '/posts' | '/comments';
type APIRoute = `${HttpMethod} ${APIPath}`;
// "GET /users" | "GET /posts" | ... | "DELETE /comments"
チャートを読み込み中...
// URLパラメータを抽出する型
type ExtractParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractParams<Rest>
: T extends `${infer _Start}:${infer Param}`
? { [K in Param]: string }
: {};
// 使用例
type UserParams = ExtractParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string }
// 型安全なURLビルダー
function buildUrl<T extends string>(
pattern: T,
params: ExtractParams<T>
): string {
let url = pattern as string;
Object.entries(params).forEach(([key, value]) => {
url = url.replace(`:${key}`, String(value));
});
return url;
}
// 使用例(型エラーが発生する場合)
buildUrl('/users/:userId/posts/:postId', {
userId: '123',
// postId が不足しているためエラー
});
Typescript の型システムは強力ですが、コンパイル時のみの検証です。 api レスポンスなど、実行時のデータ検証には zod のようなランタイム検証ライブラリが必要です。
import { z } from 'zod';
// スキーマの定義
const UserSchema = z.object({
id: z.number(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().min(0).max(150).optional(),
roles: z.array(z.string()),
createdAt: z.string().datetime()
});
// TypeScriptの型を推論
type User = z.infer<typeof UserSchema>;
// 検証の実行
const validateUser = (data: unknown): User => {
return UserSchema.parse(data); // 失敗時は例外を投げる
};
// 安全な検証(例外を投げない)
const safeValidateUser = (data: unknown) => {
return UserSchema.safeParse(data);
// { success: true, data: User } | { success: false, error: ZodError }
};
zod の parse
メソッドは検証に失敗すると例外を投げます。
エラーハンドリングを行う場合は、safeParse
メソッドを使用してください。
// APIレスポンスのスキーマ定義
const ApiResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
z.object({
data: dataSchema,
status: z.number(),
message: z.string(),
timestamp: z.string().datetime()
});
// ユーザー一覧APIのレスポンススキーマ
const UsersResponseSchema = ApiResponseSchema(
z.array(UserSchema)
);
// API呼び出しと検証
async function fetchUsers() {
const response = await fetch('/api/users');
const json = await response.json();
// ランタイム検証
const validated = UsersResponseSchema.parse(json);
return validated.data; // User[]型として扱える
}
// エラーレスポンスのスキーマ
const ErrorResponseSchema = z.object({
error: z.object({
code: z.string(),
message: z.string(),
details: z.array(z.object({
field: z.string(),
message: z.string()
})).optional()
}),
status: z.number(),
timestamp: z.string().datetime()
});
// ユニオン型でエラーも含む
const ApiResultSchema = <T extends z.ZodType>(dataSchema: T) =>
z.union([
ApiResponseSchema(dataSchema),
ErrorResponseSchema
]);
// 型安全なエラーハンドリング
async function safeFetchUser(id: string) {
try {
const response = await fetch(`/api/users/${id}`);
const json = await response.json();
const result = ApiResultSchema(UserSchema).parse(json);
if ('error' in result) {
console.error('API Error:', result.error);
return null;
}
return result.data;
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Validation Error:', error.errors);
}
return null;
}
}
// カスタムバリデーションの追加
const PasswordSchema = z
.string()
.min(8, 'パスワードは8文字以上必要です')
.refine(
(password) => /[A-Z]/.test(password),
'パスワードには大文字を含める必要があります'
)
.refine(
(password) => /[0-9]/.test(password),
'パスワードには数字を含める必要があります'
)
.refine(
(password) => /[!@#$%^&*]/.test(password),
'パスワードには特殊文字を含める必要があります'
);
// 相互依存するフィールドの検証
const RegistrationSchema = z.object({
email: z.string().email(),
password: PasswordSchema,
confirmPassword: z.string()
}).refine(
(data) => data.password === data.confirmPassword,
{
message: 'パスワードが一致しません',
path: ['confirmPassword']
}
);
// 非同期バリデーション
const UniqueEmailSchema = z.string().email().refine(
async (email) => {
const response = await fetch(`/api/check-email?email=${email}`);
const { exists } = await response.json();
return !exists;
},
{
message: 'このメールアドレスは既に使用されています'
}
);
OpenAPI(Swagger)仕様から typescript の型を自動生成することで、 api とクライアントの型定義を常に同期させることができます。
ツール | 特徴 | zodサポート | カスタマイズ性 |
---|---|---|---|
openapi-typescript | シンプルで高速 | 中 | |
openapi-typescript-codegen | クライアントコード生成 | 高 | |
zodios | zod統合 | 中 | |
orval | 多機能・高カスタマイズ | 非常に高 |
// 1. OpenAPI仕様から型を生成
// npx openapi-typescript ./openapi.yaml -o ./src/types/api.ts
// 2. 生成された型をインポート
import { paths } from './types/api';
// 3. 型安全なAPIクライアントの実装
type ApiPaths = keyof paths;
type ApiMethods<P extends ApiPaths> = keyof paths[P];
type ApiRequest<
P extends ApiPaths,
M extends ApiMethods<P>
> = paths[P][M] extends { requestBody?: { content: { 'application/json': infer R } } }
? R
: never;
type ApiResponse<
P extends ApiPaths,
M extends ApiMethods<P>
> = paths[P][M] extends { responses: { 200: { content: { 'application/json': infer R } } } }
? R
: never;
// 型安全なfetch wrapper
async function apiCall<
P extends ApiPaths,
M extends ApiMethods<P>
>(
path: P,
method: M,
options?: {
body?: ApiRequest<P, M>;
params?: Record<string, string>;
}
): Promise<ApiResponse<P, M>> {
const url = new URL(path as string, process.env.API_BASE_URL);
if (options?.params) {
Object.entries(options.params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
const response = await fetch(url.toString(), {
method: method as string,
headers: {
'Content-Type': 'application/json',
},
body: options?.body ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
// orval.config.ts
import { defineConfig } from 'orval';
export default defineConfig({
petstore: {
input: {
target: './openapi.yaml',
},
output: {
mode: 'tags-split',
target: './src/api',
schemas: './src/models',
client: 'react-query',
mock: true,
override: {
mutator: {
path: './src/api/custom-instance.ts',
name: 'customInstance',
},
operations: {
// 各エンドポイントのカスタマイズ
listPets: {
query: {
useQuery: true,
useInfinite: true,
useInfiniteQueryParam: 'page',
},
},
},
// zodスキーマの生成
zod: {
generate: true,
coerce: {
dates: true,
arrays: true,
},
},
},
},
},
});
JSON Schema完全互換
装飾子の標準化
zodとの統合が標準に
これまでの要素を組み合わせて、完全に型安全な api クライアントを実装します。
チャートを読み込み中...
// api-client.ts
import { z } from 'zod';
import { paths } from './generated/api-types';
// 設定の型定義
interface ApiConfig {
baseUrl: string;
headers?: Record<string, string>;
timeout?: number;
retry?: {
count: number;
delay: number;
};
}
// APIエラーの型定義
class ApiError extends Error {
constructor(
public status: number,
public code: string,
message: string,
public details?: unknown
) {
super(message);
this.name = 'ApiError';
}
}
// 型ヘルパー
type PathParams<T extends string> = T extends `${infer _Start}{${infer Param}}${infer Rest}`
? { [K in Param]: string | number } & PathParams<Rest>
: {};
type QueryParams<P extends keyof paths, M extends keyof paths[P]> =
paths[P][M] extends { parameters?: { query?: infer Q } } ? Q : never;
type RequestBody<P extends keyof paths, M extends keyof paths[P]> =
paths[P][M] extends { requestBody?: { content: { 'application/json': infer B } } } ? B : never;
type ResponseBody<P extends keyof paths, M extends keyof paths[P]> =
paths[P][M] extends { responses: { 200: { content: { 'application/json': infer R } } } } ? R : never;
// APIクライアントクラス
export class TypeSafeApiClient {
constructor(private config: ApiConfig) {}
private async request<T>(
url: string,
options: RequestInit,
schema?: z.ZodType<T>
): Promise<T> {
const controller = new AbortController();
const timeout = this.config.timeout || 30000;
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...this.config.headers,
...options.headers,
},
});
clearTimeout(timeoutId);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new ApiError(
response.status,
error.code || 'UNKNOWN_ERROR',
error.message || response.statusText,
error.details
);
}
const data = await response.json();
// zodスキーマが提供されている場合は検証
if (schema) {
return schema.parse(data);
}
return data as T;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof ApiError) {
throw error;
}
if (error instanceof z.ZodError) {
throw new ApiError(
422,
'VALIDATION_ERROR',
'レスポンスの検証に失敗しました',
error.errors
);
}
throw new ApiError(
0,
'NETWORK_ERROR',
error instanceof Error ? error.message : 'ネットワークエラーが発生しました'
);
}
}
// 汎用的なAPIメソッド
async call<P extends keyof paths, M extends keyof paths[P]>(
path: P,
method: M,
options?: {
params?: PathParams<P & string>;
query?: QueryParams<P, M>;
body?: RequestBody<P, M>;
schema?: z.ZodType<ResponseBody<P, M>>;
}
): Promise<ResponseBody<P, M>> {
// URLの構築
let url = `${this.config.baseUrl}${path as string}`;
// パスパラメータの置換
if (options?.params) {
Object.entries(options.params).forEach(([key, value]) => {
url = url.replace(`{${key}}`, String(value));
});
}
// クエリパラメータの追加
if (options?.query) {
const params = new URLSearchParams();
Object.entries(options.query).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
params.append(key, String(value));
}
});
const queryString = params.toString();
if (queryString) {
url += `?${queryString}`;
}
}
return this.request<ResponseBody<P, M>>(
url,
{
method: method as string,
body: options?.body ? JSON.stringify(options.body) : undefined,
},
options?.schema
);
}
// 便利メソッド
get<P extends keyof paths>(
path: P,
options?: Omit<Parameters<typeof this.call>[2], 'body'>
) {
return this.call(path, 'get' as any, options);
}
post<P extends keyof paths>(
path: P,
body?: RequestBody<P, 'post'>,
options?: Omit<Parameters<typeof this.call>[2], 'body'>
) {
return this.call(path, 'post' as any, { ...options, body });
}
put<P extends keyof paths>(
path: P,
body?: RequestBody<P, 'put'>,
options?: Omit<Parameters<typeof this.call>[2], 'body'>
) {
return this.call(path, 'put' as any, { ...options, body });
}
delete<P extends keyof paths>(
path: P,
options?: Omit<Parameters<typeof this.call>[2], 'body'>
) {
return this.call(path, 'delete' as any, options);
}
}
// スキーマ定義
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
createdAt: z.string().datetime(),
});
const UsersListSchema = z.array(UserSchema);
// クライアントの初期化
const apiClient = new TypeSafeApiClient({
baseUrl: 'https://api.example.com',
headers: {
'Authorization': `Bearer ${getToken()}`,
},
timeout: 10000,
});
// 型安全なAPI呼び出し
async function fetchUserData() {
try {
// 単一ユーザーの取得
const user = await apiClient.get('/users/{id}', {
params: { id: '123' },
schema: UserSchema,
});
console.log(user.name); // 型安全にアクセス可能
// ユーザー一覧の取得
const users = await apiClient.get('/users', {
query: { page: 1, limit: 20 },
schema: UsersListSchema,
});
users.forEach(u => console.log(u.email)); // 型安全
// ユーザーの作成
const newUser = await apiClient.post('/users', {
name: '山田太郎',
email: 'yamada@example.com',
}, {
schema: UserSchema,
});
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error [${error.code}]: ${error.message}`);
if (error.status === 422) {
console.error('Validation errors:', error.details);
}
}
}
}
Api クライアントにおけるエラーハンドリングも型安全に実装することが重要です。
// 基底エラークラス
abstract class BaseError extends Error {
abstract readonly code: string;
abstract readonly statusCode: number;
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
// ネットワークエラー
class NetworkError extends BaseError {
readonly code = 'NETWORK_ERROR';
readonly statusCode = 0;
}
// 認証エラー
class AuthenticationError extends BaseError {
readonly code = 'AUTHENTICATION_ERROR';
readonly statusCode = 401;
}
// 認可エラー
class AuthorizationError extends BaseError {
readonly code = 'AUTHORIZATION_ERROR';
readonly statusCode = 403;
}
// バリデーションエラー
class ValidationError extends BaseError {
readonly code = 'VALIDATION_ERROR';
readonly statusCode = 422;
constructor(
message: string,
public readonly errors: Array<{
field: string;
message: string;
code?: string;
}>
) {
super(message);
}
}
// サーバーエラー
class ServerError extends BaseError {
readonly code = 'SERVER_ERROR';
readonly statusCode = 500;
}
エラーは例外ではなく、正常なプログラムフローの一部として扱うべきである。
// Result型の定義
type Success<T> = {
success: true;
data: T;
};
type Failure<E = Error> = {
success: false;
error: E;
};
type Result<T, E = Error> = Success<T> | Failure<E>;
// Result型を返すAPIクライアント
class SafeApiClient extends TypeSafeApiClient {
async safeCall<P extends keyof paths, M extends keyof paths[P]>(
path: P,
method: M,
options?: Parameters<TypeSafeApiClient['call']>[2]
): Promise<Result<ResponseBody<P, M>, BaseError>> {
try {
const data = await this.call(path, method, options);
return { success: true, data };
} catch (error) {
if (error instanceof BaseError) {
return { success: false, error };
}
// 未知のエラーの場合
return {
success: false,
error: new ServerError(
error instanceof Error ? error.message : '不明なエラーが発生しました'
),
};
}
}
}
// 使用例
async function handleUserUpdate(userId: string, updates: Partial<User>) {
const client = new SafeApiClient({ baseUrl: 'https://api.example.com' });
const result = await client.safeCall('/users/{id}', 'put', {
params: { id: userId },
body: updates,
});
if (result.success) {
console.log('更新成功:', result.data);
return result.data;
} else {
// エラーの型に基づいた処理
switch (result.error.code) {
case 'VALIDATION_ERROR':
if (result.error instanceof ValidationError) {
console.error('バリデーションエラー:', result.error.errors);
}
break;
case 'AUTHENTICATION_ERROR':
// 再ログイン処理
redirectToLogin();
break;
case 'NETWORK_ERROR':
// リトライ処理
console.error('ネットワークエラー。再試行してください。');
break;
default:
console.error('エラー:', result.error.message);
}
return null;
}
}
// 従来のtry-catch方式
class TraditionalApiService {
async getUser(id: string): Promise<User | null> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
if (response.status === 404) {
return null;
}
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return UserSchema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
console.error('データ形式エラー:', error);
throw new ValidationError('不正なデータ形式', []);
}
if (error instanceof Error) {
console.error('API呼び出しエラー:', error);
throw error;
}
throw new Error('不明なエラー');
}
}
}
// Result型を使った方式
class ResultApiService {
async getUser(id: string): Promise<Result<User | null, BaseError>> {
try {
const response = await fetch(`/api/users/${id}`);
if (response.status === 404) {
return { success: true, data: null };
}
if (!response.ok) {
return {
success: false,
error: new ServerError(`HTTP ${response.status}`)
};
}
const data = await response.json();
const validated = UserSchema.safeParse(data);
if (!validated.success) {
return {
success: false,
error: new ValidationError(
'不正なデータ形式',
validated.error.errors.map(e => ({
field: e.path.join('.'),
message: e.message
}))
)
};
}
return { success: true, data: validated.data };
} catch (error) {
return {
success: false,
error: new NetworkError(
error instanceof Error ? error.message : 'ネットワークエラー'
)
};
}
}
}
// Either型を使った関数型アプローチ
type Left<E> = { _tag: 'Left'; left: E };
type Right<A> = { _tag: 'Right'; right: A };
type Either<E, A> = Left<E> | Right<A>;
const left = <E>(e: E): Either<E, never> => ({ _tag: 'Left', left: e });
const right = <A>(a: A): Either<never, A> => ({ _tag: 'Right', right: a });
const isLeft = <E, A>(e: Either<E, A>): e is Left<E> => e._tag === 'Left';
const isRight = <E, A>(e: Either<E, A>): e is Right<A> => e._tag === 'Right';
class EitherApiService {
async getUser(id: string): Promise<Either<BaseError, User | null>> {
try {
const response = await fetch(`/api/users/${id}`);
if (response.status === 404) {
return right(null);
}
if (!response.ok) {
return left(new ServerError(`HTTP ${response.status}`));
}
const data = await response.json();
const validated = UserSchema.safeParse(data);
if (!validated.success) {
return left(new ValidationError('不正なデータ形式', []));
}
return right(validated.data);
} catch (error) {
return left(new NetworkError('ネットワークエラー'));
}
}
// Either型のチェーン処理
async getUserWithPosts(id: string) {
const userResult = await this.getUser(id);
if (isLeft(userResult)) {
return userResult;
}
if (userResult.right === null) {
return left(new NotFoundError('ユーザーが見つかりません'));
}
const postsResult = await this.getUserPosts(id);
if (isLeft(postsResult)) {
return postsResult;
}
return right({
user: userResult.right,
posts: postsResult.right
});
}
}
Typescript の高度な型機能を活用することで、api クライアントの実装において コンパイル時とランタイムの両方で型安全性を確保できます。
strict
モードを有効化2025 年現在、typescript の型システムはますます強力になり、 より多くのランタイム検証ツールとの統合が進んでいます。 今後は、型定義の自動生成や ai を活用した型推論の支援など、 さらなる開発効率の向上が期待されます。