Bài này mình nói GraphQL dưới 1 góc nhìn đơn giản nhất cho những bạn mới tiếp cận để lấy cảm hứng viết code với GraphQL

Simple GraphQL API Server với NodeJS và Express

2022-03-11 820 lượt xem

GraphQL là gì

GraphQL là một ngôn ngữ thao tác và truy vấn dữ liệu mã nguồn mở cho các API và một thời gian chạy để thực hiện các truy vấn với dữ liệu hiện có. GraphQL được Facebook phát triển nội bộ vào năm 2012 trước khi phát hành công khai vào năm 2015.

Ngắn gọn thì graphQL đang là ngôn ngữ truy vấn mà ở đó ng ta muốn nó có khả năng thay thế RestApi trong tương lai. 

Lý do là rest Api đang bộc lộ vài hạn chế đang có về việc trả ra resource cố định không mang tính dynamic gây dư thừa.

Setting up GraphQL and Express

Mình dùng express khá quen thuộc nên khi sử dụng graphQL cugnx thấy nó khá nuột. Bạn code node chắc không xa lạ gì với express 😄 vậy để cài nhanh express và graphQL thì dùng như sau: app nodejs đầu tiên sử dụng Express application generator

# thêm cơ sở dữ liệu mình chọn là mongo
npm install mongoose
# cài đặt graphql và express-graphql
npm install graphql apollo-server-express
# viết api mà để kết nối không bị chặn phải dùng cors
npm i cors

Và cuối cùng chúng ta có file của package.json như sau: 

{
  "name": "learn",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www",
    "watch": "nodemon ./bin/www",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "apollo-server-express": "^2.19.2",
    "cookie-parser": "~1.4.4",
    "cors": "^2.8.5",
    "debug": "~2.6.9",
    "ejs": "~2.6.1",
    "express": "~4.16.1",
    "graphql": "^15.5.0",
    "http-errors": "~1.6.3",
    "mongoose": "^5.11.15",
    "morgan": "~1.9.1",
    "nodemon": "^2.0.15"
  }
}

Code thôi ! :D

Tạo server graphQL đơn giản nhất có thể

GraphQL được cấu thành bởi

  • middleware đính kèm trong app express ( lát sẽ sửa file app.js )
  • chema là cấu trúc bạn muốn mô tả các thực thể cho graphQL
  • resolvers là định nghĩa những function mô tả cách trả về dữ liệu tương ứng cho client

1. tạo schema

tạo file : /schema/schema.js với nội dung: 

const { gql } = require('apollo-server-express')

const schema = gql`
    type Book {
        id: ID
        name: String
    }
    # ROOT TYPE
    type Query {
        books: [Book]
        book(id: ID!): Book
    }
`
module.exports = schema

Chỗ type Book là định nghĩa bạn sẽ có 1 kiểu dữ liệu trả về là Book có 2 thuộc tính là (id, name).

Chỗ ROOT TYPE đang định nghĩa type Query giống như 1 select trong sql vậy. Nó sẽ có khả năng để client query data và tạm hiểu là 1 Query data. Trong Query thì có khai báo 2 function: books kiểu trả về là danh sách Book ( được định nghĩa ở trên) và 1 function trả về book theo id

2 . tạo fileresolvers

Tạo file /resolver/resolver.js với nội dung:

const resolvers = {
    // QUERY
    Query: {
        books: async () => Promise.resolve(
            [
                { id: 3123213, name: "sách học tán gái" },
                { id: 343, name: "lập trình chứ không hề cô đơn" },
            ]
        ),
        book: async () => Promise.resolve({ id: 343, name: "lập trình chứ không hề cô đơn" })
    }
}

module.exports = resolvers

Giải thích: Trong đây chúng ta đang hardcode trả về dữ liệu cho 2 function books và book id trong schema.js định nghĩa trước đó. nghĩa là file resolvers.js là file mô tả cụ thể hành vi. File schema mô tả cấu trúc dữ liệu.

3. Chỉnh file app gắn middleware

vào file app.js thêm đoạn code sau vào bên dưới dòng var app = express();

const { ApolloServer } = require('apollo-server-express')
// Load schema & resolvers
const typeDefs = require('./schema/schema')
const resolvers = require('./resolver/resolver')
const server = new ApolloServer({
  typeDefs,
  resolvers,
})
server.applyMiddleware({ app })
console.log(`path graphQL la: ${server.graphqlPath}`)

