Casual Developers Note

エンジニアやデザイナー向けの技術情報・英語学習情報・海外留学情報・海外旅行情報を提供中。世界を旅して人生を楽しもう。

  • ホーム
  • 技術 Tips & Tutorials
  • 技術塾
  • ライフハック
  • 海外留学
  • 英語学習
  • コラム
  • お問い合わせ
現在の場所:ホーム / アーカイブ技術 Tips & Tutorials

2019年7月18日 By KD コメントを書く

令和時代にGraphQLを知らないエンジニアはいらない!GraphQLとPrismaのバックエンド開発入門(GraphQL Yoga編)

令和時代にGraphQLを知らないエンジニアはいらない!GraphQLとPrismaのバックエンド開発入門(GraphQL Yoga編)

GraphQLとPrismaを用いたバックエンドのAPI開発をGraphQL Yogaを使って実現する方法を紹介します。

はじめに

平成後半はAPIの全盛期でした。SaaSが一般化し、SOAPなどのXMLベースの複雑なAPIからJSONベースのシンプルなAPIが主流となり、バックエンドのAPIと言えばJSONベースのREST APIを指すことがほとんどになりました。しかし、REST APIはフォーマットがJSONでシンプルになった一方で、インターフェースに明確な仕様がなく、開発者によって微妙に使い方が異なるという独自仕様の乱立に繋がりました。これはなんとかしなければいけません。エンジニアはいつももっと分かりやすくシンプルにならないかを考えています。そんな平成最後にFacebookが示した一つの解がGraphQLです。GraphQLはエンドポイントを一つにし、インターフェースを統一することで、新しいAPIを誕生させました。まだ発展途上の技術ではありますが、令和の新しい時代に相応しいクールな技術であることは間違いありません。

今回は、GraphQLとPrismaを用いて、バックエンドのAPIを作ってみましょう。

GraphQLとは?

GraphQLとは、APIのために作られたクエリー言語であり、型システムを使ってスキーマを定義することで、統一的なインターフェースを提供する仕組みです。エンドポイントが一つになるため、REST APIの時のように複数のエンドポイントを必要とせず、シンプルに設計されています。さらに、スキーマの情報からドキュメントも自動的に作られるため、APIドキュメントを作る作業からも開放されます。良いところばかりですね。

Prismaとは?

Prismaとは、データベースのための新しいORマッパーです。Prismaサーバーとして、バックエンドAPIとデータベースの間に配置することで、MySQL、PostgreSQL、MongoDBといった具体的なデータベースを完全に隠蔽し、バックエンドAPIからはPrismaが自動生成したGraphQLのクエリーでアクセスできるようになります。データベースアクセスをGraphQLのインターフェースにしてしまうということです。

GraphQLとPrismaのバックエンドを開発しよう

それでは、GraphQLとPrismaのバックエンドアプリケーションをつくってみましょう。

前提条件

以下の準備が完了している必要があります。

  • NodeJSがインストールされていること
  • Dockerがインストールされていること

詳しい環境は「環境」を参照してください。

ベースを作る

まずは、必要はファイルとフォルダを作り、パッケージをインストールします。

$ mkdir yoga-graphql-prisma-backend
$ cd yoga-graphql-prisma-backend
$ yarn init -y
$ yarn add -D @babel/cli @babel/core @babel/preset-env @babel/node
$ touch .babelrc
$ vi .babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "esmodules": true
        }
      }
    ]
  ]
}
$ yarn add -D nodemon
$ yarn add -D prisma
$ yarn add graphql-yoga prisma-client-lib
$ mkdir src
$ touch src/index.js
$ touch src/schema.graphql
$ touch src/resolvers.js
$ vi package.json
{
...
  "scripts": {
    "dev": "nodemon --ext js,graphql --exec babel-node src/index.js",
    "prisma": "prisma"
  },
...
}
$ yarn prisma init prisma
...
? Set up a new Prisma server or deploy to an existing server? Create new database
? What kind of database do you want to deploy to? MySQL
? Select the programming language for the generated Prisma client Prisma JavaScript Client
...
$ tree prisma/ -L 1
prisma/
├── datamodel.prisma
├── docker-compose.yml
├── generated
└── prisma.yml
$ mv prisma/generated src/
$ tree -I 'node_modules' -L 3
.
├── package.json
├── prisma
│   ├── datamodel.prisma
│   ├── docker-compose.yml
│   └── prisma.yml
├── src
│   ├── generated
│   │   └── prisma-client
│   ├── index.js
│   ├── resolvers.js
│   └── schema.graphql
└── yarn.lock

