Skip to Content

API Best Practices

Posted on
Table of Contents

Mở đầu

Mấy năm gần đây, mình được tham gia vào khá nhiều projects trên công ty về build API. Mỗi dự án mình lại cóp nhặt được 1 tí kinh nghiệm để build API sao “hợp lý”. Bài viết dưới đây là tổng hợp một vài lưu ý nhỏ trong quá trình làm API của mình. Hy vọng sẽ có ích cho các bạn :v

Nếu bạn đã quen với API, bạn có thể đi tới luôn mục Key Requirements

API là viết tắt của Application Programming Interface. Tại sao chúng ta lại cần design API, có tốt hơn gì so với Web (server side rendering) thông thường hay không? Thường mình thấy có 2 lý do chính:

  • Với sự phát triển chóng mặt của Internet, số lượng người truy cập vào các trang web tăng lên nhiều, các mô hình web cũ khó đáp ứng được. API sinh ra để giảm tải hơn cho server, tách riêng từng phần ra, dạng Single Responsibility. Server sẽ không render view nữa, mà chỉ quan tâm chủ yếu vào logic backend.
  • Khi đã có API, việc tương tác giữa các services hoặc với các app khác trở nên dễ dàng hơn. Ví dụ: Có rất nhiều app sử dụng tính năng đăng nhập bằng tài khoản Google/Facebook (thông qua API của Facebook/Google). Nếu không có API, sẽ rất khó để các developer khác có thể tạo ra các app mới trên nhiều Platform khác nhau.

Có 2 loại API là:

  • Public API - Public ra cho bên ngoài sử dụng.
  • Private API - Chỉ sử dụng cho việc giao tiếp giữa các sản phẩm của công ty. (Có thể là giao tiếp giữa các microservices)

Key requirements

Nếu như làm giao diện web có UX - User experiences thì trong thiết kế API, ta phải chú trọng tới DX - Developer experiences :v Vì người sử dụng API của chúng ta chủ yếu sẽ là các dev. Vì thế, hãy follow 2 key requirements này:

  1. Should use web standard: Use RESTful, SSL everywhere, Great document, HTTP status code, …
  2. Friendly to the developer: API can be explorable via a browser address bar

Best Practices

1. RESTful URLs and Actions

Key principles của REST là chia nhỏ API thành các logical resources. Các request sẽ sử dụng HTTP requests với method GET, POST, PUT, PATCH, DELETE.

Cách chọn resources thường là các danh từ có nghĩa. (Giống với tên đặt cho các models).

# Retrieves a list of tickets
GET /tickets

# Retrieves a specific ticket
GET /tickets/:id

# Create a new ticket
POST /tickets

# Update ticket
PUT /tickets/:id

# Partially update ticket
PATCH /tickets/:id

# Delete ticket
DELETE /tickets/:id

Cùng là 1 url (/tickets, /tickets/:id) nhưng với các HTTP method khác nhau thì sẽ trỏ tới các actions khác nhau.

Khi áp dụng REST vào dự án, về lâu dài ta sẽ phát sinh một vài vấn đề:

1a. Singular vs Plural

Ta nên dùng số ít hay số nhiều cho resource? ticket/:id hay tickets/:id ?

Đại đa số API của các trang web hiện tại đều dùng số nhiều. Đây cũng là chuẩn của Rails.

Tuy nhiên có 1 số API vẫn dùng số ít. Ví dụ như API của Github: https://github.com/username/repo/pull/1. Github đang dùng pull/:id thay cho pulls/:id

=> Chọn như thế nào tuỳ thuộc vào bạn. Tuy nhiên nếu framework bạn đang dùng có chuẩn cho phần này, nên follow theo đó 😁

1b. Nested resources

Nested resources khá quen thuộc, nó thể hiện relationship giữa các model. Ví dụ: https://api.example.com/v1/projects/1/tickets/2. URL này là dạng nested giữa projectstickets.

