10 cách để viết Nodejs REST APIs hiệu quả

Chúng ta sẽ cùng tìm hiểu các cách để viết các REST API với nodejs làm sao cho hiệu quả, bao gồm các chủ đề như đặt tên các route, authentication, black-box testing và sử dụng cache header đúng cách các tài nguyên. Hy vọng 10 cách được liệt kê dưới đây có thể giúp ích cho các bạn:

1. Sử dụng phương thức HTTP và các API Route

Giả sử bạn đang xây dựng một RESTful API Node.js để tạo, cập nhật, lấy thông tin hay xóa người dùng. Với các tính năng đó HTTP đã cung cấp một bộ đầy đủ các phương thức: POST, PUT, GET, PATCH hoặc DELETE.

Cách tối ưu nhất là các API route của bạn chỉ nên sử dụng danh từ cho việc xác định tài nguyên. Các route khi đó sẽ trông như thế này:

  • POST /user dùng để tạo user
  • GET /user lấy danh sách users
  • GET /user/:id lấy một user cụ thể bằng id
  • PUT /user:/id thay đổi/cập nhật thông tin user cụ thể thông qua id
  • DELETE /user/:id để xóa một user.

2. Sử dụng HTTP status code chính xác

Nếu có gì xảy ra khi server đang xử lý một request, bạn cần tạo http status code trong response trả về:

  • 2xx, nếu không có lỗi (thường là 200).
  • 3xx, nếu resourse đã chuyển
  • 4xx, nếu request không được thực hiện do lỗi client.
  • 5xx, nếu có lỗi ở phía API server (một exception nào đó xảy ra,...).

Nếu bạn sử dụng Express, thiết lập mã status khá dễ dàng: res.status(500).send({error: 'Internal server error happened'}). Thường thì ta nên tạo một helper để khai báo các định dạng và status code trả về để tiện dùng sau này trong app.

3. Sử dụng các header HTTP để gửi metadata

Để đính kèm các metadata vào payload bạn sắp gửi, sử dụng HTTP header. Các header sẽ bao gồm các thông tin:

  • phân trang
  • giới hạn tần suất
  • hoặc authentication

Nếu bạn cần thiết lập bất cứ metadata custom nào trong header, hãy thêm tiền tố X vào phía trước. Ví dụ, nếu bạn đang sử dụng CSRF token, cách thông thường là X-Csrf-Token. Tuy nhiên, theo RFC 6648 thì sẽ gây khó hiểu. Với các API mới không nên sử dụng các tên header dễ gây conflict với các ứng dụng khác. Ví dụ, OpenStack sẽ tự động thêm tiền tố vào header với OpenStack:

OpenStack-Identity-Account-ID 
OpenStack-Networking-Host-Name 
OpenStack-Object-Storage-Policy

Chú ý rằng các chuẩn HTTP không định nghĩa bất cứ giới hạn size nào trong header. Tuy nhiên Node.js đã buộc object header nhận một giới hạn kích thước là 80kB cho lý do thực tế từ Nodejs HTTP parser.

Don’t allow the total size of the HTTP headers (including the status line) to exceed HTTP_MAX_HEADER_SIZE. This check is here to protect embedders against denial-of-service attacks where the attacker feeds us a never-ending header that the embedder keeps buffering

4: Chọn đúng framework cho REST API Node.js

Việc chọn đúng framework phù hợp với yêu cầu công việc của bạn là quan trọng. Một số framework phổ biến như: Express, Koa hay Hapi Express, Koa hay Hapi có thể được sử dụng để tạo ra các web application, chúng hỗ trợ templating và rendering. Mình thì bắt đầu với Express 😄 Restify Restify tập trung hoàn toàn vào việc giúp bạn xây dựng các REST services. Restify tồn giúp bạn xây dựng các "strict" API services đáng kể, dễ bảo trì. Restify cũng đi kèm với công cụ hỗ trợ tự động DTrace. Cụ thể thì Restify đang được sử dụng trong rất nhiều các ứng dụng như npm hay Netflix.

5. Black-Box Test

Một trong những cách hay để test các REST API là xem chúng như các black-box. Black-box test là phương pháp kiểm thử chức năng của ứng dụng mà không cần quan tâm đến cấu trúc bên trong của nó hay cách nó hoạt động. Do đó, sẽ không cần mock dependency nào, hệ thống sẽ được test một thể. Chúng ta sẽ sử dụng supertest để test các REST API theo phương pháp black-box này. Sau đây là một test case đơn giản. Nó kiểm tra xem thông tin người dùng đã được trả về hay chưa, sử dụng test runner mocha:

const request = require('supertest')
 
describe('GET /user/:id', function() {
  it('returns a user', function() {
    // newer mocha versions accepts promises as well
    return request(app) 
      .get('/user') 
      .set('Accept', 'application/json') 
      .expect(200, {
        id: '1', 
        name: 'John Math' 
      }, done) 
  }) 
})

Có lẽ bạn sẽ thắc mắc: làm thế nào để tạo dữ liệu vào trong database phục vụ cho các REST API? Thông thường, test thường được viết làm sao để chúng tạo ra càng ít các giả định cho trạng thái hệ thống càng tốt. Tuy vậy, trong một vài bối cảnh bạn cần biết chính xác trạng thái của hệ thống, bạn có thể tạo các assertion và đạt được test coverage cao hơn. Tùy thuộc vào nhu cầu của bạn, bạn có thể populate cơ sở dữ liệu với các dữ liệu test theo một trong các cách sau:

  • Chạy black-box scenarios theo một tập con đã biết của dữ liệu production
  • tự populate database với dữ liệu trước khi chạy các test case

