12/08/2018, 16:47

GraphQL vs. REST - A GraphQL Tutorial

Có thể bạn đã từng nghe nói về GraphQL, nếu chưa thì GraphQL là một cách mới để lấy các API, một sự thay thế cho REST (RESTful APIs). Nó bắt đầu như là một dự án nội bộ tại Facebook, và kể từ khi nó là mã nguồn mở nó đã thu hút được rất nhiều sự quan tâm. Mục đích của bài viết này là giúp bạn có ...

Có thể bạn đã từng nghe nói về GraphQL, nếu chưa thì GraphQL là một cách mới để lấy các API, một sự thay thế cho REST (RESTful APIs). Nó bắt đầu như là một dự án nội bộ tại Facebook, và kể từ khi nó là mã nguồn mở nó đã thu hút được rất nhiều sự quan tâm. Mục đích của bài viết này là giúp bạn có thể thực hiện chuyển đổi dễ dàng từ REST sang GraphQL, cho dù bạn đã sẵn sàng với GraphQL hoặc bạn chỉ muốn thử. Không cần có kiến thức trước về GraphQL, nhưng bạn cần biết về API REST để có thể hiểu được bài viết này. Phần đầu tiên của bài viết tôi sẽ bắt đầu bằng cách đưa ra ba lý do khiến tôi nghĩ rằng GraphQL tốt hơn REST. Phần thứ hai là một hướng dẫn làm thế nào để thêm một GraphQL endpoint ở code back-end của bạn.

Graphql vs. REST: Why Drop REST?

Reason 1: Network Performance

Giả sử bạn có dữ liệu người dùng ở back-end với tên, họ, email và 10 trường khác. Ở Client, bạn chỉ cần một vài trường trong số đó. Thực hiện REST call /users nó trả về cho bạn tất cả các trường của user, và client chỉ sử dụng những gì nó cần. Rõ ràng có một số dữ liệu bị thừa, và đặc biệt khi client là ở dạng mobile. GraphQL mặc định fetch dữ liệu nhỏ nhất có thể. Nếu bạn chỉ cần tên và họ của user, bạn chỉ cần chỉ định nó trong truy vấn. Interface dưới đây được gọi là GraphiQL, giống như một API explorer cho GraphQL. Tôi tạo ra một dự án nhỏ để sử dụng trong bài viết này. Code được lưu trên GitHub, và chúng ta sẽ xem vào nó trong phần thứ hai. Ở đây, chúng ta đang fetch tất cả user - chúng ta sẽ GET /users với REST- và chỉ lấy họ và tên.

Query:

query {
    users {
        firstname
        lastname
    }
}

Result:

{
  "data": {
    "users": [
      {
        "firstname": "John",
        "lastname": "Doe"
      },
      {
        "firstname": "Alicia",
        "lastname": "Smith"
      }
    ]
  }
}

Nếu chúng ta muốn lấy thêm email, thì thêm "email" bên dưới "lastname". Một số back-end REST cung cấp tùy chọn như /users? fields = firstname, lastname để trả về các partial resources ( Google khuyến cáo). Tuy nhiên, nó không được implement theo mặc định, và nó làm cho yêu cầu khó đọc hơn, đặc biệt là khi bạn ném vào các query parameters khác:

&status=active để filter các active user &sort=createdAat để sắp xếp user dựa theo thời gian tạo &sortDirection=desc vì rõ ràng là bạn cần nó &include=projects để include các project của user

Các query parameters này là các bản vá được thêm vào REST API để bắt chước một ngôn ngữ truy vấn. GraphQL hơn hết tất cả các ngôn ngữ truy vấn, nó làm cho request ngắn gọn và chính xác ngay từ đầu.

Reason 2: The “Include vs. Endpoint” Design Choice

Hãy tưởng tượng chúng ta muốn xây dựng một công cụ quản lý dự án đơn giản. Chúng ta có ba mục: users, projects và các tasks. Chúng ta cũng xác định các mối quan hệ sau giữa các mục:

Đây là một số endpoints mà chúng ta expose:

Endpoint 1 Description 2
GET /users list ra tất cả user
GET /users/:id show user dựa vào id
GET/users/:id/projects list ra tất cả project của một user

Các endpoint đơn giản, dễ đọc và có tổ chức tốt.

Mọi thứ trở nên phức tạp hơn khi request của chúng ta trở nên phức tạp. Hãy lấy endpoint của GET / users /: id / projects: Tôi muốn chỉ hiển thị các tiêu đề của project trên trang chủ, nhưng các projects + task lại trên dashboard, trong khi đó không muốn thực hiện nhiều lần gọi REST-api. Tôi sẽ gọi:

