-->
ads here

GraphQL in practice - Part 1: Xây dựng GraphQL server với Apollo Server, ExpressJS và MongoDB

advertise here
Xin chào, hôm nay Julian Dong Blog với series bài viết về thực hành GraphQL. Series này gồm 2 bài viết về sử dụng Apollo GraphQL. Trong phần 1, mình xin giới thiệu cách xây dựng một backend server sử dựa trên Apollo Server, NodeJS và MongoDB.

Bài viết gồm các mục như sau:

  1. Giới thiệu chung, Setup Môi trường
  2. SetupDB, Apollo Server
  3. Query
  4. Mutation
  5. Subscription
Let's begin!!!

1. Giới thiệu chung

Mô tả:

Ứng dụng lần này sẽ rất đơn giản như sau, mình có một tập dữ liệu về các tác giả của những cuốn sách được lưu trữ trong cơ sở dữ liệu với các trường tên (name), năm sinh (yearOfBirth) và giới tính (gender). Mình sẽ sử dụng GraphQL để thực hiện các thao tác cơ bản CRUD trên tập dữ liệu này.

Initialize Project

  • Khởi tạo cây thư mục, tạo 1 thư mục tên server. Sau đó mở terminal, dùng lệnh
cd server  
để di chuyển vào thư mục server, sau đó gõ lệnh npm  init -y để khỏi tạo nodeJS project

  • Tạo tiếp file index.js, trong thư mục gốc, sau đó vào file package.json, sửa mục "main" trỏ đến file index.js vừa tạo
  • Cấu hình babel để sử dụng cú pháp import: tạo file .babelrc  với nội dung như sau:

// .babelrc  
 {  
  "presets": ["env"],  
  "plugins": ["transform-object-rest-spread"]  
 }  

  • Và cài đặt các packages babel-cli, babel-plugin-transform-object-rest-spread, babel-preset-env bằng yarn add hoặc npm install
yarn add babel-cli babel-plugin-transform-object-rest-spread babel-preset-env   
 // OR  
 npm install babel-cli babel-plugin-transform-object-rest-spread babel-preset-env   


  • Và cuối cùng, chính sửa file package.json như sau và cài nodemon ở global yarn add global nodemon:
...  
 "scripts": {  
   ...  
   "start": "nodemon --exec babel-node index.js"  
 },  
 ...  

Để khởi chạy server ta chỉ cần chạy lệnh yarn start hoặc npm run start
OK, vậy là chúng ta đã khởi tạo xong project. Bước tiếp theo sẽ là setup cơ sở dữ liệu và apollo server

2. Setup DB, Apollo Server

DB Setup

Để kết nối với MongDB ta có thể dùng nhiều driver khác nhau, trong bài viết này mình sẽ sử dụng driver rất phổ biến đó là mongoose.
  • Trước tiên đảm bảo rằng bạn đã cài đặt MongoDB. Cách cài đặt tuỳ theo hệ điều hành các bạn có thể tham khảo tại trang chủ MongoDB tại đây. Sau khi đã cài đặt MongoDB xong, bạn tạo 1 cơ sở dữ liệu. Ở đây là mình dùng tên demoGrapQL.
  • Cài đặt mongoose bằng yarn add mongoose 
  • Trong file index.js thêm đoạn code sau:
 import mongoose from 'mongoose';  
 // Connecting to DB  
 mongoose.connect('mongodb://127.0.0.1:27017/demoGraphQL', { useNewUrlParser: true })  
  .then(() => console.log(`Successfully connect to mongodb`))  
  .catch(error => console.log(error));  

  • Chạy lệnh yarn start, sẽ thấy kết quả Successfully connect to mongodb  tức là đã kết nối tới server thành công.

Apollo Server

Tiếp theo chúng ta tiến hành setup Apollo server.

  • Trong thư mục gốc, tạo 1 thư mục graphql  và trong đó tạo các file như sau:
    • index.js
    • typeDefs.js - Định nghĩa cú pháp của các query, mutation sử dụng trong ứng dụng.
    • resolvers.js - chứa các hàm hiện thực logic của các query, mutation được định nghĩa ở trên
  • Tiếp theo ta tiến hành cài các package sau: express, apollo-server-express, apollo-server, bằng yarn