Tuy nhiên, cũng có nhiều luồng tư tưởng mới. Ví dụ như trong Zen of Python có đoạn:

Flat is better than nested.

-> Cũng có nhiều trang để API ticket của họ dạng: https://api.example.com/v1/tickets?ticket_id=2&project_id=1. Vẫn như bên trên, lựa chọn như thế nào là tuỳ ở bạn 😁

Nested còn gây ra một vấn đề nữa, đó là long URL.

https://api-host/v1/customers/1/projects/1/orders/1/lines/1

Nếu để URL thế này sẽ rất rối và khó follow, nên tốt nhất, nếu dùng nested, bạn nên giới hạn nesting depth về <= 3.

1c. NonREST

Sẽ có những trường hợp bạn khó để dùng RESTful. Nếu ép để chuyển về dùng namespace/ REST thì URL trông sẽ không hợp lý cho lắm.

Ví dụ như khi ta active 1 tài khoản, URL thường là: https://api-host/v1/user/active. Nếu đổi về thành https://api-host/v1/active/user (đưa active về thành namespace) thì trông có vẻ không hay lắm :v

Gist trên Github cũng có 1 số action NonREST, ví dụ như khi bạn star cho 1 gist, URL sẽ là: PUT /gists/:id/star.

Hoặc khi search trên Youtube, url cũng dạng GET /search?query

2. Friendly API URL

Như trong key requirements có nhắc tới, ta nên tìm cách implement để có thể tận dụng params ngay trên URL.

2a. Filtering

Ví dụ để lấy hết các ticket, ta dùng GET /tickets. Nhưng giờ nếu chỉ muốn lấy ra các ticket open, thì ta nên implement để khi dev gọi URL dạng GET /ticket?filtering='state=open' thì ta sẽ trả về kết quả mong muốn.

Bạn có thể tham khảo qua 1 số cách implement ở đây:

2b. Sorting

API nên cho phép người dùng có thể order theo thứ tự mà họ mong muốn. Ví dụ: GET /tickets?sort=-priority để sắp xếp theo thứ tự giảm dần của priority. Hoặc GET /tickets?sort=-priority,created_at

Cách implement thì bạn có thể tham khảo qua ở đây:

2c. Searching

API URL cho phần search có thể ở dạng: GET /tickets?q=key_word.

Combie lại, 1 URL có thể ở dạng:

GET /tickets?q=key_word&state=open&sort=-priority,created_at

NOTE: Ta cũng nên cho phép người dùng sử dụng alias cho common queries: GET /tickets/recently_closed chẳng hạn.

2d. Limiting fields are returned by the API

API của bạn có thể được gọi bởi nhiều client khác nhau: Mobile device, Web client (Vuejs, Angular, React, ..) hoặc từ trên terminal. Do có sự khác nhau giữa các client, nên response cho API trả về cũng nên khác nhau.

Ví dụ ở trang show profile user. Trên màn hình web ta có thể trả về full trường: avatar, username, first_name, last_name, email, bio, .... Tuy nhiên, màn hình điện thoại nhỏ hơn nên ta không hiển thị được nhiều thông tin như vậy, nếu trả về full trường như bên API web thì cũng sẽ là thừa. Vì có trả về cũng không dùng tới.

Nếu như bạn sử dụng Graph API Explorer của Facebook, bạn có thể thấy API của Facebook đang sử dụng trường fields trên URL, cho phép chỉ trả về những trường cần thiết. Tức là client cần trường gì thì truyền lên, server sẽ trả về những trường tương ứng.

GET /tickets?fields=id,subject,customer_name

Ưu điểm của việc này là sẽ tối ưu hoá được network traffic + speed up API. (được nhiều hay không thì mình không đo được nên không rõ =)))) )

Trong các API trả về, sẽ có những API yêu cầu cần thêm các thông tin liên quan.

Ví dụ: Trong API về article thì cần thêm thông tin của user để in ra tên tác giả chẳng hạn. Bình thường code serializer trên server sẽ thêm quan hệ dạng belongs_to hoặc has_many vào, response trả về sẽ tự động có thêm relationship info.