今回はゼロからスクラッチで作りますが、GraphQL CLIを使えば一瞬で類似した構成を構築できます。

Prismaでデータベース部分を開発する

次に、Prismaでデータベースのスキーマを作りましょう。今回は、よくあるフリーマケットアプリを意識して、User、Product、Reviewのスキーマを作ってみることにしましょう。なお、Prismaの後ろで動くデータベースはMySQLとし、Docker上で可動させることにします。

docker-compose.yml

version: '3'

services:
  prisma:
    image: prismagraphql/prisma:1.34
    restart: always
    ports:
      - 4466:4466
    environment:
      PRISMA_CONFIG: |
        port: 4466
        databases:
          default:
            connector: mysql
            host: mysql
            user: root
            password: prisma
            rawAccess: false
            port: 3306
            migrations: false

  mysql:
    image: mysql:5.7
    restart: always
    ports:
    - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: prisma
    volumes:
      - mysql:/var/lib/mysql

volumes:
  mysql

なお、現時点では「rawAccess」と「migrations」はfalseにしないとPrismaのAdminページが動かないので注意が必要です。

prisma.yml

endpoint: http://localhost:4466

datamodel: datamodel.prisma

generate:
  - generator: javascript-client
    output: ../src/generated/prisma-client

hooks:
  post-deploy:
    - prisma generate

datamodel.prisma

type User {
  id: ID! @id
  name: String!
  email: String! @unique
  products: [Product!]! @relation(name: "ProductToUser", onDelete: CASCADE)
  reviews: [Review!]! @relation(name: "ReviewToUser", onDelete: CASCADE)
}

type Product {
  id: ID! @id
  name: String!
  price: Int!
  onSale: Boolean! @default(value: false)
  author: User! @relation(name: "ProductToUser", onDelete: SET_NULL)
  reviews: [Review!]! @relation(name: "ReviewToProduct", onDelete: CASCADE)
}

type Review {
  id: ID! @id
  text: String!
  author: User! @relation(name: "ReviewToUser", onDelete: SET_NULL)
  product: Product! @relation(name: "ReviewToProduct", onDelete: SET_NULL)
}

PrismaとMySQLを起動しましょう。

$ docker-compose -f prisma/docker-compose.yml up -d
$ docker-compose -f prisma/docker-compose.yml ps
     Name                   Command             State                 Ports              
-----------------------------------------------------------------------------------------
prisma_mysql_1    docker-entrypoint.sh mysqld   Up      0.0.0.0:3306->3306/tcp, 33060/tcp
prisma_prisma_1   /bin/sh -c /app/start.sh      Up      0.0.0.0:4466->4466/tcp
$ yarn prisma deploy

「prisma generate」コマンドでPrismaクライアントが自動的に更新され、「prisma deploy」により、スキーマがデータベースに反映されます。今回は「prisma.yml」の「hooks」で「prisma deploy」だけでこれらが実行さえるようにしてあります。

「http://localhost:4466/」を開くとPrismaのGraphQL用のページが表示されるので、そこからGraphQLを用いてデータベースへアクセスすることができます。

「http://localhost:4466/_admin」を開くとPrismaのAdminページが表示され、GUIでデータベースを操作することが可能です。「prisma admin」で自動的にページを開くこともできます。

Prismaの詳しい使い方は公式ドキュメントを参照して下さい。

GraphQLでCRUDのAPIを開発する

GraphQL Yogaを使ってGraphQLサーバーを構築しましょう。

index.js

import { GraphQLServer } from 'graphql-yoga';
import { prisma } from './generated/prisma-client';
import * as resolvers from './resolvers';

const server = new GraphQLServer({
  typeDefs: './src/schema.graphql',
  resolvers,
  context: {
    prisma,
  },
});

server.start(() => console.log('GraphQL server is up on 4000...'));

schema.graphql

type Query {
  users: [User!]!
  user(id: String!): User!
  products: [Product!]!
  productsOnSale: [Product!]!
  product(id: String!): Product!
  reviews: [Review!]!
  review(id: String!): Review!
}

