Trong bài viết này, chúng ta sẽ cùng tìm hiểu các cách tốt nhất để viết các REST API Node.js, bao gồm các chủ đề như đặt tên các route, xác thực, test black-box và sử dụng cache header đúng cách các tài nguyên.

Một trong những trường hợp sử dụng thông dụng nhất của Node.js là viết các RESTful API. Khi chúng tôi đang hỗ trợ khách hàng bằng cách tìm các vấn đề trong ứng dụng của họ với Trace (công cụ giám sát Node.js) thì phát hiện ra rằng có rất nhiều các developer đang gặp vấn đề với REST API. Do đó, tôi mong rằng các cách chúng tôi sử dụng với RisingStack sẽ giúp được bạn:

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

Hãy tưởng tượng rằng bạn đang xây dựng một RESTful API Node.js để tạo, cập nhật, gọi 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: POSTPUTGETPATCH và 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ừ như là các định danh tài nguyên. Các route khi đó sẽ trông như thế này:

  • POST /user hay PUT /user:/id để tạo người dùng mới.
  • GET /user để lấy danh sách người dùng.
  • GET /user/:id để lấy thông tin của một người dùng.
  • PATCH /user/:id để sửa một bản ghi người dùng đã có.
  • DELETE /user/:id để xóa một người dùng.

Các API route của bạn chỉ nên sử dụng danh từ như là các định danh tài nguyên.

@RisingStack

#2 - Sử dụng mã status HTTP đúng

