GraphQL

【 GraphQL 】用 TypeScript 寫一個 Apollo server

1. 建立專案環境

可以先參考【 Node.js 】如何在 Node.js 中建立 TypeScript 的環境

1.1 建立專案資料夾

mkdir GraphQL-with-TypeScript

1.2 初始化專案

cd GraphQL-with-TypeScript
npm init

1.3 安裝相關套件

npm install --save graphql apollo-server typescript @types/node nodemon ts-node

1.4 初始化 TypeScript

tsc --init

如果遇到 error ,請參考:【 Node.js 】如何在 Node.js 中建立 TypeScript 的環境

1.5 新增進入點檔案並修改 package.json

新增 src 資料夾,並新增一個 server.ts 作為進入點檔案。

mkdir src
touch src/server.ts

修改 package.json ,將 “main” 欄位改為進入點檔案的位置,並修改 “script” ,設定 nodemon 的進入點檔案。

// package.json
{
  ...
  "main": "src/server.ts",
  "scripts": {
    "start": "nodemon src/server.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...
}

nodemon 是開發 Node.js 時常用的套件,每當有檔案儲存時, nodemon 就會自動重啟 server,讓你可以即時看到修改的 code 產成什麼樣的影響。

1.6 測試是否成功

先新增以下程式到 src/server.ts 裡:

// src/server.ts
console.log('Works!!')

在終端機輸入:

npm start

成功的話,在終端機會顯示以下畫面:

GraphQL

2. 用 TypeScript 寫一個 Apollo server

Apollo 是用來處理 GraphQL 的一個套件,裡面包含了許多開發 GraphQL 會用到的工具。

2.1 建立 schema

在建立 Apollo Server 之前得先建立 GraphQL 的 schema,通常會習慣將 schema 另開一個資料夾,並將所有 GraphQL 的 schema 都寫在這個資料夾,以便專案變大時方便管理。

mkdir src/schema
touch src/schema/schema.graphql

GraphQL 的 schema 都是以 graphql 為副檔名:

# src/schema/schema.graphql
type Query {
  users: String
  # 當 query 的 field 是 users 時,Apollo 回傳的response data 型別為 string
}

2.2 建立 resolvers

resolvers 是用來告訴 Apollo Server 當 GraphQL 的 request 傳進來 server 時, Apollo Server 應該回傳什麼樣的 response data,通常也會新增一個資料夾用來放所有的 resolvers:

mkdir src/resolvers
touch src/resolvers/resolvers.ts

接著就要來建立我們的 reolvers 了,首先做個簡單的測試,當我們發送 query 是 users 的 request 到我們的 server 時,希望 resolvers 回傳一段文字:’Query of users success!!’ ,這也是為什麼剛剛的 schema 寫成 : users: String 的原因,代表回傳的 response data type 是 string。

// src/resolvers/resolvers.ts
export const resolvers = {
  Query: {
    users: () => `Query of users success!!`
  }
}

2.3 建立 Apollo Server

有了 schema 和 resolvers 後就可以來建立 Apollo Server 了!

// src/server.ts
import * as path from 'path';
import * as fs from 'fs';
import { ApolloServer } from 'apollo-server';

import { resolvers } from './resolvers/resolvers'

const server = new ApolloServer({
  // 用 Node.js 的 fs 和 path 模組 來讀取我們的 schema 檔案
  typeDefs: fs.readFileSync(
    path.join(__dirname, './schema/schema.graphql'),
    'utf8'
  ),
  resolvers,
});

server
  .listen()
  .then(({ url }) => {
    console.log(`Server is running on ${url}`)
  });

接著用 nodemon 執行我們的 server.ts:

npm start
GraphQL

2.4 GraphQL playground

這時候在瀏覽器打開 http://localhost:4000/ 時會有一個 GraphQL playground ,讓你可以直接輸入 query ,模擬有 GraphQL request 傳到 server 時的情況,以及收到的 response。

GraphQL playground
  ▲ GraphQL playground

點擊右邊的 DOCS 會出現目前可以 query 的 field ,也就是你目前在 schema.graphql 檔案裡的 field:

GraphQL playground

2.5 用 GraphQL playground 測試 request

GraphQL playground 的左側可以輸入 schema 的 field ,按下中間的 play 按鈕就會模擬發送 request 到 server 的情況,右邊則會顯示接收到的 response :

GraphQL playground

3. 加入 mock data

現在讓我們再更進階一點,讓 Apollo server 幫我們回傳自己建立的假資料。

3.1 修改 schema

因為之後回傳的資料不一樣了,所以也要修改 schema ,確定回傳的資料型別:

# src/schema/schema.graphql
type Query {
  users: [User]
}

type User {
  id: Int
  name: String
  age: Int
}

這樣代表回傳回來的資料會是一個 array ,且 array 裡每一個 element 的 type 都是 User。

3.2 建立 mock data,並修改 resolvers

這邊先直接將 mock data 寫在 resolvers .ts 裡,並且修改 resolvers:

// src/resolvers/resolvers.ts
const users = [
  {
    id: 1,
    name: 'Jimmy1',
    age: 18,
  },
  {
    id: 2,
    name: 'Jimmy2',
    age: 20,
  }
]

export const resolvers = {
  Query: {
    // 一般來說 resolvers 會有 4 個參數,目前還不會用到,之後用到的時候再一一說明
    // 當 query field 有 users 時,回傳 users 陣列
    users: async(parent: any, args: any, context: any, info: any) => {
      return users;
    }
  }
}

3.3 用 GraphQL playground 測試 request

因為我們用 nodemon 在 run 我們的 server.ts ,所以每當有檔案修改時就會自動重 load server.ts 檔,現在回到瀏覽器的 GraphQL playground 重新整理後就可以繼續測試是否有成功修改到 response 回傳的 data:

GraphQL

Apollo Server 會根據你寫的 field 回傳不同的資料,假設今天某個頁面只需要 user 的 id 就可以像上面那樣寫,但假設今天某個頁面需要的資料比較完整,就可以寫成下面這樣:

GraphQL

這就是 GraphQL 的好處,根據不同的 query 回傳不同的 data ,讓你的 API 變得非常彈性!

4 在 request 加入變數

有時候我們並不只想回傳全部的資料,還會希望根據 request 的變數來回傳特定的資料,例如:在 request 中放入 id,根據這個 id 找到該 user 的資料。

4.1 新增 schema

這邊要注意的是在 request 要放的變數必須用 input 開頭來指定型別:

# src/schema/schema.graphql
type Query {
  users: [User]
  user(userInput: UserConfig): User
}

type User {
  id: Int
  name: String
  age: Int
}

input UserConfig {
  id: Int
}

4.2 修改 resolvers

這時候會用到 resolvers 的第 2 個參數:args,args 參數就是用來存取 request 裡的變數用的:

// /src/resolvers/resolvers.ts
export const resolvers = {
  Query: {
    users: async(parent: any, args: any, context: any, info: any) => {
      return users;
    },
    user: async(parent: any, args: any, context: any, info: any) => {
      const userId = args.userInput.id;
      return users.find(v => v.id === userId);
    }
  }
}

4.3 用 GraphQL playground 測試 request

GraphQL 的變數可以直接寫在 query field 裡,也可以寫在 playground 的 QUERY VARIABLE 裡,不過兩種寫法不太一樣:

4.3.1 將變數寫在 query field 裡

GraphQL

可以看到我們成功拿到 id = 1 的 user 資訊了!這種寫法不需要指定型別,接著來看看另一種寫法:

4.3.2 將變數寫在 playground 的 QUERY VARIABLE 裡

GraphQL

這種寫法要先在 query 裡指定變數的型別 ( 必須和 schema 裡的型別一樣 ) ,在左下角的 QUERY VARIABLE 給該變數賦值,最後再到 query field 裡指定該參數要使用哪個變數。

5. 用 mutation 來對資料做修改

到目前為止我們都是用 query 來發送 request ,當我們要對資料做修改時 ( 新增、修改、刪除 ) ,則需要用到 mutation 這個 field。

5.1 新增 schema

記得剛剛說的:要在 request 放變數時,變數的型別指定必須用 input 開頭,所以新增 schema 如下:

type Mutation {
  createUser(userInput: UserInput): User
}

type User {
  id: Int
  name: String
  age: Int
}

input UserInput {
  id: Int
  name: String
  age: Int
}

5.2 修改 resolvers

直接在 Query 的後面新增 Mutation,利用 args.userInput 後再 push 到目前的陣列中:

// src/resolvers/resolvers.ts
export const resolvers = {
  Query: {
    users: async(parent: any, args: any, context: any, info: any) => {
      return users;
    },
    user: async(parent: any, args: any, context: any, info: any) => {
      const userId = args.userInput.id;
      return users.find(v => v.id === userId);
    }
  },
  Mutation: {
    createUser: async(parent: any, args: any, context: any, info: any) => {
      const user = args.userInput;
      users.push(user);
      return user;
    },
  }
}

5.3 用 GraphQL playground 測試 request

像這種變數比較多欄位的情況就會滿建議將變數寫在 QUERY VARIABLE 裡面,以免 query field 變成一大包:

mutation($userInput: UserInput) {
  createUser(userInput: $userInput) {
    id
    name
    age
  }
}

# QUERY VARIABLES
{
  "userInput": {
    "id": 3,
    "name": "Jimmy3",
    "age": 22
  }
}
GraphQL

如此一來就成功建立新的 user 了!接著我們用 query 來拿回所有 user ,看是不是真的成功建立新 user:

GraphQL

發現多了一個 id = 3 的 user ,代表成功新增了!

5.4 新增 updateUser 和 deleteUser

以下提供我的 code 給大家參考:

# src/schema/schems.graphql
type Query {
  users: [User]
  user(userInput: UserConfig): User
}

type Mutation {
  createUser(userInput: UserInput): User
  updateUser(userInput: UserInput): User
  deleteUser(userConfig: UserConfig): User
}

type User {
  id: Int
  name: String
  age: Int
}

input UserInput {
  id: Int
  name: String
  age: Int
}

input UserConfig {
  id: Int
}
// src/resolvers/resolvers.ts
export const resolvers = {
  Query: {
    users: async(parent: any, args: any, context: any, info: any) => {
      return users;
    },
    user: async(parent: any, args: any, context: any, info: any) => {
      const userId = args.userInput.id;
      return users.find(v => v.id === userId);
    }
  },
  Mutation: {
    createUser: async(parent: any, args: any, context: any, info: any) => {
      const user = args.userInput;
      users.push(user);
      return user;
    },
    updateUser: async(parent: any, args: any, context: any, info: any) => {
      const updatedUser = args.userInput;
      const updatedUserIndex = users.findIndex((user) => user.id === updatedUser.id);
      users[updatedUserIndex] = updatedUser;
      return updatedUser;
    },
    deleteUser: async(parent: any, args: any, context: any, info: any) => {
      const deletedUserId = args.userConfig.id;
      const deletedUserIndex = users.findIndex((user) => user.id === deletedUserId);
      const deletedUser = users.find((u, i) => i === deletedUserIndex);
      users.splice(deletedUserIndex, 1);
      return deletedUser;
    },
  }
}

6. 參考資料

The Fullstack Tutorial for GraphQL

如果覺得我的文章有幫助的話,歡迎幫我的粉專按讚哦~謝謝你!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top