Help us understand the problem. What is going on with this article?

Node.js&GraphQL part1~基本操作~

More than 1 year has passed since last update.

GraphQLとは

簡単に言うとモダンなREST APIといったところ。
「クライアント ー REST API / GraphQL ー サーバー」のようにどちらもクライアントとサーバーの橋渡しをしている点と、HTTPメソッドで動く点は同じ。HTTPメソッドで動くということは、GraphQLもRest APIと同様にどのような言語/データベース/クライアントにも対応できるということ。

Rest API との違いは、一つのエンドポイントで好きなデータを問い合わせできるという点。欲しいデータに対してその都度エンドポイントを変える必要が無く、リクエストに対してクライアント側で多様な GraphQL query なるものをくっつけてやれば、それで取得データを決定することができる。

例)
・REST API ⇒ GET/posts/123 とか GET/posts?author=3421 とか
・GraphQL ⇒ POST/graphql (w/ a graphQL query) オンリー

これはつまり以下のように言える
・REST API ⇒ エンドポイントをもとにサーバーが必要なデータを決定する
・GraphQL ⇒ GraphQL queryを利用してクライアントが必要なデータを決定する

GraphQLの利点

1. 速い
GraphQL query の記述を変えればHTTPリクエスト1回で必要なデータをごっそりと取得できる ⇒ 速い

2.柔軟
GraphQL query の記述を変えるだけでエンドポイントを増やさずに取得するデータの量や種類を決定できる(モバイルには少量のデータを送るとか)

3.簡単
一つのエンドポイントしか管理しなくてよいから

Hello GraphQL

サーバー ⇒ クライアント で既存のデータを取得してみる

1 ) プロジェクトの初期化とBabelのインスト-ル

> npm init
> npm install babel-cli babel-preset-env
※ babel-cli:Babelのコンパイルに必要なコマンドを実行するのに必要
※ babel-preset-env:Babelに何をパースすべきか伝える

2 ) package.json / .babelrc / index.js を以下のように作成
キャプチャ.JPG
3 ) 「npm run start」 でコンパイルしてBabelの動作確認

> npm run start

> graphql@1.0.0 start C:\Users\xxx\Desktop\GraphQL\graphql
> babel-node src/index.js

hello

4 ) GraphQL Yogaのセットアップ

GraphQLはクエリがどのように働くかを決めているだけで実装そのものではない。
つまり、環境によってどのように実装するかを決めるのは開発者の仕事。

例えるなら、JavascriptがECMAスクリプトでどのように働くかを記述されており、そのJavaScriptを動かすためにChromeはV8エンジンを、MozillaはSpiderMonkeyを実装しているといった感じ。

ということで、今回はまずGraphQLがNodejs上で動くような環境作りから始める必要があり、そのために GraphQL Yoga を使用する。GraphQL Yoga は多機能&高性能&簡単で人気。

> npm i graphql-yoga

ダミーのデータをサーバーに準備しつつセットアップ。

index.js
import { GraphQLServer } from 'graphql-yoga';

// Type definition (Schema)
const typeDefs = `
    type Query { 
        hello: String!
        name: String!
    }
`

// Resolvers 
const resolvers = {
    Query : {
        hello() {
            return 'Hello GraphQL'
        },
        name() {
            return 'Hugo'
        }
    }
}

const server = new GraphQLServer({
    typeDefs,
    resolvers
})

server.start(() => {
    console.log('server is up');
})

  1. typeDefs でサポートする型を定義する(スキーマの定義)
  2. resolvers でクエリに対する処理を定義する
  3. serverのインスタンスを作成する
  4. serverを立ち上げる

※ TypeDefsは「クエリ名: 型!」で定義する(末尾の「!」が無い場合は null 可能ということ)

5 ) 実行

>npm run start
> graphql@1.0.0 start C:\Users\xxx\Desktop\GraphQL\graphql
> babel-node src/index.js

server is up

この状態で「localhost:4000」につなぐと GraphQL の Playground につながるので、クエリの実行と Hello GraphQL を確認する。
キャプチャ.JPG

6 ) ついでに Live Reload セットアップ
プロジェクトを保存する度にサーバーを自動でリロードするように設定する。

> npm install nodemon --save-dev

