Bài 29.4: API Routes trong TypeScript

Bài 29.4: API Routes trong TypeScript
Chào mừng bạn trở lại với hành trình khám phá lập trình web hiện đại! Cho đến nay, chúng ta đã tập trung chủ yếu vào việc xây dựng giao diện người dùng (frontend) đẹp mắt và tương tác mượt mà với HTML, CSS, JavaScript, TypeScript và React/Next.js. Nhưng trong thế giới thực, các ứng dụng web thường cần tương tác với dữ liệu, thực hiện các tác vụ yêu cầu quyền riêng tư hoặc tài nguyên server-side. Đây là lúc khái niệm API Routes trong Next.js trở nên vô cùng mạnh mẽ và cần thiết.
Hãy tưởng tượng bạn đang xây dựng một ứng dụng Next.js và bạn cần:
- Lưu dữ liệu vào database.
- Xử lý thanh toán mà không muốn để lộ khóa API bí mật trên trình duyệt.
- Kết nối đến một dịch vụ bên thứ ba yêu cầu xác thực server-side.
- Hoặc đơn giản là tạo ra các "điểm cuối" (endpoints) API nội bộ để frontend của bạn giao tiếp.
Bạn có thể nghĩ đến việc xây dựng một server backend riêng biệt (ví dụ: với Node.js, Express, Python, Ruby...). Tuyệt vời! Đó là một cách tiếp cận phổ biến. Nhưng Next.js mang đến một giải pháp tích hợp ngay trong dự án frontend của bạn: API Routes.
API Routes là gì?
Hiểu một cách đơn giản, API Routes cho phép bạn tạo ra các điểm cuối API ngay trong thư mục pages/api
(hoặc app/api
trong Next.js 13+ App Router, nhưng chúng ta sẽ tập trung vào pages/api
cho phù hợp với cấu trúc Pages Router phổ biến) của dự án Next.js của bạn. Thay vì xuất ra một component React để render giao diện, các file trong thư mục này sẽ xuất ra một hàm xử lý server-side (serverless function).
Khi một request HTTP đến một đường dẫn nằm trong /api/*
, Next.js sẽ không tìm kiếm file page để render, mà thay vào đó, nó sẽ thực thi code trong file API Route tương ứng và gửi về response.
Điều tuyệt vời nhất khi kết hợp API Routes với TypeScript là bạn có được sự an toàn kiểu dữ liệu (type safety) cho cả request và response, giảm thiểu lỗi không đáng có và giúp code của bạn dễ bảo trì hơn rất nhiều.
Tại sao lại sử dụng API Routes?
- Đơn giản hóa cấu trúc dự án: Đối với nhiều ứng dụng, đặc biệt là các ứng dụng vừa và nhỏ, việc có cả frontend và "backend nhẹ" trong cùng một dự án Next.js giúp quản lý dễ dàng hơn.
- Serverless Native: Next.js được thiết kế để triển khai lên các nền tảng serverless (như Vercel, Netlify Functions...), và API Routes là cách tự nhiên để viết các hàm serverless trong ngữ cảnh của ứng dụng Next.js.
- Truy cập tài nguyên Server-side: Bạn có thể viết code trong API Routes để tương tác trực tiếp với database, hệ thống file server, hoặc gọi các API bên ngoài sử dụng các khóa bí mật mà không cần lo lắng về việc client nhìn thấy chúng.
- Giải quyết vấn đề CORS: Vì request đi từ frontend của bạn đến API Route cùng nguồn gốc (
your-app.com
->your-app.com/api/...
), bạn sẽ không gặp phải các vấn đề về Cross-Origin Resource Sharing (CORS) phức tạp thường thấy khi frontend và backend nằm trên các domain khác nhau. - Tích hợp TypeScript: Như đã đề cập, việc sử dụng TypeScript giúp bạn định nghĩa rõ ràng kiểu dữ liệu cho dữ liệu nhận được từ client (request body, query params) và dữ liệu trả về cho client (response), mang lại trải nghiệm phát triển tốt hơn.
Bắt đầu với API Route đơn giản
Hãy tạo API Route đầu tiên của bạn. Mở dự án Next.js và tạo file sau:
pages/api/hello.ts
Nội dung của file này sẽ như sau:
import { NextApiRequest, NextApiResponse } from 'next';
// Định nghĩa kiểu dữ liệu cho response
type Data = {
message: string;
};
export default function handler(
req: NextApiRequest, // req là NextApiRequest
res: NextApiResponse<Data> // res là NextApiResponse, và chúng ta chỉ định kiểu dữ liệu cho response body là Data
) {
// Kiểm tra phương thức HTTP
if (req.method === 'GET') {
// Trả về response JSON với status 200
res.status(200).json({ message: 'Chào mừng bạn đến với API Route đầu tiên của tôi với TypeScript!' });
} else {
// Nếu không phải phương thức GET, trả về lỗi 405 Method Not Allowed
res.setHeader('Allow', ['GET']); // Cho biết chỉ cho phép phương thức GET
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
Giải thích code:
import { NextApiRequest, NextApiResponse } from 'next';
: Chúng ta import hai kiểu dữ liệu quan trọng từ thư việnnext
.NextApiRequest
đại diện cho đối tượng yêu cầu HTTP đến (chứa thông tin về request như method, headers, body, query params...), cònNextApiResponse
đại diện cho đối tượng phản hồi HTTP mà chúng ta sẽ gửi lại cho client.type Data = { message: string; };
: Chúng ta định nghĩa một kiểu dữ liệuData
cho cấu trúc JSON mà API này sẽ trả về thành công.export default function handler(...)
: Mỗi API Route file phải xuất ra một hàmdefault
duy nhất. Next.js sẽ gọi hàm này khi request đến đường dẫn/api/hello
. Hàm này nhận hai tham số:req
(request) vàres
(response).req: NextApiRequest
: Chúng ta khai báoreq
có kiểu làNextApiRequest
. TypeScript sẽ cung cấp gợi ý code và kiểm tra kiểu cho các thuộc tính củareq
(ví dụ:req.method
,req.query
,req.body
,req.headers
).res: NextApiResponse<Data>
: Chúng ta khai báores
có kiểu làNextApiResponse
. Quan trọng hơn, chúng ta sử dụng kiểu generic<Data>
để chỉ định rằng phương thứcres.json()
sẽ trả về dữ liệu có cấu trúc phù hợp với kiểuData
. Điều này giúp TypeScript kiểm tra xem bạn có đang trả về đúng cấu trúc dữ liệu mong đợi hay không.if (req.method === 'GET')
: Chúng ta kiểm tra xem phương thức HTTP của request có phải làGET
hay không. API Routes thường được sử dụng để xử lý các phương thức HTTP khác nhau (GET để lấy dữ liệu, POST để tạo mới, PUT để cập nhật, DELETE để xóa...).res.status(200).json({...});
: Nếu làGET
, chúng ta thiết lập status code của response là200 OK
(thành công) và gửi về một đối tượng JSON sử dụng phương thứcres.json()
. Nhờ kiểu<Data>
trênres
, TypeScript sẽ yêu cầu đối tượng bạn truyền vàojson()
phải có thuộc tínhmessage
kiểustring
.res.setHeader('Allow', ['GET']); res.status(405).end(...);
: Nếu phương thức request không được hỗ trợ (ở đây là chỉ hỗ trợGET
), chúng ta trả về status code405 Method Not Allowed
. Phương thứcres.setHeader
giúp thông báo cho client biết những phương thức nào được chấp nhận.res.end()
kết thúc response mà không gửi body.
Bây giờ, bạn có thể chạy ứng dụng Next.js (npm run dev
hoặc yarn dev
) và truy cập vào đường dẫn http://localhost:3000/api/hello
trên trình duyệt hoặc dùng công cụ như Postman/Insomnia/curl
. Bạn sẽ thấy response:
{
"message": "Chào mừng bạn đến với API Route đầu tiên của tôi với TypeScript!"
}
Nếu bạn thử gửi request với phương thức khác (ví dụ: POST), bạn sẽ nhận được lỗi 405.
Xử lý nhiều phương thức HTTP
Một API Route thường cần xử lý nhiều phương thức HTTP khác nhau tùy thuộc vào hành động mong muốn. Ví dụ, cùng một đường dẫn /api/users
có thể dùng để lấy danh sách người dùng (GET) hoặc tạo người dùng mới (POST).
Chúng ta có thể mở rộng file pages/api/users.ts
(ví dụ) để xử lý cả GET và POST:
import { NextApiRequest, NextApiResponse } from 'next';
// Định nghĩa kiểu dữ liệu cho User
interface User {
id: string;
name: string;
email: string;
}
// Kiểu dữ liệu cho request body khi tạo user
interface CreateUserRequestBody {
name: string;
email: string;
}
// Kiểu dữ liệu cho response khi lấy danh sách user
interface GetUsersResponse {
users: User[];
}
// Kiểu dữ liệu cho response khi tạo user
interface CreateUserResponse {
success: boolean;
userId: string;
message?: string;
}
// Kết hợp các kiểu response có thể có cho API này
type UsersApiResponse = GetUsersResponse | CreateUserResponse | { message: string }; // Thêm kiểu cho lỗi
export default function handler(
req: NextApiRequest,
res: NextApiResponse<UsersApiResponse> // Response có thể là một trong các kiểu đã định nghĩa
) {
switch (req.method) {
case 'GET':
// Logic để lấy danh sách người dùng
const users: User[] = [ // Dữ liệu giả định
{ id: 'user1', name: 'Alice', email: 'alice@example.com' },
{ id: 'user2', name: 'Bob', email: 'bob@example.com' },
];
res.status(200).json({ users });
break;
case 'POST':
try {
// Lấy dữ liệu từ request body và ép kiểu (type assertion)
// Next.js tự động parse JSON body cho bạn
const { name, email } = req.body as CreateUserRequestBody;
// Kiểm tra dữ liệu nhận được
if (!name || !email) {
return res.status(400).json({ success: false, message: 'Thiếu tên hoặc email.' });
}
// Logic để tạo người dùng mới (ví dụ: lưu vào database)
const newUserId = `user_${Date.now()}`; // Tạo ID giả định
// Trả về response thành công
res.status(201).json({ success: true, userId: newUserId, message: 'Người dùng đã được tạo thành công!' });
} catch (error: any) {
console.error('Lỗi khi xử lý POST request:', error);
res.status(500).json({ success: false, message: 'Đã xảy ra lỗi server.' });
}
break;
default:
// Phương thức không được hỗ trợ
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
Giải thích code:
- Chúng ta định nghĩa các
interface
cho cấu trúc dữ liệuUser
,CreateUserRequestBody
(dữ liệu mong đợi trong body của POST request),GetUsersResponse
, vàCreateUserResponse
. type UsersApiResponse = ...
: Chúng ta sử dụng kiểu kết hợp (union type) để báo cho TypeScript biết rằng response của API này có thể có nhiều hình dạng khác nhau tùy thuộc vào phương thức và kết quả.- Chúng ta sử dụng một khối
switch
dựa trênreq.method
để xử lý các phương thứcGET
vàPOST
riêng biệt. case 'GET':
: Nếu là GET, chúng ta tạo một mảngusers
giả định và trả về nó trong một đối tượng{ users: [...] }
với status200 OK
. TypeScript đảm bảo cấu trúc này phù hợp vớiGetUsersResponse
.case 'POST':
: Nếu là POST, chúng ta truy cập dữ liệu gửi lên trongreq.body
. Next.js tự động parse JSON body cho bạn, nênreq.body
sẽ là một đối tượng JavaScript. Tuy nhiên, TypeScript mặc định coireq.body
làany
. Để có type safety, chúng ta sử dụngreq.body as CreateUserRequestBody;
(type assertion). Điều này nói với TypeScript rằng "tôi biết chắc chắnreq.body
sẽ có cấu trúc này", cho phép bạn truy cập các thuộc tính nhưname
vàemail
một cách an toàn với gợi ý code. Lưu ý: type assertion không kiểm tra lúc runtime. Để an toàn hơn, bạn nên thêm logic kiểm tra dữ liệu (validation) sau khi nhận đượcreq.body
.- Chúng ta thêm một khối
try...catch
để bắt lỗi trong quá trình xử lý request POST và trả về status500 Internal Server Error
nếu có lỗi xảy ra. Status201 Created
được sử dụng khi tạo tài nguyên mới thành công. Status400 Bad Request
được trả về nếu dữ liệu gửi lên không hợp lệ. - Phần
default
xử lý các phương thức khác không được định nghĩa.
Với API Route này, bạn có thể:
- Gửi request
GET
đếnhttp://localhost:3000/api/users
để lấy danh sách người dùng giả định. - Gửi request
POST
đếnhttp://localhost:3000/api/users
với body dạng JSON{ "name": "Charlie", "email": "charlie@example.com" }
để mô phỏng việc tạo người dùng mới.
API Routes Động (Dynamic Routes)
Giống như Pages Router, API Routes cũng hỗ trợ các đường dẫn động. Điều này cực kỳ hữu ích khi bạn muốn truy cập một tài nguyên cụ thể dựa trên ID hoặc slug.
Ví dụ, để lấy thông tin chi tiết của một người dùng dựa trên ID, bạn có thể tạo file:
pages/api/users/[id].ts
Nội dung file:
import { NextApiRequest, NextApiResponse } from 'next';
interface User {
id: string;
name: string;
email: string;
}
// Kiểu dữ liệu cho response (có thể là User hoặc thông báo lỗi)
type UserDetailApiResponse = User | { message: string };
export default function handler(
req: NextApiRequest,
res: NextApiResponse<UserDetailApiResponse>
) {
// Lấy giá trị của segment động [id] từ req.query
const { id } = req.query;
// req.query luôn là string hoặc mảng string, nên id sẽ là string | string[] | undefined
// Trong trường hợp [id].ts, id thường là string hoặc undefined (nếu đường dẫn chỉ là /api/users/)
const userId = Array.isArray(id) ? id[0] : id;
if (!userId) {
return res.status(400).json({ message: 'Thiếu ID người dùng.' });
}
if (req.method === 'GET') {
// Logic để tìm người dùng theo userId (ví dụ: truy vấn database)
// Dữ liệu giả định:
const users: User[] = [
{ id: 'user1', name: 'Alice', email: 'alice@example.com' },
{ id: 'user2', name: 'Bob', email: 'bob@example.com' },
];
const user = users.find(u => u.id === userId);
if (user) {
res.status(200).json(user); // Trả về thông tin người dùng
} else {
// Không tìm thấy người dùng
res.status(404).json({ message: `Không tìm thấy người dùng với ID: ${userId}` });
}
} else if (req.method === 'PUT') {
// Logic để cập nhật người dùng với userId
// Ví dụ đơn giản:
const updatedData = req.body as Partial<User>; // Mong đợi một phần dữ liệu User
// ... xử lý cập nhật ...
res.status(200).json({ message: `Người dùng ${userId} đã được cập nhật.` });
} else if (req.method === 'DELETE') {
// Logic để xóa người dùng với userId
// ... xử lý xóa ...
res.status(200).json({ message: `Người dùng ${userId} đã được xóa.` });
} else {
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
Giải thích code:
- File được đặt tên
[id].ts
trong thư mụcusers
. Điều này cho Next.js biết rằng phần[id]
của đường dẫn là một tham số động. - Chúng ta lấy giá trị của tham số động bằng cách truy cập
req.query
. Đối tượngreq.query
chứa các tham số từ query string (ví dụ:/api/users/user1?active=true
) và các tham số từ đường dẫn động. Tên thuộc tính trongreq.query
sẽ khớp với tên file trong ngoặc vuông (ví dụ:id
trong[id].ts
). req.query.id
có thể làstring
,string[]
(nếu có nhiều segment động lồng nhau) hoặcundefined
. Chúng ta cần xử lý trường hợp này, ở đây chúng ta giả định đơn giản là lấy phần tử đầu tiên nếu nó là mảng hoặc giữ nguyên giá trị.- Đối với phương thức
GET
, chúng ta sử dụnguserId
để tìm kiếm trong mảngusers
giả định. Nếu tìm thấy, trả về đối tượnguser
với status200 OK
. Nếu không, trả về status404 Not Found
và một thông báo lỗi. - Chúng ta cũng thêm ví dụ về cách xử lý phương thức
PUT
(cập nhật) vàDELETE
. Lưu ý cách sử dụngPartial<User>
để chỉ định rằng body của request PUT có thể chỉ chứa một phần các thuộc tính của User.
Với cấu trúc này, bạn có thể truy cập:
http://localhost:3000/api/users/user1
(GET) để lấy thông tin Alice.http://localhost:3000/api/users/user999
(GET) sẽ trả về lỗi 404.http://localhost:3000/api/users/user1
(PUT) để cập nhật thông tin Alice.http://localhost:3000/api/users/user2
(DELETE) để xóa Bob.
Tóm lại
API Routes trong Next.js, đặc biệt khi kết hợp với TypeScript, cung cấp một cách tiếp cận cực kỳ hiệu quả và an toàn kiểu dữ liệu để thêm các chức năng server-side vào ứng dụng web của bạn mà không cần một backend riêng biệt phức tạp.
Bạn đã học cách:
- Tạo một API Route cơ bản trong thư mục
pages/api
. - Sử dụng các kiểu dữ liệu
NextApiRequest
vàNextApiResponse
từ Next.js để định nghĩa rõ ràng kiểu của request và response. - Định nghĩa kiểu cho response body bằng cách sử dụng generic
<Data>
trênNextApiResponse
. - Xử lý các phương thức HTTP khác nhau (
GET
,POST
, v.v.) trong cùng một API Route file. - Truy cập dữ liệu từ request body (
req.body
) và sử dụng type assertion để làm việc với TypeScript. - Tạo Dynamic API Routes (ví dụ:
[id].ts
) và truy cập các tham số động từreq.query
. - Quan trọng là luôn nhớ về xử lý lỗi và trả về các status code HTTP phù hợp (200, 201, 400, 404, 405, 500).
Sức mạnh của API Routes nằm ở sự linh hoạt và tích hợp chặt chẽ với môi trường Next.js. Chúng cho phép bạn xây dựng các ứng dụng full-stack mạnh mẽ chỉ với một framework duy nhất. Hãy thực hành và khám phá thêm các khả năng của chúng!
Comments