type Mutation {
  createUser(name: String!, email: String!): User!
  deleteUser(id: String!): String!
  createProduct(
    name: String!
    price: Int!
    onSale: Boolean
    userId: String!
  ): Product!
  updateProduct(id: ID!, name: String, price: Int, onSale: Boolean): Product!
  deleteProduct(id: String!): String!
  createReview(text: String!, userId: String!, productId: String!): Review!
  deleteReview(id: String!): String!
}

type User {
  id: ID!
  name: String!
  email: String!
  products: [Product!]!
  reviews: [Review!]!
}

type Product {
  id: ID!
  name: String!
  price: Int!
  onSale: Boolean!
  author: User!
  reviews: [Review!]!
}

type Review {
  id: ID!
  text: String!
  author: User!
  product: Product!
}

resolvers.js

const Query = {
users: (parent, args, { prisma }, info) => prisma.users(),
user: (parent, { id }, { prisma }, info) => prisma.user({ id }),
products: (parent, args, { prisma }, info) => prisma.products(),
productsOnSale: (parent, args, { prisma }, info) =>
prisma.products({ where: { onSale: true } }),
product: (parent, { id }, { prisma }, info) => prisma.product({ id }),
reviews: (parent, args, { prisma }, info) => prisma.reviews(),
review: (parent, { id }, { prisma }, info) => prisma.review({ id }),
};
const Mutation = {
createUser(parent, { name, email }, { prisma }, info) {
return prisma.createUser({ name, email });
},
async deleteUser(parent, { id }, { prisma }, info) {
const user = await prisma.user({ id });
if (!user) {
throw new Error('User not found!');
}
const deletedUser = await prisma.deleteUser({ id });
return deletedUser.id;
},
async createProduct(
parent,
{ name, price, onSale, userId },
{ prisma },
info
) {
const user = await prisma.user({ id: userId });
if (!user) {
throw new Error('User not found!');
}
const product = await prisma.createProduct({
name,
price,
onSale,
author: {
connect: {
id: userId,
},
},
});
return { ...product, author: user };
},
async updateProduct(parent, { id, name, price, onSale }, { prisma }, info) {
const product = await prisma.product({ id });
if (!product) {
throw new Error('Product not found!');
}
const updatedProduct = await prisma.updateProduct({
data: { name, price, onSale },
where: { id },
});
return updatedProduct;
},
async deleteProduct(parent, { id }, { prisma }, info) {
const product = await prisma.product({ id });
if (!product) {
throw new Error('Product not found!');
}
const deletedProduct = await prisma.deleteProduct({ id });
return deletedProduct.id;
},
async createReview(parent, { text, userId, productId }, { prisma }, info) {
const user = await prisma.user({ id: userId });
if (!user) {
throw new Error('User not found!');
}
const product = await prisma.product({ id: productId });
if (!product) {
throw new Error('Product not found!');
}
const review = await prisma.createReview({
text,
author: {
connect: {
id: userId,
},
},
product: {
connect: {
id: productId,
},
},
});
return { ...review, author: user, product };
},
async deleteReview(parent, { id }, { prisma }, info) {
const review = await prisma.review({ id });
if (!review) {
throw new Error('Review not found!');
}
const deletedReview = await prisma.deleteReview({ id });
return deletedReview.id;
},
};
const User = {
products: (parent, args, { prisma }, info) =>
prisma.user({ id: parent.id }).products(),
reviews: (parent, args, { prisma }, info) =>
prisma.user({ id: parent.id }).reviews(),
};
const Product = {
author: (parent, args, { prisma }, info) =>
prisma.product({ id: parent.id }).author(),
reviews: (parent, args, { prisma }, info) =>
prisma.product({ id: parent.id }).reviews(),
};
const Review = {
author: (parent, args, { prisma }, info) =>
prisma.review({ id: parent.id }).author(),
product: (parent, args, { prisma }, info) =>
prisma.review({ id: parent.id }).product(),
};
export { Query, Mutation, User, Product, Review };

これで実装は完了です。

動作確認

完成したバックエンドAPIの動作確認をしてみましょう。

まずは、サーバーを起動します。

$ yarn dev
...
GraphQL server is up on 4000...

「http://localhost:4000/」を開き、以下のGraphQLのクエリーで確認してみましょう。

Userへのクエリー