Tuy nhiên, theo như phần limited field mà ta nói ở trên, cách này là server quyết định response trả về -> Có thể trả về thừa thông tin, không linh hoạt cho lắm :D

Cách làm đó là ta nên thêm embed query:

GET /articles/12?embed=user.name,user.avatar

2f. Pagination

Pagination là 1 phần rất quan trọng, không chỉ trong API mà cả cho giao diện web. Hiện nay các thư viện support pagination cũng được implement theo hướng response trả về khá giống nhau. Và ta cũng có thể chỉ định page và số lượng items trả về ngay trên URL theo dạng:

GET /tickets?page=3&items=15

{
  success: true,
  data: [...],
  meta: {
	 paging: {
	 page: current_page,
	 items: current_items_in_page,
	 counts: total_item,
	 prev: link_to_prev_page,
	 next: link_to_next_page
	}
  }
}

Trong Ruby on Rails thì mình hay dùng gem pagy.

3. Versioning

Nếu ta dùng git để gắn tag mỗi lần release để đánh dấu cho những mốc phát triển của phần mềm, thì việc đánh version cho API cũng có ý nghĩa tương tự. API, requirements luôn thay đổi, dù ít hay nhiều. Nên việc bạn đánh dấu lại API version của mình cũng là cần thiết.

Một cách đánh version khá phổ biến đó là: z.y.x

  • Nếu thay đổi là bugfix, tăng x.
  • Nếu thay đổi là add thêm feature, tăng y.
  • Nếu thay đổi thuộc dạng breaking changes, tăng z.

Breaking changes là gì? Ví dụ như thay đổi response format, từ XML sang JSON, hoặc API gọi tới /tickets sẽ bị xoá trong version tiếp, .. Nói chung là những thay đổi có thể ảnh hưởng xấu tới các app hiện tại của developer đang dùng API.

1 API không bao giờ cố định mãi được, thay đổi là điều chắc chắn xảy ra. Quan trọng là chúng ta quản lý thay đổi như thế nào?

Lời khuyên là: Clear document. Thông báo lịch force-update cụ thể để người dùng có thể theo dõi được. Giống như 1 số API sắp bị deprecated trong version 3.0 thì từ version 2.7, mỗi lần gọi tới những API này, người dùng đều nhận được WARNING, để biết mà update app.

Một vài vấn đề:

Nên đặt API version ở đâu?

Ta có thể để luôn trong URL, dạng api.example.com/v1, hoặc example.com/api/v1. Cũng có thể đặt trong custom header, dạng: accept-version: v1, hoặc Accept header: application/vnd.example.v1+json hoặc application.vnd.example+json.version=1.0

Để ở đâu cũng được, nhưng mình nghĩ đặt trên url thì hay hơn :v vì nhìn được luôn, khá trực quan.

Khi nâng cấp version thì quản lý source code như thế nào?

Vấn đề là khi ta nâng cấp từ version 1 lên version 2, ta sẽ chỉ migrate 1 số endpoint, chứ k phải toàn bộ. Vậy các endpoint mà vẫn giữ nguyên code thì sẽ như thế nào?

Ví dụ:

# app/controllers/api/v1/tickets_controller.rb
class Api::V1::TicketsController
  def show
	 # Logic here
  end
end

Khi nâng cấp lên version 2, endpoint này vẫn giữ nguyên logic, k thay đổi gì cả. Vậy bạn sẽ làm thế nào?

Cách 1: Tạo 1 controller app/controllers/api/v2/tickets_controller.rb, và bê nguyên code của bên controller V1 sang =)). Cách này ưu điểm là nhanh gọn, code từng version sẽ được tách biệt hoàn toàn. Tuy nhiên nhược điểm là code logic sẽ bị lặp rất nhiều. Ví dụ lên V3 code hàm này vẫn thế, thì bạn sẽ có 3 đoạn code giống nhau trong source code.

