
TypeScriptのためにAngularからインスパイアされて作られたバックエンド用のフレームワークNestJSについて紹介します。
はじめに
最近勢いがあるTypeScriptですが、皆さんはバックエンドのAPIを構築する場合はどんなフレームワークを使っていますか?TypeScriptを使わない時と同じようにExpressやHapiなどを使っている人が多いと思いますが、もう一つ新しい選択肢があります。それはNestJSです。特徴は、Angularのようにバックエンドを書ける点と、結果としてフォルダやファイルの構成が明確化されているので、構成に悩むことなく本来のAPI開発に専念できる点です。
今回はNestJSで簡単なCRUDのAPIをさくさく作ってみましょう。
NestJSとは?
NestJSとは、TypeScriptおよびモダンJavaScriptのために作られたエンタープライズ級のNodeJSフレームワークです。冒頭でも述べたとおり、Angularにインスパイアされているため、Angularのような書き方でコーディングすることができます。(デコレータを使ったコーディングですね。)フォルダやファイルの構成も分かりやすく、フレームワークとして必要な機能もしっかりと有しています。ドキュメントもしっかり書かれており、サンプルとなるソースコードも公開されているので、すぐに開発が始められます。
簡単なAPIを作ってみよう
それでは、今回はユーザを管理する簡単なAPIを作ってみましょう。
ベースを作る
NestCLIコマンドを使ってプロジェクトを生成し、必要なフォルダとファイルを作成します。
$ yarn add glbal @nestjs/cli
$ nest --version
6.5.0
$ nest new my-nestjs-api
...
? Which package manager would you ❤️ to use? yarn
...
$ cd my-nestjs-api/
$ yarn add uuid @types/uuid joi @types/joi
$ mkdir -p src/common/pipes
$ mkdir -p src/users
$ mkdir -p src/users/models
$ mkdir -p src/users/dto
$ mkdir -p src/users/schemas
$ touch src/common/pipes/validation.pipe.ts
$ touch src/users/users.service.ts
$ touch src/users/users.controller.ts
$ touch src/users/users.module.ts
$ touch src/users/models/user.model.ts
$ touch src/users/schemas/create-user.schema.ts
$ touch src/users/schemas/update-user.schema.ts
$ touch src/users/schemas/user.enum.ts
$ touch src/users/users.controller.spec.ts
$ rm src/app.service.ts
$ rm src/app.controller.ts
$ rm src/app.controller.spec.ts
$ tree src/
src/
├── app.module.ts
├── common
│ └── pipes
│ └── validation.pipe.ts
├── main.ts
└── users
├── dto
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
├── models
│ └── user.model.ts
├── schemas
│ ├── create-user.schema.ts
│ ├── update-user.schema.ts
│ └── user.enum.ts
├── users.controller.spec.ts
├── users.controller.ts
├── users.module.ts
└── users.service.ts
構成ができました。
ユーザをCRUDするAPIを作る
ユーザを管理するCRUDのAPIを実装しましょう。
create-user.dto.ts
export class CreateUserDto {
readonly name: string;
readonly gender: string;
readonly age: number;
}
update-user.dto.ts
export class UpdateUserDto {
readonly name: string;
readonly gender: string;
readonly age: number;
}
user.model.ts
export class User {
constructor(
public id: string,
public name: string,
public gender: string,
public age: number,
) {}
}
users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
import { User } from './models/user.model';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UserService {
private users: User[] = [];
createUser(createUserDto: CreateUserDto) {
const userId = uuid();
const newUser = new User(
userId,
createUserDto.name,
createUserDto.gender,
createUserDto.age,
);
this.users.push(newUser);
return userId;
}
readAllUsers() {
return [...this.users];
}
readUser(userId: string) {
const [user, _] = this.findUser(userId);
return { ...user };
}
updateUser(userId: string, updateUserDto: UpdateUserDto) {
const [user, index] = this.findUser(userId);
const updateUser = { ...user };
if (updateUserDto.name) {
updateUser.name = updateUserDto.name;
}
if (updateUserDto.gender) {
updateUser.gender = updateUserDto.gender;
}
if (updateUserDto.age) {
updateUser.age = updateUserDto.age;
}
this.users[index] = { ...updateUser };
}
deleteUser(userId: string) {
const [user, _] = this.findUser(userId);
this.users = this.users.filter(u => u.id !== user.id);
}
private findUser(id: string): [User, number] {
const index = this.users.findIndex(u => u.id === id);
const user = this.users[index];
if (!user) {
throw new NotFoundException(`Could not find the user of id: ${id}`);
}
return [user, index];
}
}
users.controller.ts
import {
Body,
Controller,
Get,
Post,
UsePipes,
Param,
Patch,
Delete,
} from '@nestjs/common';
import { UserService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
addUser(@Body() createUserDto: CreateUserDto) {
const userId = this.userService.createUser(createUserDto);
return {
id: userId,
};
}
@Get()
getAllUsers() {
return this.userService.readAllUsers();
}
@Get(':id')
getUser(@Param('id') id: string) {
return this.userService.readUser(id);
}
@Patch(':id')
updateUser(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
this.userService.updateUser(id, updateUserDto);
return null;
}
@Delete(':id')
removeUser(@Param('id') id: string) {
this.userService.deleteUser(id);
return null;
}
}
users.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './users.controller';
import { UserService } from './users.service';
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
app.module.ts
import { Module } from '@nestjs/common';
import { UserModule } from './users/users.module';
@Module({
imports: [UserModule],
controllers: [],
providers: [],
})
export class AppModule {}
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
一通り完成しました。
バリデーションを追加する
user.enum.ts
export enum Gender {
Male = 'male',
Female = 'female',
}
create-user.schema.ts
import * as Joi from 'joi';
import { Gender } from './user.enum';
export const createUserSchema = Joi.object().keys({
name: Joi.string()
.min(3)
.required(),
gender: Joi.string()
.valid([Gender.Male, Gender.Female])
.required(),
age: Joi.number()
.integer()
.min(0)
.required(),
});
update-user.schema.ts
import * as Joi from 'joi';
import { Gender } from './user.enum';
export const updateUserSchema = Joi.object().keys({
name: Joi.string().min(3),
gender: Joi.string().valid([Gender.Male, Gender.Female]),
age: Joi.number()
.integer()
.min(0),
});
validation.pipe.ts
import * as Joi from 'joi';
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
constructor(private readonly schema: Object) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = Joi.validate(value, this.schema);
if (error) {
throw new BadRequestException(error);
}
return value;
}
}
users.controller.ts (追記)
import {
Body,
Controller,
Get,
Post,
UsePipes,
Param,
Patch,
Delete,
} from '@nestjs/common';
import { UserService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ValidationPipe } from '../common/pipes/validation.pipe';
import { createUserSchema } from './schemas/create-user.schema';
import { updateUserSchema } from './schemas/update-user.schema';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
@UsePipes(new ValidationPipe(createUserSchema))
addUser(@Body() createUserDto: CreateUserDto) {
const userId = this.userService.createUser(createUserDto);
return {
id: userId,
};
}
@Get()
getAllUsers() {
return this.userService.readAllUsers();
}
@Get(':id')
getUser(@Param('id') id: string) {
return this.userService.readUser(id);
}
@Patch(':id')
@UsePipes(new ValidationPipe(updateUserSchema))
updateUser(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
this.userService.updateUser(id, updateUserDto);
return null;
}
@Delete(':id')
removeUser(@Param('id') id: string) {
this.userService.deleteUser(id);
return null;
}
}
これでバリデーションの追加ができました。
少しテストを書いてみる
最後に、コントローラーのテストを少し書いて動作確認しましょう。
users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './users.controller';
import { CreateUserDto } from './dto/create-user.dto';
import { Gender } from './schemas/user.enum';
import { UserService } from './users.service';
import { UpdateUserDto } from './dto/update-user.dto';
import { NotFoundException } from '@nestjs/common';
describe('UserController', () => {
let userController: UserController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [UserService],
}).compile();
userController = app.get<UserController>(UserController);
});
describe('addUser', () => {
it('should return id of the new user', () => {
const newUser: CreateUserDto = {
name: 'Tom',
gender: Gender.Male,
age: 22,
};
expect(userController.addUser(newUser)).toHaveProperty('id');
});
});
describe('getAllUsers', () => {
it('should return all users', () => {
const users: CreateUserDto[] = [
{
name: 'Tom',
gender: Gender.Male,
age: 22,
},
{
name: 'Mary',
gender: Gender.Female,
age: 19,
},
{
name: 'Keid',
gender: Gender.Male,
age: 30,
},
];
userController.addUser(users[0]);
userController.addUser(users[1]);
userController.addUser(users[2]);
expect(userController.getAllUsers()).toEqual(
users.map(user => ({ ...user, id: expect.anything() })),
);
});
});
describe('getUser', () => {
it('should return a user', () => {
const users: CreateUserDto[] = [
{
name: 'Tom',
gender: Gender.Male,
age: 22,
},
{
name: 'Mary',
gender: Gender.Female,
age: 19,
},
{
name: 'Keid',
gender: Gender.Male,
age: 30,
},
];
const userId0 = userController.addUser(users[0]).id;
const userId1 = userController.addUser(users[1]).id;
const userId2 = userController.addUser(users[2]).id;
expect(userController.getUser(userId0)).toEqual({
...users[0],
id: userId0,
});
expect(userController.getUser(userId1)).toEqual({
...users[1],
id: userId1,
});
expect(userController.getUser(userId2)).toEqual({
...users[2],
id: userId2,
});
});
});
describe('updateUser', () => {
it('should update the user', () => {
const newUser: CreateUserDto = {
name: 'Mary',
gender: Gender.Female,
age: 19,
};
const addedUserId = userController.addUser(newUser).id;
const updateUser: UpdateUserDto = {
name: 'Super Man',
gender: Gender.Male,
age: 100,
};
userController.updateUser(addedUserId, updateUser);
expect(userController.getUser(addedUserId)).toEqual({
...updateUser,
id: addedUserId,
});
});
});
describe('removeUser', () => {
it('should remove the user', () => {
const users: CreateUserDto[] = [
{
name: 'Keid',
gender: Gender.Male,
age: 30,
},
{
name: 'Jobs',
gender: Gender.Male,
age: 56,
},
];
const userId0 = userController.addUser(users[0]).id;
const userId1 = userController.addUser(users[1]).id;
userController.removeUser(userId1);
expect(userController.getAllUsers()).toHaveLength(1);
expect(() => userController.getUser(userId1)).toThrow();
});
});
});
テストを実行してみましょう。
$ yarn test
...
PASS src/users/users.controller.spec.ts
UserController
addUser
✓ should return id of the new user (11ms)
getAllUsers
✓ should return all users (4ms)
getUser
✓ should return a user (2ms)
updateUser
✓ should update the user (2ms)
removeUser
✓ should remove the user (2ms)
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: 2.743s, estimated 3s
Ran all test suites.
✨ Done in 3.83s.
OKですね。
最後に
いかがでしたか?これでNestJSの基本はマスターできたと思います。ルールが決まっているので簡単に実装が始められるところが良いですね。ちなみに、現在NestJSのGitHubページ上に日本語のドキュメントが無いので、コントリビュートするチャンスですよ(笑)それでは。
環境
- NodeJS: v12.3.1
- Yarn: 1.16.0
- TypeScript: 3.5.2
- NestJS: 6.5.0