ảnh minh hoạ:

Sau khi bạn thêm xong là thành công rồi. Chỉ còn thực thi code nodejs và test truy vấn với prlayGround cảu graphQL nữa thôi

Run code node và query thử

# chạy node với lệnh 
node ./bin/www

Nếu không vấn đề thì bạn có thể chạy http://localhost:3000/graphql để chạy playground: 

bạn gõ vào trong đó query thử như sau: 

query hungDepTrai{
  books {
      id,
      name
    }
}

Kết quả:

Sau thực thi xong bạn sẽ thấy kết quả là các hardcode của bạn. Và câu chuyện bạn cần có kết nối vào db để lấy ra data thật. Và mình sẽ kết nối mongodb bằng mongoose. Nếu bạn nào chưa làm việc với mongoose thì xem qua bài viết này của mình: Kết nối đến MongoDB với Nodejs dùng mongoose và dùng schema để làm việc với mongoose trong nodejs mongodb

Mình tạm hiểu là bạn flow theo 2 bài viết của mình trên kia và biết cách connect đến mongodb rồi thì tiếp theo chúng ta tạo 1 schema cho hệ thống là: 

// File models/book.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const BookSchema = new Schema({
    name: {
        type: String
    },
    description: {
        type: String
    },
    authorId: {
        type: String
    }
})
module.exports = mongoose.model('books', BookSchema)

và file Author: 

// File models/author.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const AuthorSchema = new Schema({
    name: {
        type: String
    },
    age: {
        type: Number
    }
})
module.exports = mongoose.model('authors', AuthorSchema)

Sửa lại resolvers là 1 function lấy data thực từ db ra như sau: 

const Book = require('../models/Book')
const resolvers = {
    // QUERY
    Query: {
        books: async (parent, args, context) => await Book.find({}),
        book: async (parent, args, context) => {
            const { id } = args // id truyền vào từ câu query theo định nghĩa của file /schema/schema.js: book(id: ID!): Book
            return await Book.findById(id)
        },
    }
}
module.exports = resolvers

Vào query thử thì thấy data có gì đâu 😄 Kết quả nè :

Ừ thì mình đã làm CUD đâu nè. Muốn có data phải qua phần 2 để làm việc với mutation nha. 

II. Phần 2 -Mutation và tích hợp thêm mongoose

Với mỗi ứng dụng thì điều cơ bản phải có là CRUD, ở trên chúng ta đã sử dụng Query để R rồi, vậy CUD sẽ dùng gì? Với GraphQL, những điều này được thực hiện bằng cách sử dụng Mutations.

Để tạo 1 cái gì trong GraphQL bạn cứ nhớ phải định nghĩa type input và output trong file schema rồi mới định nghĩa thực thi cụ thể trong resolvers như sau: 

const { gql } = require('apollo-server-express')

const schema = gql`
    type Book {
        id: ID
        name: String
    }
    # ROOT TYPE
    type Query {
        books: [Book]
        book(id: ID!): Book
    }

    type Mutation {
        createBook(name: String, description: String, authorId: ID!): Book
    }
`

module.exports = schema

Bạn thấy cái định nghĩa mới là createBook có các args gồm (name, description, authorId ) và kết quả trả ra là 1 Kiểu dữ liệu Book ( được định nghĩa ở trên rồi nè)

Cập nhật thêm file resolvers cho hàm mới là createBook như sau: 

const Book = require('../models/Book')

const resolvers = {
    // QUERY
    Query: {
        books: async (parent, args, context) => await Book.find({}),
        book: async (parent, args, context) => {
            const { id } = args // id truyền vào từ câu query theo định nghĩa của file /schema/schema.js: book(id: ID!): Book
            return await Book.findById(id)
        },
    },
    // MUTATION để CUD data
    Mutation: {
        createBook: async (parent, args, context) => {
            /// bạn thêm logic lưu data từ trong args định nghĩa trong schema như sau
            const { name, description, authorId } = args
            return await (new Book({ name, description, authorId })).save()
        }
    }
}

module.exports = resolvers

Vào background query thử thì thấy được vầy nè: 

III. Phần 3 - Cập nhật đầy đủ cho book và author

edit lại code cho schema

const { gql } = require('apollo-server-express')