nodemonをインストールしたら package.json の scripts を以下のように書き変える。

package.json
  "scripts": {
    "start": "nodemon src/index.js --exec babel-node"
  }

index.jsを自動で実行しなおす、babel-nodeを通して。という意味になる。
これで「npm run start」を実行することで自動リロード状態になる。

GraphQL Types

【Scalar Type】

5つのビルトイン type が存在する。
1. ID: ユニークなid
2. String: 文字列
3. Int: 整数
4. Float: 小数
5. Boolean: true/false

ScalarType
// Type definition (Schema)
const typeDefs = `
    type Query { 
        id: ID!
        name: String!
        age: Int!
        height: Float!
     isAdult: Boolean!
    }
`

【Custom Type】

開発者が定義する type。

CustomType
// Type definition (Schema)
const typeDefs = `
    type Query { 
        me: User!
    }
    type User {
        id: ID!
        name: String!
        email: String!
        age: Int
    }
`

// Resolvers 
const resolvers = {
    Query : {
        me(){
            return {
                id: '12345',
                name: 'Hugo',
                email: 'hugo@example.com',
                age: 27
            }
        }
    }
}
`

※ クエリの形に注意
キャプチャ.JPG

Resolver Operation Arguments

index.js
// 引数に '!' が有る場合、その引数は required
const typeDefs = `
    type Query { 
        greeting(name: String, position: String): String!
        add(a:Float!, b:Float!): Float!
    }
`

const resolvers = {
    Query : {
        greeting(parent, args, ctx, info) {
            if(args.name && args.position) {
                return `Hello, ${args.name}! You are my favorite ${args.positino}`;
            } else {
                return 'Hello!'
            }
        },
        add(parent, args){
            return args.a + args.b
        }
    }
}

キャプチャ.JPG

Resolverには4つの引数が存在する

  1. parent : Type間にリレーションがある場合の親を指す
  2. args : operation arguments supplied
  3. ctx : contexutual data (ユーザの情報)
  4. info : サーバーに送信されたアクションの情報など

Array Type の扱い

【Arrays of Scalar types】

index.js
const typeDefs = `
    type Query { 
        add(numbers: [Float!]!): Float!
    }
`

const resolvers = {
    Query : {
        add(parent, args, ctx, info){
            if(args.numbers.length === 0 ) {
                return 0
            } 
            return args.numbers.reduce( ( accumulator, currentValue ) => accumulator + currentValue )

    }
}

キャプチャ.JPG

【Arrays of Custom types】

index.js
// Demo data
const users = [{
    id: '1',
    name: 'Hugo',
    email: 'hugo@ex.com',
    age: 27
}, {
    id: '2',
    name: 'Taro',
    email: 'taro@ex.com'
}, {
    id: '3',
    name: 'Aike',
    email: 'mike@ex.com'
}]

// Type definition (Schema)
const typeDefs = `
    type Query { 
        users(query: String): [User!]!
    }
    type User {
        id: ID!
        name: String!
        email: String!
        age: Int
    }
`

const resolvers = {
    Query : {
        users(parent, args, ctx, info) {
            if (!args.query){
                return users
            } else {
                return users.filter(user => user.name.toLowerCase().includes(args.query))
            }
        }
}

キャプチャ.JPG

Relation

Type の ID を追加することでリレーションが生まれる。

index.js
// Demo data
const users = [{
    id: '1',
    name: 'Hugo',
    email: 'hugo@ex.com',
    age: 27
}, {
    id: '2',
    name: 'Taro',
    email: 'taro@ex.com'
}, {
    id: '3',
    name: 'Aike',
    email: 'aike@ex.com'
}]
const posts = [{
    id: '4',
    title: 'aaa',
    body: 'asdtfhaer',
    published: true,
    author: '1'   /// 注目
}, {
    id: '5',
    title: 'fff',
    body: 'hrtea',
    published: true,
    author: '1'   /// 注目
}, {
    id: '6',
    title: 'Ahwerike',
    body: 'aikreshe',
    published: true,
    author: '2'   /// 注目
}]

const typeDefs = `
    type Query { 
        users(query: String): [User!]!
        posts(query: String): [Post!]!        
    }

    type User {
        id: ID!
        name: String!
        email: String!
        age: Int
        posts : [Post!]!  /// 注目
    }

    type Post {
        id: ID!
        title: String!
        body: String!
        published: Boolean!
        author: User!   /// 注目
    }