Cách 2: Tạo 1 controller app/controllers/api/v2, kế thừa lại code từ controller v1. Ưu điểm: Code không bị lặp, sử dụng kế thừa. Tuy nhiên nhược điểm là version sau sẽ bị phụ thuộc vào version cũ.

Sample code cho 2 cách:

# Cách 1
class Api::V2::TicketsController
  def show
	 # Logic giống với bên controller v1
  end
end

# Cách 2
class Api::V2::TicketsController < Api::V1::TicketsController
end

Mỗi cách đều có ưu, nhược điểm, lựa chọn cách nào là tuỳ bạn =)) Nếu bạn có cách làm nào hay hơn, thì comment ở dưới cuối bài viết nhé. Mình xin chân thành cảm ơn 🍻

4. Handle Errors

Error trả về là 1 phần rất quan trọng. Error của bạn đúng, đủ, rõ ràng, là cách rất tốt để cho các developer có thể debug được.

  • Format phải rõ ràng. Nếu có lỗi gì, cần có description chi tiết.
  • Phải public, có quy định về mã lỗi.
  • HTTP status code phải trả về đúng quy định: 4xx là lỗi từ phía client, 5xx là lỗi từ phía server.

Ví dụ:

{
  code: 123,
  message: "Missing title",
  description: "xxxx"
}

Về HTTP status code, bạn có thể tham khảo ở đây:

5. Documentation

Check documentation có lẽ là bước đầu tiên mỗi khi developer nào muốn bắt đầu sử dụng API của bạn. Vì thế, hãy chuẩn bị 1 bộ doc thật đầy đủ và chi tiết.

1 Document tốt là document có thể cho phép người dùng chạy thử luôn trên trình duyệt hoặc trên URL.

Bạn có thể tham khảo Stripe document.

Ngoài ra, bạn có thể dùng 1 tool rất nổi tiếng, đó là Swagger. Mới đầu khi mình biết đến swagger, mình chỉ nghĩ đó là 1 tool support việc viết document. Làm việc viết document trở nên dễ dàng hơn. Điều đó đúng, nhưng chưa đủ =))

Swagger Codegen cho phép bạn generate ra code từ file swagger.yaml. Flow sẽ là:

  1. Server viết code backend
  2. Khi release 1 version mới, CI sẽ dùng swagger để build 1 client, push lên git.
  3. Client (app/ javascript client, ..) sẽ include thư viện vừa được build kia vào trong code client, để dùng ngay.

Bạn có thể tham khảo cách làm ở đây: How to use swagger to generate API client as an Angular lib

Nếu bạn code Nodejs, thì bạn không nên bỏ qua thư viện này swagger node. Support tận răng cho việc viết API luôn =))

6. Others

Mục này mình để 1 số notes nhỏ, nhưng cũng không kém phần quan trọng:

  • Response format: Trước đây mọi người thường dùng XML, nhưng giờ mọi người đã chuyển qua dùng JSON nhiều hơn. Compare trend: XML API vs JSON API
  • Authentication: Sử dụng OAuth 2 + Bearer token. Trong Rails bạn có thể dùng gem doorkeeper.
  • SSL everywhere: For security!!!
  • Caching: Sử dụng ETag và Last-Modified field trong Header của request.
  • Không nên đặt sensitive data trên URL, ví dụ: GET /tickets?api_key=xxxx

Summary

Trên đây là một số vấn đề mình gặp phải, học được trong quá trình làm API. Trên này cũng có rất nhiều thông tin mình đọc được ở các bài viết trên Internet (nhưng đọc lâu quá rồi, k nhớ nguồn =))) ).

Tóm lại, sau khi đọc bài này, bạn chỉ nên nhớ 2 ý chính trong mục Key Requirements là đủ =))

Thank you for reading!!! Hy vọng bài viết có thể có ích cho bạn :v

References:

comments powered by Disqus