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())
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
とすることが慣習的に多いようです。
実行結果を確認
サーバを起動して試します。
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"}]
|
tilfin
freelance software engineer