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

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

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 とすることが慣習的に多いようです。

実行結果を確認

サーバを起動して試します。

1
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"}]