`

そして、Resolverにオブジェクトを追加することでクエリ時にリレーションをもつTypeを引きずってくることができる

index.js
const resolvers = {

    Query : {
        users(parent, args, ctx, info) {
            return users    // call posts() coded below (userの数だけcallする)
        },
        posts(parent, args, ctx, info) {
            return posts    // call author() coded below (postの数だけcallする)
        }
    },

    Post: {
        author(parent, args, ctx, info) {
            return users.find(user => user.id === parent.author) // parentはPost
       // 自分(Post)のauthor(UserのID)とマッチするUserを返却

        }
    },

    User: {
        posts(parent, args, ctx, info) {
            return posts.filter(post => post.author === parent.id) // parentはUser
       // 自分(User)のIDとマッチするauthorをもつPostを返却
        }
    }
}

キャプチャ.JPG

まとめ

index.js
import { GraphQLServer } from 'graphql-yoga';

// Demo data
// Arrayをフィールドに持ったりなんかしないですよ
const users = [{
    id: '1',
    name: 'Hugo',
    email: 'hugo@ex.com',
    age: 27,
}, {
    id: '2',
    name: 'Taro',
    email: 'taro@ex.com'
}, {
    id: '3',
    name: 'Aike',
    email: 'aike@ex.com'
}]

const posts = [{
    id: '4',
    title: 'About React',
    body: 'intriguing',
    published: true,
    author: '1' // userのID
}, {
    id: '5',
    title: 'About GraphQL',
    body: 'Im now studying',
    published: true,
    author: '1'
}, {
    id: '6',
    title: 'Money',
    body: 'very little',
    published: true,
    author: '2'
}]

const comments = [{
    id: '11',
    text: 'oh my god!',
    author: '1',  // userのID
    post: '4'    // postのID
}, {
    id: '21',
    text: 'jesus christ',
    author: '1',
    post: '4'
}, {
    id: '31',
    text: 'goo goo dloo l doo',
    author: '2',
    post: '5'
}, {
    id: '41',
    text: 'ironman',
    author: '3',
    post: '6'
}]


// Type definition (Schema)
const typeDefs = `
    type Query { 
        users(query: String): [User!]!
        posts(query: String): [Post!]!
        comments: [Comment!]!        
    }

    type User {
        id: ID!
        name: String!
        email: String!
        age: Int
        posts : [Post!]!
        comments: [Comment!]!
    }

    type Post {
        id: ID!
        title: String!
        body: String!
        published: Boolean!
        author: User!
        comments: [Comment!]!
    }

    type Comment {
        id: ID!
        text: String!
        author: User!
        posts: Post!
    }
`

// Resolvers
const resolvers = {

    Query : {

        users(parent, args, ctx, info) {
            if (!args.query){
                return users
            } else {
                return users.filter(user => user.name.toLowerCase().includes(args.query.toLowerCase()))
            }
        },

        posts(parent, args, ctx, info) {
            if (!args.query){
                return posts    // call Post() coded below
            } else {
                return posts.filter(post => post.title.toLowerCase().includes(args.query))
            }
        },

        comments(parent, args, ctx, info){
            return comments
        }
    },

    Post: {
        author(parent, args, ctx, info) {
            return users.find(user => user.id === parent.author) // parentはPost
        },
        comments(parent, args, ctx, info){
            return comments.filter(comment => comment.post === parent.id)
        }
    },

    User: {
        posts(parent, args, ctx, info) {
            return posts.filter(post => post.author === parent.id) // parentはUser
        },
        comments(parent, args, ctx, info){
            return comments.filter(comment => comment.author === parent.id)
        }
    },

    Comment: {
        author(parent, args, ctx, info){
            return users.find(user => user.id === parent.author) // parentはComment
        },
        posts(parent, args, ctx, info){
            return posts.filter(post => post.id === parent.post);
        }
    }
}

const server = new GraphQLServer({
    typeDefs,
    resolvers
})

server.start(() => {
    console.log('server is up');
})

キャプチャ.JPG

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした