點燈坊

失くすものさえない今が強くなるチャンスよ

如何使用 Apollo Server Express ?

Sam Xiao's Avatar 2019-11-13

除了 Apollo Server 外,Apollo 另外提供了 Apollo Server Express,可搭配 Express 與其他 Middleware 做進階應用。

Version

macOS Catalina 10.15.1
WebStorm 2019.2.4
Node 10.16.3
Apollo Server Express 2.9.7

Introduction

Apollo 官方提供了 Apollo Server 與 Apollo Server Express,兩者有以下差異:

Apollo Server

  • 不可使用 Express 的 middleware
  • Server 的任意 path 都可執行 GraphQL API 與 GraphQL Playground
  • 已經設定好 WebSocket
  • 適合一般使用

Apollo Server Express

  • 可自行使用 Express 的 middleware
  • 預設以 /graphql 為 path
  • WebSocket 須另外設定
  • 適合進階使用

可發現大部分場景 Apollo Server 就很夠用,若需使用 middleware 預期他進階功能,則要使用 Apollo Server Express。

Install Package

$ yarn add express apollo-server-express graphql
$ yarn add @babel/core @babel/cli @babel/preset-env @babel/node --dev
$ yarn add nodemon rimraf cross-env --dev

安裝 Express、Apollo Server Express、GraphQL 與 Babel 所需 package。

$ yarn add express apollo-server-express graphql

安裝 Express、Apollo Server Express 與 GraphQL。

Apollo Server Express 只是 Express 的 middleware,因此必須安裝 Express

$ yarn add @babel/core @babel/cli @babel/preset-env @babel/node --dev

安裝 Babel 相關 package,由於只是轉譯用,安裝成 devDependency 即可。

其中 @babel/node 負責將 ES6+ 轉譯成 Node 能執行的 js。

$ yarn add nodemon rimraf cross-env --dev

安裝 nodemon 可監控檔案修改重新 Babel 轉譯與重啟 Node。

安裝 rimraf,稍後 yarn clean 時刪除 dist 目錄。

安裝 cross-env 可跨平台設定 NODE_ENV 環境變數。

server/package.json

path005

Babel Configuration

server/.babelrc

{
  "presets": [
    "@babel/preset-env"
  ]
}

在 project 根目錄下建立 .babelrc,使用剛剛安裝的 @babel/preset-env 的設定轉譯 ES6+。

Nodemon Configuration

server/nodemon.json

{
  "exec": "NODE_ENV=development yarn dev",
  "watch": ["server/src/*"],
  "ext": "js, json"
}
  • exec:當有變動時,將執行 yarn dev
  • watch:Nodemon 將持續觀察的目錄
  • ext:Nodemon 將持續觀察的 extension

watchext 可視實際需求加以修改

Node

src/index.js

import { ApolloServer, gql } from 'apollo-server-express'
import express from 'express'

let data = [
  { title: 'FP in JavaScript', category: 'FP'},
  { title: 'RxJS in Action', category: 'FRP'},
  { title: 'Speaking JavaScript', category: 'JS'}
]

let typeDefs = gql`
  type Query {
    books(category: BookCategory!): [Book]
  }

  type Book {
    title: String
    category: BookCategory
  }

  enum BookCategory {
    FP
    FRP
    JS
  }
`
let books = (_, { category }) => data.filter(x => x.category === category)

let resolvers = {
  Query: {
    books
  }
}

let app = express()

let apolloServer = new ApolloServer({ typeDefs, resolvers })

apolloServer.applyMiddleware({ app })

app.listen({ port: 4000 },
  () => console.log(`GraphQL Server ready at http://localhost:4000${ apolloServer.graphqlPath }`)
)

server/src 目錄下建立 index.js

第 1 行

import { ApolloServer, gql } from 'apollo-server-express'

apollo-server-express import 進 ApolloServergql

注意是 apollo-server-express 不是 apollo-server

第 2 行

import express from 'express'

express import 進 express。

第 4 行

let data = [
  { title: 'FP in JavaScript', category: 'FP'},
  { title: 'RxJS in Action', category: 'FRP'},
  { title: 'Speaking JavaScript', category: 'JS'}
]

原始資料為 array of object,包含 titlecategory 兩個 property。

10 行

let typeDefs = gql`
  ...
`

typeDefs 內定義 GraphQL schema。

11 行

type Query {
  books(category: BookCategory!): [Book]
}

定義 books query,其 category argument 型別為 BookCategory enum 且不可為 null,回傳為 Book type array。

15 行

type Book {
  title: String
  category: BookCategory
}

定義 Book type,包含 object 內的 titlecategory,其中 category 型別為 BookCategory enum。

20 行

enum BookCategory {
  FP
  FRP
  JS
}

定義 BookCategory enum,包含 FPFRPJS 三個 enumeration。

29 行

let resolvers = {
  Query: {
    books
  }
}

resolvers 內定義 books query。

27 行

let books = (_, { category }) => data.filter(x => x.category === category)

實現 books query,resolver 依序有 4 個 argument: parentargscontextinfo,讀取 argument 只會用到第二個 args,而 parent 目前用不到可用 _ 表示, contextinfo 目前可忽略。

直接從 args destructure 出 category 使用。