Nếu có gì xảy ra khi server đang xử lý một request, bạn cần thiết lập đúng mã status trong response trả về:

  • 2xx, nếu không có lỗi (thành công).
  • 3xx, nếu tài nguyên đã bị gỡ bỏ.
  • 4xx, nếu request không được thực hiện do lỗi client.
  • 5xx, nếu có lỗi ở phía API (một ngoại lệ 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'}). Với Restify: res.status(201).

Bạn có thể xem danh sách đầy đủ các mã status ở đây.

#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 truy cập
  • Xác thực.

Đây là một danh sách các header HTTP đã chuẩn hóa.

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 (nhưng không chuẩn) là đặt tên kiểu 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 xung đột 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 kích cỡ nào trong header. Tuy nhiên Node.js (kể từ lúc viết bài này) đã buộc object header nhận một giới hạn kích thước là 80kB cho lý do thực tế.

Không cho phép kích thước tổng của các HTTP header (bao gồm các mã status) vượt quá HTTP_MAX_HEADER_SIZEĐiều này giúp bảo vệ các embedder khỏi các cuộc tấn công từ-chối-dịch-vụ, khi kẻ tấn công sẽ gửi các header không-kết-thúc cho các embedder lưu giữ buffering.

Từ Node.js HTTP parser

#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à tối quan trọng. Sau đây là một số gợi ý:

Express, Koa hay Hapi

Express, Koa hay Hapi có thể được sử dụng để tạo ra các ứng dụng nền web, chúng hỗ trợ templating và rendering. Nếu ứng dụng của bạn cần cung cấp dịch vụ phía người dùng, hãy thử dùng một trong số chúng và tận hưởng thành quả.

Restify

Ở một khía cạnh khác, Restify tập trung hoàn toàn vào việc giúp bạn xây dựng các dịch vụ REST. Restify tồn tại để giúp bạn xây dựng các dịch vụ API "chuẩn" đáng kể, dễ bảo trì. Restify cũng đi kèm với công cụ hỗ trợ tự động DTrace.

Về mức độ phủ sóng thì Restify đang được sử dụng trong rất nhiều các ứng dụng, điển hình như npm hay Netflix.

Restify tồn tại để giúp bạn xây dựng các dịch vụ API "chuẩn" đáng kể, dễ bảo trì.

@RisingStack

#5 - Test black-box các REST API

Một trong những cách hay nhất để 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 như một thể duy nhất.

Để giúp bạn test các REST API theo phương pháp black-box này, ta sẽ sử dụng module supertest.

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 để phổ biến 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 trang 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 mức độ test cao hơn.

Tùy thuộc vào nhu cầu của bạn, bạn có thể phổ biến cơ sở dữ liệu với các dữ liệu test theo một trong các cách sau:

  • Chạy kịch bản test black-box theo một tập con đã biết của dữ liệu production.
  • Phổ biến database với dữ liệu thủ công trước khi chạy các test case.

Dĩ nhiên, black-box test không đồng nghĩa với việc 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.

#6 - JWT - Xác thực phi trạng thái

Các API phải là phi trạng thái, bởi vậy xác thực cũng tương tự. JWT (Jason 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 yêu cầu.
  • Signature (JWT không mã hóa payload mà chỉ kí xác thực).

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

const koa = require('koa')  
const jwt = require('koa-jwt')

const app = koa()

app.use(jwt({  
  secret: 'very-secret' 
}))

// Protected middleware
app.use(function *(){  
  // content of the token will be available on this.state.user
  this.body = {
    secret: '42'
  }
})

Sau khi thêm, endpoint API đã đượ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 cứ lớp 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ả giá trị thời gian sống.

Ngoài ra, bạn phải luôn luôn 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.

Trong bài viết trước, chúng tôi đã giải thích chi tiết về phương pháp xác thực web. Tôi khuyến nghị các bạn hãy tìm hiểu thêm.

#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 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ể

@RisingStack

Các header này sẽ cố gắng kiểm tra liệu một phiên bản tài nguyên được lưu trữ trên server có trùng với phiên bản của cùng một tài nguyên hay không. Vì lý do này, các header có thể là:

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

Chúng chính là:

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

Hãy xem qua ví dụ dưới đây để hiểu thêm!

Ở 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 lẫn 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 đúng.

Client có thể thiết lập header If-Modified-Since và If-None-Match một lần khi nó cố gắng gửi request với cùng tài nguyên. Nếu response trả về là giống nhau, server chỉ đơn giản là phản hồi lại với mã status 304 - Not Modified và không gửi lại tài nguyên nữa.

#8 - Quản lý giới hạn rate (rate limiting)

Rate limiting được sử dụng để điều khiển việc một cunsumer có thể gửi đến API bao nhiêu request.

Để nói với API 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: the number of requests remaining in the same interval. số lượng các request cong lại trong cùng khoảng thời gian.
  • X-Rate-Limit-Reset: thời gian mà rate limit được thiết lập.

Phần lớn các framework HTTP hỗ trợ bằng các công cụ bên ngoài hay là các plugin. Ví dụ, nếu bạn sử dụng Koa, hãy thử package koa-ratelimit.

Chú ý rằng thời gian window có thể thay đổi tùy vào các nhà cung cấp API khác nhau - 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, hưởng lợi ích từ chúng. Do đó việc cung cấp các document đi kèm với các REST API là điều tối cần thiết.

Các project open-source sau đây có thể giúp bạn tạo ra các document cho API:

Nếu bạn muốn sử dụng sản phẩm đã host, bạn có thể dùng Apiary.

#10 - Đừng quên tương lai sáng lạn của API

Trong vài năm vừa qua, hai ngôn ngữ truy vấn lớn cho các API đã mọc 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 - trong 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 các API và là một runtime để hoàn thành các truy vấn đó. GraphQL cung cấp một miêu tả hoàn chỉnh và dễ hiểu về dữ liệu trong các API, cho phép client sức mạnh để yêu cầu chính xác thứ client cần, và chỉ thứ client cần, không hơn. GraphQL khá dễ dàng để phát triển các API theo thời gian, kích hoạt các công cụ phát triển mạnh mẽ. Xem thêm ở đây.

Falcor

Falcor là một nền tảng dữ liệu tiên tiến giúp tối ưu sức mạnh cho Netflix UI. Nó cho phép bạn model tất cả dữ liệu back-end như một object JSON ảo độc lập trên server Node. Ở phía client, bạn sẽ làm việc với object JSON remote, sử dụng các toán tử Javascript quen thuộc như get, set và call. Nếu bạn hiểu dữ liệu, bạn đã hiểu các API. Xem thêm ở đây.

Truyền cảm hứng với các REST API tuyệt vời

Nếu bạn có dự đị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:

Tôi mong là qua bài viết này, các bạn đã có một cái nhìn tổng thể về các API, tại sao các API nên được viết bằng Node.js.

Bài viết được dịch từhttps://blog.risingstack.com/10-best-practices-for-writing-node-js-rest-apis/