GET /users/:id/projects cho trang home GET /users/:id/projects?include=tasks (ví dụ) ở trang dashboard phía back-end trả về sẽ bao gồm tất cả các task có liên quan.

Đó là thực tế phổ biến khi thêm các query paramater ?include=... để làm việc này, và thậm chí là nó được đề xuất bởi JSON API. Các query paramater như ?include=tasks vẫn có thể đọc được, nhưng trước kia chúng kết thúc với ?include = tasks, tasks.owner, tasks.comments, tasks.comments.author. Trong trường hợp này, sẽ là khôn ngoan hơn khi tạo ra một /project endpoint để làm việc này? Một kiểu giống như / projects? userId =: id & include = tasks, hoặc là / tasks? userId =: id. Đây có thể là một sự lựa chọn thiết kế khó khăn, thậm chí còn phức tạp hơn nếu chúng ta có các mối quan hệ nhiều - nhiều.

GraphQL sử dụng include approach ở khắp mọi nơi. Điều này làm cho cú pháp để fetch các mối quan hệ trở nên mạnh mẽ và nhất quán.

Đây là một ví dụ về fetch cả các project và task từ user có id 1.

Query:

{
  user(id: 1) {
    projects {
      name
      tasks {
        description
      }
    }
  }
}

Result:

{
  "data": {
    "user": {
      "projects": [
        {
          "name": "Migrate from REST to GraphQL",
          "tasks": [
            {
              "description": "Read tutorial"
            },
            {
              "description": "Start coding"
            }
          ]
        },
        {
          "name": "Create a blog",
          "tasks": [
            {
              "description": "Write draft of article"
            },
            {
              "description": "Set up blog platform"
            }
          ]
        }
      ]
    }
  }
}

Như bạn thấy, cú pháp truy vấn dễ đọc. Nếu chúng ta muốn đi sâu hơn và bao gồm các task, comments, hình ảnh và tác giả, chúng ta sẽ không nghĩ hai lần về cách tổ chức API của chúng ta. GraphQL giúp bạn dễ dàng tìm ra các object phức tạp.

Reason 3: Managing Different Types of Clients

Khi xây dựng một back-end, chúng ta luôn luôn bắt đầu bằng cách cố gắng làm cho API được sử dụng rộng rãi bởi càng nhiều clients càng tốt. Tuy nhiên, clients luôn muốn call ít hơn và fetch được nhiều dữ liệu hơn. Với deep includes, partial resources và filter, các request được tạo bởi web và mobile có thể khác nhau rất nhiều từ các các ứng dụng khác nhau.

Với REST, có một vài giải pháp. Chúng ta có thể tạo một endpoint tùy chỉnh (ví dụ: một alias endpoin kiểu /mobile_user), một đại diện là (Content-Type: application / vnd.rest-app-example.com + v1 + mobile + json) hoặc thậm chí một client-specific API (như Netflix đã từng làm). Cả ba trong số chúng đều đòi hỏi phải có thêm effort từ team back-end.

GraphQL mang lại nhiều hơn cho client. Nếu client cần những request phức tạp, nó sẽ tự xây dựng các truy vấn tương ứng. Mỗi client có thể sử dụng cùng một API khác nhau.

How to Start with GraphQL

Trong hầu hết các cuộc tranh luận về "GraphQL vs. REST" ngày nay, mọi người nghĩ rằng họ phải chọn một trong hai. Đơn giản điều đó là không đúng.

Các ứng dụng hiện đại thường sử dụng một số dịch vụ khác nhau, để lộ một số API. Chúng ta thực sự có thể nghĩ đến GraphQL như là một gateway hoặc một wrapper cho tất cả các dịch vụ này. Tất cả các client sẽ truy cập endpoint của GraphQL, và endpoint này sẽ truy cập vào lớp cơ sở dữ liệu, giống như một dịch vụ như kiểu ElasticSearch hoặc Sendgrid, hoặc các endpoint REST khác.

Cách thứ hai của việc sử dụng cả hai là có endpoint riêng /graphql trên REST API của bạn. Điều này đặc biệt hữu ích nếu bạn đã có nhiều client đang sử dụng REST API nhưng bạn muốn thử GraphQL mà không ảnh hưởng đến cấu trúc hiện tại. Và đây là giải pháp mà chúng ta đang khám phá ngày nay.

Như đã nói ở trên, tôi sẽ minh họa tutorial này bằng một ví dụ nhỏ, có sẵn trên GitHub. Nó là một công cụ quản lý dự án đơn giản với users, projects, và tasks.

Các công nghệ được sử dụng cho project này là Node.js và Express cho web server, SQLite làm hệ quản trị cơ sở dữ liệu, và Sequelize như một ORM. Ba model user, project và task được định nghĩa trong thư mục model. Các endpoint REST /api/users, /api/projects và /api/tasks được định nghĩa trong thư mục rest.

Lưu ý rằng GraphQL có thể được cài đặt trên bất kỳ kiểu back-end và cơ sở dữ liệu nào, sử dụng bất kỳ ngôn ngữ lập trình. Các công nghệ được sử dụng ở đây được chọn vì tính đơn giản và dễ đọc.

Mục tiêu của chúng ta là tạo một endpoint /graphql mà không xoá bỏ các REST endpoint. GraphQL endpoint sẽ truy cập trực tiếp ORM để lấy dữ liệu, để nó hoàn toàn độc lập với REST logic.

Types

Model được trình bày trong GraphQL theo dạng type. Nên sẽ có một ánh xạ 1-1 giữa các model của bạn và các GraphQL type. User type chúng ta sẽ có dạng:

type User {
  id: ID! # The "!" means required
  firstname: String
  lastname: String
  email: String
  projects: [Project] # Project is another GraphQL type
}

Queries

Query xác định những truy vấn bạn có thể chạy trên GraphQL API của bạn. Theo quy ước, cần có một RootQuery, chứa tất cả các truy vấn hiện có. Ta sẽ chỉ ra truy vấn trong REST tương đương của mỗi query dưới đây:

type RootQuery {
  user(id: ID): User           # Tương ứng với GET /api/users/:id
  users: [User]                # Tương ứng với GET /api/users
  project(id: ID!): Project    # Tương ứng với GET /api/projects/:id
  projects: [Project]          # Tương ứng với GET /api/projects
  task(id: ID!): Task          # Tương ứng với GET /api/tasks/:id
  tasks: [Task]                # Tương ứng với GET /api/tasks
}

Mutations

Nếu truy vấn là request GET, mutations có thể được xem như POST / PATCH / PUT / DELETE request (mặc dù thực sự chúng là các phiên bản đồng bộ của các query). Theo quy ước, chúng ta đưa tất cả các mutations của chúng ta vào RootMutation:

type RootMutation {
  createUser(input: UserInput!): User             # Tương ứng với POST /api/users
  updateUser(id: ID!, input: UserInput!): User    # Tương ứng với PATCH /api/users
  removeUser(id: ID!): User                       # Tương ứng với DELETE /api/users

  createProject(input: ProjectInput!): Project
  updateProject(id: ID!, input: ProjectInput!): Project
  removeProject(id: ID!): Project
  
  createTask(input: TaskInput!): Task
  updateTask(id: ID!, input: TaskInput!): Task
  removeTask(id: ID!): Task
}

Lưu ý rằng chúng ta giới thiệu các type ở đây, được gọi là UserInput, ProjectInput, và TaskInput. Đây cũng là một điều phổ với REST, để tạo ra một input data để create và update resource. Ở đây, UserInput type của chúng ta là User type mà không có các trường id và projects, và từ khoá input thay thế cho type:

input UserInput {
  firstname: String
  lastname: String
  email: String
}

Schema

Với các type, query và mutation, chúng ta định nghĩa GraphQL schema, đó là endpoint của GraphQL exposes:

schema {
  query: RootQuery
  mutation: RootMutation
}

Ví dụ: GraphiQL.

Resolvers

Bây giờ chúng ta có public schema, đã đến lúc phải nói cho GraphQL biết phải làm gì khi mỗi queries/mutations được request. Resolvers có thể làm những việc khó khăn; ví dụ:

  • Truy cập một internal REST endpoint
  • Gọi một microservice
  • Truy cập database để CRUD operations

Chúng ta đang chọn tùy chọn thứ ba trong app ví dụ. Chúng ta hãy xem các file esolvers:

const models = sequelize.models;

RootQuery: {
  user (root, { id }, context) {
    return models.User.findById(id, context);
  },
  users (root, args, context) {
    return models.User.findAll({}, context);
  },
  // Resolvers for Project and Task go here
},
    
/* For reminder, our RootQuery type was:
type RootQuery {
  user(id: ID): User
  users: [User]
 
  # Other queries
}

Điều này có nghĩa là, nếu truy vấn user (id: ID!)được request trên GraphQL, thì chúng ta sẽ trả về User.findById (), là một Sequelize ORM function , từ database.

Còn về việc join các model khác trong request? Vâng, chúng ta cần phải định nghĩa nhiều resolvers hơn:

User: {
  projects (user) {
    return user.getProjects(); // getProjects is a function managed by Sequelize ORM
  }
},
    
/* For reminder, our User type was:
type User {
  projects: [Project] # We defined a resolver above for this field
  # ...other fields
}
*/

Vì vậy, khi chúng ta request projects field trong một User type ở GraphQL, join này sẽ được append vào database query.

Cuối cùng, resolvers cho mutation:

RootMutation: {
  createUser (root, { input }, context) {
    return models.User.create(input, context);    
  },
  updateUser (root, { id, input }, context) {
    return models.User.update(input, { ...context, where: { id } });
  },
  removeUser (root, { id }, context) {
    return models.User.destroy(input, { ...context, where: { id } });
  },
  // ... Resolvers for Project and Task go here
}

Để giữ data trên server clean, ta disable các resolver cho các mutation, có nghĩa là các mutation sẽ không create, update hoặc delete các operations trong cơ sở dữ liệu (và do đó sẽ trả về null). Query

query getUserWithProjects {
  user(id: 2) {
    firstname
    lastname
    projects {
      name
      tasks {
        description
      }
    }
  }
}

mutation createProject {
  createProject(input: {name: "New Project", UserId: 2}) {
    id
    name
  }
}

Result

{
  "data": {
    "user": {
      "firstname": "Alicia",
      "lastname": "Smith",
      "projects": [
        {
          "name": "Email Marketing Campaign",
          "tasks": [
            {
              "description": "Get list of users"
            },
            {
              "description": "Write email template"
            }
          ]
        },
        {
          "name": "Hire new developer",
          "tasks": [
            {
              "description": "Find candidates"
            },
            {
              "description": "Prepare interview"
            }
          ]
        }
      ]
    }
  }
}

Có thể mất một chút thời gian để rewrite tất cả các type, query và resolver cho ứng dụng hiện tại của bạn. Tuy nhiên, có rất nhiều công cụ có thể giúp bạn. Ví dụ, có những công cụ translate một SQL schema sang một GraphQL schema, bao gồm cả các resolvers!

Putting Everything Together

Với schema và các resolver được định nghĩa tốt những gì cần làm đối với từng truy vấn của schema, chúng ta có thể gắn một endpoint /graphql trên back-end của chúng ta:

// Mount GraphQL on /graphql
const schema = makeExecutableSchema({
  typeDefs, // Our RootQuery and RootMutation schema
  resolvers: resolvers() // Our resolvers
});
app.use('/graphql', graphqlExpress({ schema }));

Và chúng ta có một "nice looking" GraphiQL interface ở back-end. Để tạo request không có GraphiQL, chỉ cần sao chép URL request chạy nó với cURL, AJAX hoặc trực tiếp trên trình duyệt. Tất nhiên, có một số client GraphQL để giúp bạn build các query này.

What’s Next?

Mục đích của bài viết này là cung cấp cho bạn một sự thú vị của những gì trông giống với GraphQL và cho bạn thấy rằng vẫn có thể thử GraphQL mà không cần loại bỏ cấu trúc REST của bạn. Cách tốt nhất để biết GraphQL có phù hợp với nhu cầu của bạn hay không là hãy thử nó. Tôi hy vọng rằng bài viết này sẽ giúp bạn hiểu sâu hơn. Có rất nhiều tính năng chúng ta đã không đưa ra trong bài viết này, chẳng hạn như update real-time, server-side batching, authentication, authorization, client-side caching, file upload, vv. Một nguồn tuyệt vời để tìm hiểu về các tính năng này đó là How to GraphQL.

Dưới đây là một số nguồn hữu ích khác:

Server-side Tool Description
graphql-js Implement reference của GraphQL. Bạn có thể sử dụng nó với express-graphql để tạo một server.
graphql-server Một máy chủ GraphQL all-in-one được tạo ra bởi Apollo team.
Client-side Tool Description
Relay Một framework để kết nối React và GraphQL
apollo-client Một GraphQL client binding cho React, Angular 2 và các front-end framework khác

Kết luận, GraphQL sẽ không thay thế REST ngay lúc này, nhưng nó cung cấp giải pháp cho một vấn đề thực sự. Nó tương đối mới, và các phương pháp hay nhất vẫn đang phát triển, nhưng chắc chắn đó là một công nghệ mà chúng ta sẽ nghe về trong vài năm tới.

UNDERSTANDING THE BASICS

GraphQL là gì ?

GraphQL là ngôn ngữ truy vấn và là một thay thế cho REST. Nó bắt đầu như là một internal project tại Facebook.

Sự khác biệt giữa GraphQL và REST ?

GraphQL merge tất cả các RESTful endpoint của bạn thành một và sử dụng băng thông ít hơn.

References

https://www.toptal.com/api-development/graphql-vs-rest-tutorial

0