34 行

let app = express()

使用 express() 建立 app object。

36 行

let apolloServer = new ApolloServer({ typeDefs, resolvers })

使用 ApolloServer 建立 apollo object,將 typeDefsresolvers 組合成 object 傳進其 constructor。

38 行

apolloServer.applyMiddleware({ app })

將 Apollo Server Express 以 middleware 套用到 Express 上。

43 行

app.listen({ port: 4000 },
  () => console.log(`GraphQL Server ready at http://localhost:4000${ apollo.graphqlPath }`)
)

Apollo Server Express 預設會以 /graphql 為 path。

僅管不使用 Express 設定 /,Apollo Server Express 預設也只使用 /graphql 為 path

Yarn Script

server/package.json

"scripts": {
  "serve": "nodemon",
  "start": "node ./dist/index.js",
  "dev": "babel-node ./src/index.js",
  "prod": "yarn clean && cross_env NODE_ENV=production yarn build && yarn start",
  "clean": "rimraf dist",
  "build": "babel ./src --out-dir dist",
  "docker:build": "yarn clean && cross_env NODE_ENV=production yarn build && docker build -t $npm_package_name:$npm_package_version ."
},

設定 Apollo Server Express 的 Yarn script。

"serve": "nodemon",

yarn server 執行 nodemon

"start": "node ./dist/index.js",

yarn start 使用 node 執行 Babel 轉譯後的 js 並啟動 Express 與 Apollo Server Express。

"dev": "babel-node ./src/index.js",

yarn dev 使用 babel-node 將 ES6+ 轉譯成 Node 能執行的 js 在記憶體內,並啟動 Express 與 Apollo Server Express。

"prod": "yarn clean && cross_env NODE_ENV=production yarn build && yarn start",

yarn prod 設定 NODE_ENVproduction,且依序執行:

  • yarn clean 刪除 dist 目錄
  • yarn build 使用 Babel 轉譯成 Node 能執行的 js
  • yarn start 使用 node 執行 Babel 轉譯後的 js 並啟動 Express 與 Apollo Server Express
"clean": "rimraf dist",

yarn clean 刪除 dist 目錄。

"build": "babel ./src --out-dir dist"

yarn build 使用 Babel 將 src 目錄下的 ES6+ 轉譯成 Node 能執行的 js 到 dist 目錄下。

將來要包進 Docker 的 js 也是 dist 目錄,而非 src 目錄

  "docker:build": "yarn clean && cross_env NODE_ENV=production yarn build && docker build -t $npm_package_name:$npm_package_version ."

yarn docker:buildyarn prod 類似,差異只在最後 docker:build 包成 image 而非啟動 Apollo Server Express。

path009

Summary

雖然 Yarn script 內分的很細,但其實會用到的 script 只有 3 個:

  • yarn serve:development 時使用,存檔後 Nodemon 會自動 Babel 轉譯,以 babel-node 重新啟動 Express 與 Apollo Server Express
  • yarn build:development 時使用,以 babelsrc 目錄下所有 js 重新轉譯到 dist 目錄下
  • yarn docker:build:產生 Apollo Server Express 的 Docker image

Dockerfile

server/dockerfile

FROM node:lts-alpine
WORKDIR /usr/app
COPY package.json .
RUN yarn install --production
COPY dist/* ./
CMD [ "node", "index.js" ]

server 目錄下建立 dockerfile,由於我們要另外安裝 Express 與 Apollo Server Express,因此要另外寫 dockerfile 安裝 package,不可直接使用 docker-compose.yml

第 1 行

FROM node:lts-alpine

使用最新 LTS 版的 node:lts-alpine 為基底建立 image。

建議使用 alpine 為 production image,size 會小很多

第 2 行

WORKDIR /usr/app

將 working directory 切換到 /usr/app,相當於:

mkdir /usr/app
cd /usr/app

稍後 COPYRUNCMD 都會在此目錄下。

第 3 行

COPY package.json .

將根目錄的 package.json 複製到 Docker 內的 /usr/app,因為要使用 yarn install 安裝 Express 與 Apollo Server Express。

第 4 行

RUN yarn install --production

執行 yarn install 安裝 dependencies 下的 package。

第 5 行

COPY dist/* ./

將 Babel 編譯過的 js 複製進 Docker 內的 /usr/app

第 6 行

CMD [ "node", "index.js" ]

執行 Docker 內的 /usr/app/index.js 啟動 Apollo Server Express。

Start Apollo Express

$ yarn serve

使用 yarn all 同時啟動 Apollo Server Express 與 Vue。

Vue 一同往常啟動在 http://localhost:8080

path006

若訪問 http://localhost:4000,不再顯示 GraphQL Playground,而是由 Express 接管。

path008

不再如 Apollo Server 隨便打 http://localhost:4000 下的網址都可啟動 GraphQL Playground。

path007

必須明確訪問 http://localhost:4000/graphql 才能使用 GraphQL Playground。

Conclusion

  • 若要使用其他 middleware,就要使用 Apollo Server Express,但預設 path 為 /graphql 而不是 /

Sample Code

完整範例可在我的 GitHub 上找到

Reference

Apollo Docs, Integrating with Node.js middleware