query Users {
users {
id
name
email
products {
id
name
price
onSale
}
reviews {
id
text
}
}
}
query User {
user(id: "cjy74aney00nz070468vkvyb9") {
id
name
email
products {
id
name
price
onSale
}
reviews {
id
text
}
}
}
mutation CreateUser {
createUser (name: "Keid", email: "keid@developer.com") {
id
name
email
products {
name
}
reviews {
text
}
}
}
mutation DeleteUser {
deleteUser(id: "cjy74330m00na0704prykyccw")
}

Productへのクエリー

query Products {
products {
id
name
price
onSale
author {
id
name
email
}
reviews {
id
text
}
}
}
query ProductsOnSale {
productsOnSale {
id
name
price
onSale
}
}
query Product {
product(id: "cjy74bwk900on07044uc259vh") {
id
name
price
onSale
author {
id
name
email
}
reviews {
id
text
}
}
}
mutation CreateProduct {
createProduct(name: "iPhone8", price: 60000, userId: "cjy74aney00nz070468vkvyb9") {
id
name
price
onSale
author {
id
name
email
}
}
}
mutation UpdateProduct {
updateProduct(id: "cjy74bwk900on07044uc259vh", onSale: true) {
id
name
price
onSale
author {
id
name
email
}
}
}
mutation DeleteProduct {
deleteProduct(id: "cjy74bv4h00of07045g8eipuj")
}

Reviewへのクエリー

query Reviews {
reviews {
id
text
author {
id
name
email
}
product {
id
name
price
onSale
}
}
}
query Review {
review(id: "cjy74v0zf00qa0704oppn1hr0") {
id
text
author {
id
name
email
}
product {
id
name
price
onSale
}
}
}
mutation CreateReview {
createReview(
text: "The product is great!",
userId: "cjy74aney00nz070468vkvyb9", 
productId: "cjy74bwk900on07044uc259vh")
{
id
text
author {
id
name
email
}
product {
id
name
price
onSale
}
}
}
mutation DeleteReview {
deleteReview(id: "cjy74uvu700py0704a2c0hyub")
}

なお、もちろんIDは適切に設定する必要があります。

NewImage

このように期待通りに動作しました。いろいろ試してみましょう!

おまけ

Subscriptionの実装

GraphQLの他の機能としてSubscriptionがあります。Subscriptionを簡単に説明すると、WebSocketを使ってイベント発生時にクライントに通知するGraphQLの仕組みです。

今回はおまけとして、Reviewが追加された時に通知する実装を追加しましょう。

index.js

import { GraphQLServer, PubSub } from 'graphql-yoga';
import { prisma } from './generated/prisma-client';
import * as resolvers from './resolvers';
const pubsub = new PubSub();
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers,
context: {
prisma,
pubsub,
},
});
server.start(() => console.log('GraphQL server is up on 4000...'));

schema.graphql(抜粋)

...
type Subscription {
reviewCreated: Review!
}
...

resolvers.js(抜粋)

const Mutation = {
...
async createReview(
parent,
{ text, userId, productId },
{ prisma, pubsub },
info
) {
const user = await prisma.user({ id: userId });
if (!user) {
throw new Error('User not found!');
}
const product = await prisma.product({ id: productId });
if (!product) {
throw new Error('Product not found!');
}
const review = await prisma.createReview({
text,
author: {
connect: {
id: userId,
},
},
product: {
connect: {
id: productId,
},
},
});
const newReview = { ...review, author: user, product };
pubsub.publish('REVIEW_CREATED', {
reviewCreated: newReview,
});
return newReview;
},
...
};
...
const Subscription = {
reviewCreated: {
subscribe: (parent, args, { pubsub }, info) =>
pubsub.asyncIterator('REVIEW_CREATED'),
},
};

実装完了です。それでは、動作確認しましょう。

「http://localhost:4000/」を開き、以下を実行しましょう。

subscription SubscribeReview {
reviewCreated {
id
text
author {
id
name
email
}
product {
id
name
price
onSale
}
}
}

NewImage

NewImage

NewImage

Reviewの作成時に、作成されたReviewを取得できました。

最後に

いかがでしたか?これで令和時代に相応しいGraphQLを使いこなすエンジニアになることができましたね!GraphQLはまだまだ新しい技術ですが、今までのREST APIに取って代わっていくと思いますので、機会があれば積極的に使っていきましょう。それでは。

環境

  • NodeJS: v12.3.1
  • Yarn: 1.16.0
  • Docker: 18.09.2
  • @babel/cli: 7.5.0
  • @babel/core: 7.5.4
  • @babel/node: 7.5.0
  • @babel/preset-env: 7.5.4
  • nodemon: 1.19.1
  • prisma: 1.34.1
  • graphql-yoga: 1.18.1
  • prisma-client-lib: 1.34.1

カテゴリ : 技術 Tips & Tutorials タグ : backend, graphql, graphql-yoga, prisma

2019年6月28日 By KD コメントを書く

TypeScriptのためのバックエンド用フレームワークNestJSにさくさく入門しよう

TypeScriptのためのバックエンド用フレームワークNestJSにさくさく入門しよう

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();

一通り完成しました。

バリデーションを追加する

PipeにJoiでのバリデーションを追加しましょう。

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

カテゴリ : 技術 Tips & Tutorials タグ : nestjs, nodejs, rest-api, typescript

2019年5月20日 By KD コメントを書く

DockerコンテナのThe PID 1 Problemとその解決策(NodeJS編)

DockerコンテナのThe PID 1 Problemとその解決策(NodeJS編)

Dockerでアプリケーションを構築する場合に発生するメジャーな問題である「The PID 1 Problem」とその解決策を紹介します。

はじめに

アプリケーションをDocker化するにあたり、プロダクション用のDockerfileでは開発用のDockerfile以上にいくつか気をつける点があります。その中の一つが今回紹介する「The PID 1 Problem」の対応です。Dockerをプロダクションに適用する場合は、理解しておく必要があります。それでは、問題の内容と解決策を見ていきましょう。

The PID 1 Problemとは?

「The PID 1 Problem」とは、この記事で指摘されているDockerコンテナのPID 1に関する問題です。一般的にPID 1は「initプロセス」と呼ばれており、システムが起動した際に最初に起動するプロセスです。このinitプロセスの役割は、ゾンビプロセスの除去とサブプロセスへのシグナルの伝搬です。何の対策もせずにアプリケーションをDocker上で起動した場合、そのアプリケーションの起動プロセスがコンテナ上の最初のプロセスになってしまうため、ゾンビプロセスが残ったり、シグナルが正しく処理されないという問題が発生します。開発者として明確に困る点は、Docker化したアプリケーションが正しく停止しないことです。

tiniとは?

「tini」とは、コンテナ向けに作られたシンプルなinitプログラムです。Dockerコンテナの起動コマンドで使用することで、PID 1の本来の役割を果たし、「The PID 1 Problem」を回避できます。

解決策

それではNodeJSの簡単なDockerアプリケーションを作って、「The PID 1 Problem」を解決してみましょう。

サンプルNodeJSアプリケーションの作成

単にメッセージを返すだけのアプリケーションを作ります。

$ mkdir dockerized-app-fixed-pid1
$ cd dockerized-app-fixed-pid1/
$ yarn init -y
$ yarn add express
$ touch app.js
$ touch Dockerfile
$ touch .dockerignore

コードは以下のようにします。

app.js

const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hey, dockerized app!');
});
app.listen(3000, () => {
console.log('Dockerized app is up...');
});

Dockerfile

FROM node:12.2-alpine
ENV NODE_ENV=production
WORKDIR /node
COPY package.json yarn.lock ./
RUN mkdir app && chown -R node:node .
USER node
RUN yarn install && yarn cache clean --force
WORKDIR /node/app
COPY --chown=node:node . .
EXPOSE 3000
CMD ["node", "app.js"]

.dockerignore

node_modules/

試しにビルドして実行してみましょう。

$ docker build -t dockerized-app .
$ docker run -p 3000:3000 dockerized-app
Dockerized app is up...

動作確認してから停止します。

$ curl localhost:3000/
Hey, dockerized app!
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS                    NAMES
a3b8c29a79c6        dockerized-app      "node app.js"       About a minute ago   Up 59 seconds       0.0.0.0:3000->3000/tcp   quizzical_heyrovsky
$ docker top a3b
PID                 USER                TIME                COMMAND
55419               1000                0:00                node app.js
$ docker stop a3b
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS                       PORTS               NAMES
a3b8c29a79c6        dockerized-app      "node app.js"       About a minute ago   Exited (137) 4 seconds ago                       quizzical_heyrovsky
$ docker rm a3b