yarn add express apollo-server-express apollo-server

  • Tiếp theo, trong file typeDefs.js và resolvers.js, thêm lần lượt các đoạn code sau:
Trong typeDefs.js:
// IN typeDefs.js  
 import { gql } from 'apollo-server';  
 const typeDefs = gql`  
  type Author {  
   id: ID!,  
   name: String!,  
   yearOfBirth: Int!,  
   gender: Boolean,  
  }  
  type Query {  
   authors: [Author]  
  }  
 `;  
 export default typeDefs;  

Trong resolvers.js:
// IN resolvers.js  
 const resolvers = {  
  Query: {  
   authors: () => {  
    return [];  
   },  
  }  
 }  

  • Tiếp theo, trong file graphql/index.js thêm đoạn code sau:

 import express from 'express';  
 import { ApolloServer } from 'apollo-server-express';  
 import typeDefs from './typeDefs';  
 import resolvers from './resolvers';  
 const app = express();  
 const server = new ApolloServer({  
  typeDefs,  
  resolvers,  
  introspection: true,  
 });  
 server.applyMiddleware({app});  
 app.listen(3000, () => {  
   console.log('Server is running on PORT: 3000');  
 })  

  • Và trong file index.js ở thư mục gốc, ta thêm import graphql vừa tạo vào server. Khi đó file index.js như sau:

 import mongoose from 'mongoose';  
 import './graphql';  
 // Connecting to DB  
 mongoose.connect('mongodb://127.0.0.1:27017/demoGraphQL', { useNewUrlParser: true })  
  .then(() => console.log(`Successfully connect to mongodb`))  
  .catch(error => console.log(error));  

Như vậy là ta đã hoàn thành bước connect DB và setup Apollor server. Chạy lệnh yarn start ở terminal và nhập URL này vào thanh địa chỉ trình duyệt: http://localhost:3000/graphql, trong khu bên trái, thực hiện query authors,  sẽ được kết quả như hình dưới.

3. Query

Phần tiếp theo chúng ta sẽ đi đến hiện thực các query. Nếu các bạn chưa hiểu query là gì có thể đọc thêm tại trang chủ của graphql hoặc mình cũng có bài viết về query, mutation tại đây.
Trong ứng dụng này, chúng ta sẽ hiện thực 2 query đơn giản như mọi ví dụ về CRUD khác, đó là trả về danh sách các author và trả về thông tin của 1 author cụ thể nào đó ứng với 2 query là authorsauthor(id). Cùng nhìn lại file typeDefs.js của chúng ta ở trên. Trong đó mình đã định nghĩa 1 type Author với các field ứng với model Author của chúng ta ở DB. Bên cạnh đó mình cũng đã định nghĩa sẵn query authors, trả về kết quả là một mảng các Author
  • Tiếp theo chúng ta sẽ định nghĩa query author(id), nhận input là id của 1 author và trả về 1 đối tượng với type Author. Ta sửa file typeDefs.js như sau:
 import { gql } from 'apollo-server';   
  const typeDefs = gql`   
  type Author {   
   id: ID!,   
   name: String!,   
   yearOfBirth: Int!,   
   gender: Boolean,   
  }   
  type Query {   
   authors: [Author]   
   author(id: String!): Author  
 }   
 `;   
  export default typeDefs;   

  • Sau khi định nghĩa query xong, chúng ta sẽ hiện thực để query trả về kết quả mong muốn bằng cách thêm code logic ở resolvers.js
 import Author from '../models/Author';  
 import mongoose from 'mongoose';  
 const { ObjectId } = mongoose.Types;  
 const resolvers = {   
  Query: {   
   authors: () => {   
   try {  
     const authors = await Author.find();  
     return authors;  
    } catch (err) {  
     console.log('ERROR-authors');  
     console.log(err.message);  
     throw new Error(err.message);  
    }  
   },  
   author: async (root, {id}) => {  
    try {  
     const author = await Author.findOne({_id: ObjectId(id)});  
     return author;  
    } catch (err) {  
     console.log('ERROR-author');  
     console.log(err.message);  
     throw new Error(err.message);  
    }  
   }  
  }   
 }   


  • Giải thích: 
    • Chúng ta sẽ import model Author Schema để thực hiện các thao tác với DB
    • Import hàm ObjectId của mongoose để convert String ID thành dạng ObjectId của MongoDB
    • Đối với query authors, ta sẽ trả về danh sách tất cả các author hiện có trong DB, vì vậy ta sử dụng hàm find() không có tham số để truy vấn trong DB
    • Đối với query author, ta sẽ trả về 1 author có ID được nhập từ input, ta sử dụng hàm findOne với tham số là id của author.
