
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は適切に設定する必要があります。
このように期待通りに動作しました。いろいろ試してみましょう!
おまけ
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
}
}
}
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