Middleware trong Next.js - Cách Tiếp Cận Đúng Đắn
Khi bạn bắt đầu làm việc với middleware trong Next.js, có vẻ như thật đơn giản:
chỉ cần thêm một vài chuyển hướng, bảo vệ một số tuyến đường, xong.
Nhưng vấn đề ở đây là: hầu hết các lập trình viên (bao gồm cả tôi trong quá khứ) cuối cùng lại viết middleware như thế này:
typescript
export async function middleware(request: NextRequest, user: User | null) {
const { pathname } = request.nextUrl;
if (pathname.startsWith("/dashboard")) {
if (!user) {
return NextResponse.redirect(new URL("/login", request.url));
}
} else if (pathname.startsWith("/onboarding")) {
if (!user) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
if (pathname.startsWith("/login")) {
if (user) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
} else if (pathname.startsWith("/signup")) {
if (user) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
}
return NextResponse.next();
}
Mặc dù đoạn mã này hoạt động tốt ở cái nhìn đầu tiên, nhưng có một vấn đề lớn trong cách tiếp cận này.
Khi bạn thêm nhiều tuyến đường hơn (ví dụ: /profile, /settings, /forgot-password, v.v.)... tệp của bạn sẽ trở thành một mớ hỗn độn của các if/else.
Và điều này sẽ chỉ trở nên tồi tệ hơn khi dự án của bạn mở rộng.
Không thú vị, không mở rộng được, và chắc chắn - không sạch sẽ.
Suy Nghĩ Theo Quy Tắc, Không Phải Điều Kiện
Thay vì mã hóa cứng mọi chuyển hướng, chúng ta chỉ cần định nghĩa các điều kiện.
Hãy nghĩ về middleware như một cảnh sát giao thông.
Cảnh sát không quan tâm bạn là ai. Họ chỉ kiểm tra bạn đang đi đâu và liệu bạn có được phép ở đó hay không.
Vì vậy, hãy mô tả các quy tắc của chúng ta một cách có cấu trúc.
Bước 1: Định Nghĩa Nhóm Tuyến Đường
Chúng ta sẽ chia các tuyến đường thành hai nhóm đơn giản:
typescript
const PROTECTED_ROUTES = ["/dashboard", "/onboarding"];
const AUTH_ROUTES = ["/login", "/signup"];
- Nhóm tuyến đường bảo vệ: chỉ người dùng đã đăng nhập mới có thể truy cập.
- Nhóm tuyến đường xác thực: chỉ người dùng chưa đăng nhập mới nên thấy chúng.
Đã sạch hơn rất nhiều so với việc viết 20 câu lệnh if kiểm tra.
Bước 2: Mô Tả Quy Tắc
Bây giờ hãy tạo một kiểu cho các quy tắc:
typescript
interface RouteRule {
routes: string[];
condition: (user: User | null) => boolean;
redirect: string;
}
Mỗi quy tắc nói:
- Các tuyến đường mà nó áp dụng
- Điều kiện kích hoạt một chuyển hướng
- Địa điểm chuyển hướng đến
Bước 3: Viết Các Quy Tắc
Đây là hình dạng của hai quy tắc của chúng ta:
typescript
const routeRules: RouteRule[] = [
{
routes: PROTECTED_ROUTES,
condition: (user) => !user, // nếu không có người dùng, chuyển hướng
redirect: "/login",
},
{
routes: AUTH_ROUTES,
condition: (user) => !!user, // nếu đã đăng nhập, chuyển hướng
redirect: "/dashboard",
},
];
Vậy là xong. Thêm nhiều nhóm? Chỉ cần thêm nhiều đối tượng.
Không cần phải chạm vào logic một lần nữa.
Bước 4: Áp Dụng Các Quy Tắc Trong Middleware
Bây giờ chúng ta lặp qua các quy tắc:
typescript
export class Middleware {
private routeRules = routeRules;
async handle(request: NextRequest, user: User | null) {
const { pathname } = request.nextUrl;
for (const rule of this.routeRules) {
const isMatch = rule.routes.some((r) =>
pathname.startsWith(r)
);
if (isMatch && rule.condition(user)) {
return NextResponse.redirect(
new URL(rule.redirect, request.url)
);
}
}
return NextResponse.next();
}
}
Không có sự lộn xộn. Không có hỗn loạn. Chỉ có logic rõ ràng.
Tại Sao Đây Là "Cách Đúng"
- Mở rộng – Thêm tuyến đường mới chỉ trong vài giây
- Dễ đọc – Không có rừng rậm các câu lệnh if/else
- Tái sử dụng – Một nơi trung tâm cho các quy tắc
- Mở rộng – Có thể được mở rộng cho vai trò, quyền hạn, v.v.
- Tuân thủ SOLID.
Middleware phải cảm thấy như một middleware, không phải một cơn ác mộng.
Những Suy Nghĩ Cuối Cùng
Mẹo rất đơn giản:
Ngừng mã hóa cứng các điều kiện. Bắt đầu suy nghĩ theo quy tắc.
Mô hình này sẽ giúp bạn tiết kiệm nhiều đau đầu, đặc biệt nếu ứng dụng của bạn có xác thực và một tập hợp các trang được bảo vệ ngày càng phát triển.
Nếu bạn thấy điều này thú vị, hãy bình luận và chúng ta hãy nói chuyện!