Dĩ nhiên, black-box test không có nghĩa là bạn không cần viết unit test. Trong hầu hết các trường hợp, bạn vẫn cần viết unit test cho các API. Cụ thể các bạn có thể tham khảo bài viết trước của mình: Testing API end point in Nodejs

6. JWT, Stateless Authentication

Các API phải là phi trạng thái, và authentication cũng tương tự. JWT (Json Web Token) chính là ý tưởng đó. JWT gồm 3 phần:

  • Header: chứa kiểu của token, thuật toán hash.
  • Payload: Chứa các claims.
  • Signature (JWT không mã hóa payload).

Thêm xác thực loại JWT vào ứng dụng khá đơn giản và nhanh chóng:

import * as jwt from 'jsonwebtoken';

const expiresIn = parseInt('86400');
const token = jwt.sign(user.toObject(), process.env.APP_KEY, {
  expiresIn // in seconds
});

Sau đó, API endpoint đã được bảo vệ với JWT. Để truy cập vào các endpoint được bảo vệ, bạn cần phải cung cấp token trong trường header Authorization:

curl --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" my-website.com

Hãy chú ý một điều rằng module JWT trên không phụ thuộc bất kì database nào, do tất cả token JWT có cơ chế tự xác thực, và chúng còn bao gồm cả thời gian tồn tại. Ngoài ra, bạn phải chắc chắn rằng tất cả các API endpoint trong ứng dụng sẽ chỉ được truy cập thông qua kết nối bảo mật sử dụng HTTPS.

7. Sử dụng các request có điều kiện

Các request có điều kiện là các request HTTP được thực thi theo các cách khác nhau, phụ thuộc vào các header HTTP cụ thể. bạn có thể xem các header này như là điều kiện tiên quyết: nếu chúng gặp nhau, các request sẽ được thực thi theo các cách khác nhau. Các header này sẽ cố gắng kiểm tra liệu version của resource được lưu trữ trên server có trùng với version của cùng resource hay không. Do vậy, các header có thể là:

  • Timestamp của lần thay đổi gần nhất.
  • Một tag nào đó, khác nhau tùy vào phiên bản.

Các header này là:

  • Last-Modified ( chỉ ra lần gần nhất dữ liệu được chỉnh sửa ).
  • Etag ( biểu thị tag ).
  • If-Modified-Since (dùng với header Last-Modified ).
  • If-None-Match (dùng với header Etag ).

Hãy xem ví dụ sau: Ở hình dưới đây, client đã không có bất cứ phiên bản cũ nào của tài nguyên doc. Do đó cả 2 header If-Modified-Since và If-None-Match đều không được sử dụng khi tài nguyên được gửi. Sau đó, server phản hồi với các header Etag and Last-Modified được thiết lập. Client có thể thiết lập header If-Modified-SinceIf-None-Match một lần khi cố gắng gửi request tới cùng resource. Nếu response trả về là giống nhau, server đơn giản là response lại với mã status 304 - Not Modified và không gửi lại resource nữa.

8. Bao quát rate limiting

Rate limiting được sử dụng để kiểm soát việc một consumer có thể gửi đến API bao nhiêu request. Để cho API biết có bao nhiêu request đã được gửi, trong header hãy thiết lập như sau:

  • X-Rate-Limit-Limit, số lượng các request được cho phép trong một khoảng thời gian cho trước.
  • X-Rate-Limit-Remaining, số lượng các request còn lại trong cùng khoảng thời gian.
  • X-Rate-Limit-Rese: thời gian mà rate limit được thiết lập lại.

Phần lớn các framework HTTP hỗ trợ bằng các công cụ bên ngoài (hoặc với plugin). Ví dụ, nếu bạn sử dụng Koa, hãy thử package koa-ratelimit. Chú ý rằng thời gian có thể thay đổi tùy vào các nhà cung cấp API - ví dụ đối với Github là 1 giờ, trong khi Twitter là 15 phút.

9. Tạo các document API đúng chuẩn

Các API được viết ra để mọi người có thể sử dụng chúng. Do đó việc cung cấp các document đi kèm với các REST API là điều cần thiết. Dưới đây là các open-source giúp các bạn trong việc tạo api doc:

10. Đừng quên tương lai của APIs

Trong vài năm vừa qua, hai ngôn ngữ truy vấn lớn cho các API đã nổi lên: GraphQL của Facebook và Falcor của Netflix. Tại sao ta lại cần chúng đến thế?

Tưởng tượng một request tài nguyên RESTful như sau:

/org/1/space/2/docs/1/collaborators?
include=email&page=1&limit=10

Request này có thể trở nên vượt qua ngoài tầm kiểm soát của chúng ta khá dễ dàng - trường hợp bạn muốn lấy cùng định dạng response cho tất cả model vào mọi thời điểm. Lúc này thì GraphQL và Falcor sẽ giúp bạn. GraphQL là một ngôn ngữ truy vấn cho APIs và một runtime để hoàn thành những truy vấn đó. Nó cung cấp mô tả đầy đủ và dễ hiểu về dữ liệu trong API của bạn, đồng thời cho phép clients quyền yêu cầu chính xác những gì họ cần. Falcor là một nền tảng sáng tạo giúp tối ưu hóa sức mạnh cho Netflix UI. Nó cho phép bạn model mọi dữ liệu ở backend như một JSON object ảo trên Node server của bạn. Ở phía client, bạn làm việc với JSON object thông qua một số toán tử JavaScript như get, set và call.

Một số REST API tuyệt vời

Nếu bạn định bắt đầu phát triển một REST API Node.js hoặc tạo một phiên bản mới của API cũ, chúng tôi đã thu thập 4 ví dụ thực tế để giúp bạn:

Reference

https://medium.com/the-node-js-collection/10-best-practices-for-writing-node-js-rest-apis-7643a7765cd