const schema = gql`
    type Book {
        id: ID
        name: String,
        description: String,
        author: Author
    }
    type Author {
        id: ID!
        name: String
        age: Int
        books: [Book]
    }

    # ROOT TYPE
    type Query {
        books: [Book]
        book(id: ID!): Book
        authors: [Author]
        author(id: ID!): Author
    }

    type Mutation {
        createBook(name: String, description: String, authorId: ID!): Book,
        createAuthor(name: String, age: Int): Author
    }
`
module.exports = schema

Cập nhật định nghĩa file resolvers như sau:

const Author = require('../models/Author')
const Book = require('../models/Book')

const resolvers = {
    // QUERY
    Query: {
        books: async (parent, args, context) => await Book.find({}),
        book: async (parent, args, context) => {
            const { id } = args // id truyền vào từ câu query theo định nghĩa của file /schema/schema.js: book(id: ID!): Book
            return await Book.findById(id)
        },
        authors: async (parent, args, context) => await Author.find({}),
        author: async (parent, args, context) => {
            const { id } = args // id truyền vào từ câu query theo định nghĩa của file /schema/schema.js: book(id: ID!): Book
            return await Author.findById(id)
        },
    },
    // MUTATION để CUD data
    Mutation: {
        createBook: async (parent, args, context) => {
            /// bạn thêm logic lưu data từ trong args định nghĩa trong schema như sau
            const { name, description, authorId } = args
            return await (new Book({ name, description, authorId })).save()
        },
        createAuthor: async (parent, args, context) => {
            /// bạn thêm logic lưu data từ trong args định nghĩa trong schema như sau
            const { name, age } = args
            return await (new Author({ name, age })).save()
        }
    }
}
module.exports = resolvers

Tới đây là bạn có thể tạo tác giả sau đó lấy danh sách tác giả như sau: 

À có vẻ work hết rồi. ơ mà trong type Author gồm: { id, name, age, books }. Nhưng khi query books thì lỗi banh nóc nhà luôn 😄 

Lý do bạn chưa ref cho Author cách lấy books như nào nên nó ngu không biết lấy và kết quả là văng lỗi. Bạn cần phải định nghĩa thêm cho resolvers như sau: 

const Author = require('../models/Author')
const Book = require('../models/Book')

const resolvers = {
    // QUERY
    Query: {
        books: async (parent, args, context) => await Book.find({}),
        book: async (parent, args, context) => {
            const { id } = args // id truyền vào từ câu query theo định nghĩa của file /schema/schema.js: book(id: ID!): Book
            return await Book.findById(id)
        },
        authors: async (parent, args, context) => await Author.find({}),
        author: async (parent, args, context) => {
            const { id } = args // id truyền vào từ câu query theo định nghĩa của file /schema/schema.js: book(id: ID!): Book
            return await Author.findById(id)
        },
    },
    Book: {
        author: async (parent, args, context) => {
            // data object hiện tại là parent tương ứng là 1 book
            return await Author.findById(parent.authorId)
        }
    },
    // chỉ cho resolver khi cần lấy danh sách books từ 1 object là author thì phải làm sao
    Author: {
        books: async (parent, args, context) => {
            // data object hiện tại là parent tương ứng là 1 author
            return await Book.find({ authorId: parent.id }) || []
        }
    },
    // MUTATION để CUD data
    Mutation: {
        createBook: async (parent, args, context) => {
            /// bạn thêm logic lưu data từ trong args định nghĩa trong schema như sau
            const { name, description, authorId } = args
            return await (new Book({ name, description, authorId })).save()
        },
        createAuthor: async (parent, args, context) => {
            /// bạn thêm logic lưu data từ trong args định nghĩa trong schema như sau
            const { name, age } = args
            return await (new Author({ name, age })).save()
        }
    }
}
module.exports = resolvers

Kết quả sau khi bạn lưu tác giả theo sách thì được như vầy:

 

Trong phần 2 mình sẽ hướng dẫn tiếp theo phần: 

 bạn theo dõi bài viết: Cách kết nối client react với graphQL nodejs

Một vấn đề quan trọng đối với các ứng dụng web ngày nay là tương tác thời gian thực. Để đáp ứng cho vấn đề trên thì graphQL cung cấp khái niệm Subscriptions. Khi một client subscribes một sự kiện, nó sẽ khởi tạo và giữ kết nối ổn định với máy chủ. Bất cứ khi nào sự kiện cụ thể đó thực sự xảy ra, máy chủ sẽ đẩy dữ liệu tương ứng đến máy khách.