注目点として、このコンテナはControl+Cやdocker stopで停止しようとしてもすぐに停止することはなく、正しい停止処理で失敗して強制終了する形で停止しています。

補足として、NodeJSにおけるExit Codeの見方ですが、公式サイトのExit Codeのページを見ると書かれています。つまり、NodeJSのSignal ExitはUnixのSignal Numberに128を足した数字になります。上記の場合は、Exit Codeは「137」なので、128+9ということであり、「9」は「SIGKILL」のシグナルを示しているので、強制終了されていることが分かります。

--initによる対応(一時的な起動)

Dockerの「--init」オプションと使うことでDockerに同封されているinitプロセスを有効にできます。開発環境など一時的にこの問題を解決したい場合に適しています。

それではこのオプションを付けて起動します。

$ docker run --init -p 3000:3000 dockerized-app
Dockerized app is up...

停止します。

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAM
ES
200687bc9992        dockerized-app      "node app.js"       38 seconds ago      Up 37 seconds       0.0.0.0:3000->3000/tcp   vig
ilant_hofstadter
$ docker top 200
PID                 USER                TIME                COMMAND
55642               1000                0:00                /dev/init -- node app.js
55677               1000                0:00                node app.js
$ docker stop 200
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                       PORTS               NAMES
200687bc9992        dockerized-app      "node app.js"       54 seconds ago      Exited (143) 7 seconds ago                       vigilant_hofstadter
$ docker rm 200

停止はすばやく実行され、Exit Code「143」(128+15なので「SIGTERM」)で正常に終了しています。

tiniによる対応(永続的な起動)

「tini」をDockerfile上に追加することでプロダクション用にこの問題を解決できます。今回はtiniの「-e」オプションを使い、Exit Code「143」を「0」にマップすることにします。

Dockerfile

FROM node:12.2-alpine
ENV NODE_ENV=production
RUN apk add --no-cache tini
WORKDIR /node
COPY package.json yarn.lock ./
RUN mkdir app && chown -R node:node .
USER node
RUN yarn install && yarn cache clean --force
WORKDIR /node/app
COPY --chown=node:node . .
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "-e", "143", "--"]
CMD ["node", "app.js"]

それでは変更したDockerfileでビルドして起動します。

$ docker build -t dockerized-app .
$ docker run -p 3000:3000 dockerized-app
Dockerized app is up...

停止します。

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  
NAMES
c7ebb0d93ce7        dockerized-app      "/sbin/tini -e 143 -…"   10 seconds ago      Up 9 seconds        0.0.0.0:3000->3000/tcp 
cocky_haslett
$ docker top c7e
PID                 USER                TIME                COMMAND
55904               1000                0:00                /sbin/tini -e 143 -- node app.js
55938               1000                0:00                node app.js
$ docker stop c7e
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES
c7ebb0d93ce7        dockerized-app      "/sbin/tini -e 143 -…"   36 seconds ago      Exited (0) 5 seconds ago                       cocky_haslett

Exit Code「0」で想定通りに終了しています。

最後に

いかがでしたか?これでDockerコンテナのメジャーな問題である「The PID 1 Problem」を解決できるようになったと思います。プロダクションでDockerを適用する際は注意しましょう。それでは。

環境

  • Docker: 18.09.2
  • NodeJS: v12.2.0
  • Yarn: 1.16.0

カテゴリ : 技術 Tips & Tutorials タグ : docker, nodejs

  • « 前のページ
  • 1
  • 2
  • 3
  • 4
  • 5
  • …
  • 34
  • 次のページ »

ブログ更新情報や海外の関連情報などを配信する無料メルマガ

Sponsored Links

About Author

KD

世界を旅し日本を愛するエンジニア。大学でコンピュータサイエンスの楽しさを学び、日本の大手IT企業で働く中で、新しい技術やスケールするビジネスが北米にある事に気づく。世界に挑戦するための最大の壁が英語であったため、フィリピン留学およびカナダ留学を経て英語を上達させた。現在は日本在住でエンジニアとして働きつつ、次の挑戦に備えて世界の動向を注視している。挑戦に終わりはない。このブログでは、エンジニアやデザイナー向けの技術情報から、海外に留学したい人向けの留学情報、海外に興味がある人向けの海外旅行情報など、有益な情報を提供しています。

https://casualdevelopers.com/

最近の投稿

  • 2020年JS周辺のバックエンド寄りの注目技術!ネクストNodeJSの「Deno」と分散型パッケージレジストリの「Entropic」の紹介

    2020年JS周辺のバックエンド寄りの注目技術!ネクストNodeJSの「Deno」と分散型パッケージレジストリの「Entropic」の紹介

    2020年1月13日
  • 今さら聞けないJavaによる関数型プログラミング入門 ~ラムダ式、ストリーム、関数型インターフェース~

    今さら聞けないJavaによる関数型プログラミング入門 ~ラムダ式、ストリーム、関数型インターフェース~

    2019年11月4日
  • ReactのためのEslintおよびPrettierの設定方法 ~Airbnb JavaScript Style Guideの適用~

    ReactのためのEslintおよびPrettierの設定方法 ~Airbnb JavaScript Style Guideの適用~

    2019年10月30日
  • BashからZshに移行する方法(Mac編)

    BashからZshに移行する方法(Mac編)

    2019年10月21日
  • Create React Appを使わないでゼロからReactの開発環境を構築する方法(Webpack/Docker編)

    Create React Appを使わないでゼロからReactの開発環境を構築する方法(Webpack/Docker編)

    2019年9月30日

カテゴリ

  • 技術 Tips & Tutorials (100)
  • 技術塾 (6)
  • ライフハック (26)
  • 海外留学 (12)
  • 英語学習 (3)
  • コラム (6)

アーカイブ

最高の学習のために

人気記事ランキング

  • MySQLで「ERROR 2003 (HY000): Can't connect to MySQL server」と怒られた時の対処法
    MySQLで「ERROR 2003 (HY000): Can't connect to MySQL server」と怒られた時の対処法
  • Jupyter Notebookで「The kernel appears to have died. It will restart automatically.」というエラーが出た場合の原因と対処法
    Jupyter Notebookで「The kernel appears to have died. It will restart automatically.」というエラーが出た場合の原因と対処法
  • 爆速でJenkinsをマスターしよう(GitHubアカウント統合編) ~ JenkinsのGitHub Organizationの設定方法 ~
    爆速でJenkinsをマスターしよう(GitHubアカウント統合編) ~ JenkinsのGitHub Organizationの設定方法 ~
  • Expressで「Cannot set headers after they are sent to the client」と怒られた時の対処法
    Expressで「Cannot set headers after they are sent to the client」と怒られた時の対処法
  • SAKURAのメールボックスで独自ドメインのメールを設定し、Gmail経由で送受信する方法
    SAKURAのメールボックスで独自ドメインのメールを設定し、Gmail経由で送受信する方法
  • バンクーバー留学豆知識:バンクーバーのATMで日本の銀行のキャッシュカードを使ってお得にお金を引き出す方法
    バンクーバー留学豆知識:バンクーバーのATMで日本の銀行のキャッシュカードを使ってお得にお金を引き出す方法
  • [tips][perl] Perlで文字コードをいい感じに処理する方法
    [tips][perl] Perlで文字コードをいい感じに処理する方法
  • PythonでWebスクレイピング入門(Scrapy+Selenium編)
    PythonでWebスクレイピング入門(Scrapy+Selenium編)
  • Amazon EC2インスタンスにSSHできなくなった時の対処法
    Amazon EC2インスタンスにSSHできなくなった時の対処法
  • SpringBootのProfile毎にプロパティを使い分ける3つの方法
    SpringBootのProfile毎にプロパティを使い分ける3つの方法

Bitcoin寄付 / BTC Donation

Bitcoinを寄付しよう

BTC
Select Payment Method
Personal Info

Donation Total: BTC 0.0010

このブログの運営のためにBitcoinでの寄付を募集しています。お気持ち程度の寄付を頂けると管理者の励みになります。

Bitcoin寄付について知りたい方はこちらの記事へ

ビットコイン取引ならここ

  • ホーム
  • 技術 Tips & Tutorials
  • 技術塾
  • ライフハック
  • 海外留学
  • 英語学習
  • コラム
  • サイトマップ
  • タグ一覧
  • プライバシーポリシー
  • お問い合わせ

Copyright © 2023 KD - Casual Developers Notes