Node.js の HTTP サーバフレームワーク Koa で作られた API サーバに GraphQL 導入する方法です。
簡略化した例として、TodoリストのAPIサーバとして POST /api/todo で新規アイテムを登録し、 GET /api/todo でリストで取得できるもので説明します。同様の操作ができる機能を GraphQL で提供します。
RESTful API サーバの実装状態#
koa-router はネストすることが可能なので、 /api は apiRouter として定義し、後で /graphql は別途 graphqlRouter として利用します。
package.json#
依存する npm モジュールは以下の通りです。
1
2
3
4
5
6
7
8
9
|
{
"dependencies": {
"graphql": "^14.2.1",
"koa": "^2.7.0",
"koa-bodyparser": "^4.2.1",
"koa-graphql": "^0.8.0",
"koa-router": "^7.4.0"
}
}
|
models/todo.js#
メモリ上の配列でTodoを管理する簡易的なものです。中身は同期処理ですが、より実践的にするため async (Promise) にしています。
1
2
3
4
5
6
7
8
9
10
11
12
|
const todo = []
exports.getItems = async () => {
return todo
}
exports.createItem = async ({ content }) => {
const created = new Date().toISOString()
const newItem = { content, created }
todo.push(newItem)
return newItem
}
|
routes/api.js#
REST API /api 以下のハンドリングとリクエストボディのパースをしています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const Router = require('koa-router')
const bodyParser = require('koa-bodyparser')
const todo = require('../models/todo')
const apiRouter = new Router()
apiRouter
.use(bodyParser())
.post('/todo', async (ctx, next) => {
const { content } = ctx.request.body
ctx.body = await todo.createItem({ content })
})
.get('/todo', async (ctx, next) => {
ctx.body = await todo.getItems()
})
module.exports = apiRouter
|
app.js#
サーバの基礎部分です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const rootRouter = new Router()
rootRouter
.use('/api', require('./routes/api').routes())
//.use('/graphql', require('./routes/graphql').routes()) ここを後でコメントアウトします。
app
.use(rootRouter.routes())
.use(rootRouter.allowedMethods())
app.listen(3000)
|
GraphQL#
routes/graphql/type.js#
- OperationType として createTodoItem, getTodoItems を提供します。
- Todoアイテムの型として返却用の ObjectType TodoItemType と、登録用の InputObjectType TodoItemInputType を定義します。
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
|
const {
GraphQLObjectType,
GraphQLInputObjectType,
GraphQLNonNull,
GraphQLString,
GraphQLList,
} = require('graphql')
const todo = require('../../models/todo')
const TodoItemType = new GraphQLObjectType({
name: 'TodoItem',
fields: () => ({
content: { type: new GraphQLNonNull(GraphQLString) },
created: { type: new GraphQLNonNull(GraphQLString) },
})
})
const TodoItemInputType = new GraphQLInputObjectType({
name: 'TodoItemInput',
fields: () => ({
content: { type: new GraphQLNonNull(GraphQLString) }
})
})
const createTodoItem = {
type: TodoItemType,
description: 'Create new todo item',
args: {
item: { type: TodoItemInputType }
},
resolve: async (value, { item }, ctx) => {
return todo.createItem(item)
}
}
const getTodoItems = {
type: new GraphQLNonNull(new GraphQLList(TodoItemType)),
description: 'Get todo items',
resolve: async (value, args, ctx) => {
return todo.getItems()
}
}
module.exports = {
createTodoItem,
getTodoItems,
}
|
routes/graphql/index.js#
- type から QueryType と MutationType を定義して、 schema にしています。
- koa-graphql はミドルウエアとして利用して、 schema に基づいた graphqlRouter を提供します。ちなみに今回の例では引数を function にしなくても問題ないですが、実践的に使う場合は context にユーザー情報を渡すなど必要になってきます。
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
|
const Router = require('koa-router')
const graphqlHTTP = require('koa-graphql')
const { GraphQLSchema, GraphQLObjectType } = require('graphql')
const { createTodoItem, getTodoItems } = require('./type')
const query = new GraphQLObjectType({
name: 'Query',
description: 'Query definitions',
fields: () => ({
getTodoItems
})
})
const mutation = new GraphQLObjectType({
name: 'Mutation',
description: 'Mutation definitions',
fields: () => ({
createTodoItem
})
})
const schema = new GraphQLSchema({ query, mutation })
const graphqlRouter = new Router()
graphqlRouter
.all('/', graphqlHTTP(req => {
return {
schema,
context: {},
graphiql: true,
pretty: true
}
}))
module.exports = graphqlRouter
|
graphql エンドポイントを 追加#
1
2
3
|
rootRouter
.use('/api', require('./routes/api').routes())
.use('/graphql', require('./routes/graphql').routes()) // ここを有効に
|
GraphQL のパス名は /graphql とすることが慣習的に多いようです。
実行結果を確認#
サーバを起動して試します。
node app.js
Todoアイテムを REST API に POST して登録#
ターミナルから curl で登録します。
1
2
3
4
|
curl -X POST \
-H 'Content-Type: application/json;charset=UTF-8' \
-d '{"content":"牛乳を買う"}' \
http://localhost:3000/api/todo
|
1
|
{"content":"牛乳を買う","created":"2019-04-13T10:20:48.377Z"}
|
Todoアイテムを GraphQL mutation から登録#
ブラウザで http://localhost:3000/graphql を開くとコンソールが開きます。
左のペインで下記を定義して再生ボタンでリクエストします。
1
2
3
4
5
6
7
8
|
mutation {
createTodoItem(item: {
content: "図書館に本を返す"
}) {
content,
created
}
}
|
右のペインに結果が表示されます。
1
2
3
4
5
6
7
8
|
{
"data": {
"createTodoItem": {
"content": "図書館に本を返す",
"created": "2019-04-13T10:22:48.405Z"
}
}
}
|
Todoリストを GraphQL query で取得#
左のペインで下記を定義して再生ボタンでリクエストします。
1
2
3
4
5
6
|
query {
getTodoItems {
content,
created
}
}
|
右のペインに結果が表示されます。 REST の POST で登録したものと GraphQL の mutation で登録したものが両方返ってきています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"data": {
"getTodoItems": [
{
"content": "牛乳を買う",
"created": "2019-04-13T10:20:48.377Z"
},
{
"content": "図書館に本を返す",
"created": "2019-04-13T10:22:48.405Z"
}
]
}
}
|
Todoリストを REST API の GET で取得#
ターミナルから curl で確認してみます。
1
|
curl http://localhost:3000/api/todo
|
1
|
[{"content":"牛乳を買う","created":"2019-04-13T10:20:48.377Z"},{"content":"図書館に本を返す","created":"2019-04-13T10:22:48.405Z"}]
|