Lúc này quay trở lại GraphQL Playground (http://localhost:3000/graphql), ta chạy lại query authors, sẽ vẫn thấy kết quả là mảng rỗng vì hiện tại DB chưa có dữ liệu gì. Hãy thử insert 1 document author với các trường như trong định nghĩa, sau đó chạy lại query authors ta sẽ thấy kết quả là 1 mảng với 1 phần tử là document vừa tạo.
Kết quả query authors

Để kiểm tra query author, ta dùng chính ID của document vừa tạo để nhập vào cho query, khi đó kết quả trả về sẽ là chính document đó.
Kết quả query author(id)

4. Mutation

Nếu Query ứng với thao tác đọc dữ liệu R (ead) thì Mutation sẽ ứng với các thao tác thay đổi dữ liệu. Chúng ta sẽ hiện thực phần C(reate) - U(pdate) - D(elete) ứng với 3 mutation:

  • createAuthor
  • updateAuthor
  • deletedAuthor

Tạo 1 author mới (createAuthor)

  • Trong file typeDefs.js, thêm đoạn code sau để định nghĩa mutation
...  
 type Mutation {  
   createAuthor(  
    name: String!,  
    yearOfBirth: Int!,  
    gender: Boolean,  
   ): Author  
 }  
 ...  


  • Trong file resolvers.js, thêm hàm createAuthor vào Mutation để hiện thực logic cho mutation vừa định nghĩa ở typeDefs.js

...  
 Mutation: {  
   createAuthor: async (root, { name, yearOfBirth, gender = true }) => {  
    try {  
     const newAuthor = await Author.create({  
      name,  
      yearOfBirth,  
      gender  
     });  
     return newAuthor;  
    } catch (err) {  
     console.log('ERROR-createAuthor');  
     console.log(err.message);  
     throw new Error(err.message);  
    }  
   },  
 }  
 ...  

  • Như ta định nghĩa, createAuthor mutation sẽ nhận input là các thông tin của author gồm (name, yearOfBirth và gender) và kết quả trả về là 1 type Author. Do đó ta dùng hàm Author.create() để thêm 1 author vào DB và return author vừa được tạo.
Create Author mutation



Update author:

  • Định nghĩa updateAuthor mutation trong typeDefs
 ...  
 type Mutation {  
   ...  
   updateAuthor(  
    id: String!  
    newName: String!  
    newYearOfBirth: Int!  
    newGender: Boolean  
   ): Author  
 }  
 ...  

  • Thêm logic trong resolvers

...  
   updateAuthor: async (root, {id, newName, newYearOfBirth, newGender }) => {  
    try {  
     const author = Author.findById(id);  
     if (!author) {  
      throw new Error('Author not found');  
     }  
     const updatedAuthor = await Author.findOneAndUpdate({_id:id}, {  
      name: newName,  
      yearOfBirth: newYearOfBirth,  
      gender: newGender,  
     });  
     return {  
      id,  
      name: newName,  
      yearOfBirth: newYearOfBirth,  
      gender: newGender,  
     };  
    } catch (err) {  
     console.log('ERROR-updateAuthor');  
     console.log(err.message);  
     throw new Error(err.message);  
    }  
   },  
 ...  


  • Kết quả:
Update author mutation

Xoá 1 author trong cơ sở dữ liệu

Tương tự như trên ta lần lượt chỉnh sửa các file typeDefs.js và resolvers.js

// IN typeDefs.js  
 ...  
   deleteAuthor(  
    id: String!  
   ): String  
 ...  

// IN resolvers.js  
 ...  
   deleteAuthor: async (root, { id }) => {  
    try {  
     const author = Author.findById(id);  
     if (!author) {  
      throw new Error('Author not found');  
     }  
     await Author.findOneAndRemove({_id: id});  
     return id;  
    } catch (err) {  
     console.log('ERROR-updateAuthor');  
     console.log(err.message);  
     throw new Error(err.message);  
    }  
   }  
 ...  

Kết quả:
Delete author mutation

4. Subscription

Đây là phần khiến mình tốn nhiều thời gian nhất khi lần đầu tìm hiểu và apply vào dự án đang làm.
Subscription ở đây được hiện thực dưới dạng một listener lắng nghe các trigger và thay đổi dữ liệu theo kết quả trả về từ các trigger đó. Tiếp theo đây chúng ta sẽ tiến hành setup subscription và thấy được nó hoạt động thế nào trên Apollo server

Setup Subscription

Cùng nhìn lại file graphql/index.js, ta thấy hiện tại các query và mutation đang chạy trên một apollo server, để subscription có thể hoạt động, ta cần thêm 1 http server để lắng nghe các thay đổi về dữ liệu.
  • Cài đặt http package để tạo http server
yarn add http  

  • Chúng ta sẽ sử dụng hàm createServer  từ http để tạo server phục vụ cho subscription

 const httpServer = createServer(app);  
 server.installSubscriptionHandlers(httpServer);  

  • Tiếp theo, ta setup 1 biến context trong apollo server. Khi sử dụng subscription, sẽ luôn có 1 connection giữa client và server, connection này có trách nhiệm chuyển những dữ liệu cần thiết và connection này được handle trong biến context
  • Sau khi chỉnh sửa, ta có được file graphql/index.js hoàn chỉnh như sau:

 import express from 'express';  
 import { ApolloServer } from 'apollo-server-express';  
 import { createServer } from 'http';  
 import typeDefs from './typeDefs';  
 import resolvers from './resolvers';  
 const app = express();  
 const server = new ApolloServer({  
  typeDefs,  
  resolvers,  
  context: async ({ req, connection }) => {  
   if (connection) {  
    console.log('subscription working');  
    return ;  
   }  
  },  
  introspection: true,  
 });  
 server.applyMiddleware({app});  
 const httpServer = createServer(app);  
 server.installSubscriptionHandlers(httpServer);  
 httpServer.listen({ port: 3000 }, () => {  
  const url = `http://localhost:3000${server.graphqlPath}`;  
  console.log(`🚀 GraphQL server ready at ${url}`);  
 });  


  • Trong thư mục graphql, tạo file pubsub.js với nội dung như sau:
 import { PubSub } from 'apollo-server';  
 const pubsub = new PubSub();  
 export default pubsub;  

  • Định nghĩa Subscription trong typeDefs. Ở đây subscription sẽ listen thay đổi từ 2 trigger là thêm author mới và cập nhật thông tin từ author
  ...  
   type Query {...}  
   type Mutation {...}  
   type Subscription {  
     authorCreated: Author  
     authorUpdated: Author  
  }  
 ...  

Đến đây chúng ta đã xong phần setup cho subscription, tiếp theo sẽ hiện thực logic cho subscription

Implement Subscription in resolvers


  • Đầu tiên chúng ta sẽ import pubsub module vào file resolvers.js. Sau đó đặt tên cho 2 subscription của chúng ta. 
 // IN resolvers.js  
 import pubsub from './pubsub';  
 const AUTHOR_CREATED = 'AUTHOR_CREATED';  
 const AUTHOR_UPDATED = 'AUTHOR_UPDATED';  


  • Tiếp theo ta thêm 2 subscription vào object Subscription trong resolvers

 ...  
 const resolvers = {  
  Query: {...},  
  Mutation: {...},  
  Subscription: {  
   authorCreated: {  
    subscribe: () => pubsub.asyncIterator(AUTHOR_CREATED),  
   },  
   authorUpdated: {  
    subscribe: () => pubsub.asyncIterator(AUTHOR_UPDATED),  
   }  
  }  
 }  
 ...  


  • Trên đoạn code trên ta thấy rằng, hai subscription của chúng ta sẽ observe những thay đổi được trigger với tên AUTHOR_CREATED  AUTHOR_UPDATED
  • Tiếp theo trong mutation createAuthor và udpateAuthor, ta thêm đoạn code sau:

1:  ...  
2:  const resolvers = {  
3:   ...  
4:   Mutation: {  
5:    createAuthor: async (root, { name, yearOfBirth, gender = true }) => {  
6:     try {  
7:      const newAuthor = await Author.create({  
8:       name,  
9:       yearOfBirth,  
10:       gender  
11:      });  
12:      pubsub.publish(AUTHOR_CREATED,{ authorCreated: newAuthor}); // ADD THIS  
13:      return newAuthor  
14:     } catch (err) {  
15:      console.log('ERROR-createAuthor');  
16:      console.log(err.message);  
17:      throw new Error(err.message);  
18:     }  
19:    },  
20:    updateAuthor: async (root, {id, newName, newYearOfBirth, newGender }) => {  
21:     try {  
22:      const author = Author.findById(id);  
23:      if (!author) {  
24:       throw new Error('Author not found');  
25:      }  
26:      const updatedAuthor = await Author.findOneAndUpdate({_id:id}, {  
27:       name: newName,  
28:       yearOfBirth: newYearOfBirth,  
29:       gender: newGender,  
30:      });  
31:      pubsub.publish(AUTHOR_UPDATED,{ authorUpdated: { // ADD Line 31-36  
32:       id,   
33:       name: newName,   
34:       yearOfBirth: newYearOfBirth,  
35:       gender: newGender,  
36:      } });  
37:      return {  
38:       id,  
39:       name: newName,  
40:       yearOfBirth: newYearOfBirth,  
41:       gender: newGender,  
42:      };  
43:     } catch (err) {  
44:      console.log('ERROR-updateAuthor');  
45:      console.log(err.message);  
46:      throw new Error(err.message);  
47:     }  
48:    },  
49:    ...  
50:   }  
51:  }  
52:  ...  


  • Đối với subscription authorCreated, sẽ được trigger khi một author mới được tạo ra. Khi đó ta sẽ publish ở hàm createAuthor, đồng thời dữ liệu thay đổi (ở đây là author mới) sẽ được thêm gán vào trong subscription để xử lý khi cần. Tương tự với khi subscription authorUpdated.
  • Giờ hãy thử mở playground lên và mở khai báo và chạy subscription authorCreated,  sau đó thử tạo một author mới, ta sẽ được kết quả như sau:
Tạo 1 author mới Mario Puzo

Kết quả trên subscription authorCreated
  • Các bạn có thể thử với subscription authorUpdated.

Kết

Đây là một ví dụ rất đơn giản để giới thiệu cho các bạn về cách 1 GraphQL server được hiện thực và chạy như thế nào. Source code để các bạn có thể đọc kĩ hơn tại github của mình ở đây.
Trong bài viết tiếp theo mình sẽ viết tiếp một ứng dụng sử dụng Apollo client để connect với GraphQL server này.
Nếu bạn có bất kì ý kiến đóng góp hay thắc mắc gì thì hãy comment phía dưới để mình cùng thảo luận hoặc cải thiện chất lượng cho các bài viết sau.
Nếu thấy hay và hữu ích, hãy chia sẻ cho mọi người biết và để mình có động lực viết thêm nhiều bài viết chất lượng hơn nữa nhé. Thanks.

Reference

Advertisement
COMMENTS ()