API Gateway: HTTP API hay REST API, và Dựng Route Đầu Tiên
Function URL ở bài 01 đủ để gọi một hàm qua HTTP, nhưng nó chỉ trỏ tới đúng một hàm và không có khái niệm route. Sản phẩm của ta cần nhiều đường: một đường để tạo link, một đường để mở link, rồi sau này thêm đường cho đăng nhập và dashboard. Đó là việc của API Gateway. Bài này chọn loại API phù hợp rồi dựng hai route thật đầu tiên.
Mục tiêu
Hiểu khác biệt giữa HTTP API và REST API của API Gateway để chọn đúng, rồi dựng POST /links (tạo link, trả mã ngắn) và GET /{code} (chuyển hướng 301). Trên đường đi, xử lý validate input, đọc path parameter, và để API Gateway lo CORS preflight. Tài nguyên trong free tier, deploy rồi xóa, chi phí không đáng kể.
Hai loại API, hai triết lý
API Gateway có hai sản phẩm khác nhau cùng làm việc nhận HTTP request rồi gọi backend: REST API (ra đời trước, nhiều tính năng) và HTTP API (ra sau, tối giản). Tài liệu AWS nói thẳng sự đánh đổi: "REST APIs support more features than HTTP APIs, while HTTP APIs are designed with minimal features so that they can be offered at a lower price."
Khi nào cần REST API? Tài liệu liệt kê: "Choose REST APIs if you need features such as API keys, per-client throttling, request validation, AWS WAF integration, or private API endpoints." Đó là các tính năng quản lý nặng tay: phát API key cho từng client, giới hạn tần suất theo từng client, validate request ngay tại gateway, gắn WAF trực tiếp, hay endpoint riêng tư trong VPC.
Với URL shortener, ta không cần nhóm đó ở phiên bản đầu. Cái ta cần là một API public, rẻ, và quan trọng là có sẵn cách xác thực bằng JWT cho Cognito ở bài sau. Đây là chỗ HTTP API thắng rõ: bảng so sánh trong tài liệu cho thấy HTTP API hỗ trợ JWT authorizer gốc, còn REST API thì không (REST API phải dùng một Lambda authorizer để tự kiểm JWT). Vì bài 07 dùng Cognito phát JWT, HTTP API cho ta đường tích hợp gọn nhất.
REST API HTTP API
Giá cao hơn thấp hơn
API key / usage plan có không
Throttle theo client có không
Request validation có không (validate trong code)
WAF gắn trực tiếp có không (qua CloudFront)
JWT authorizer gốc không (dùng Lambda) có ← hợp Cognito
Cognito authorizer có có (qua JWT)
Một điểm cần ghi nhớ cho bài 16: HTTP API không gắn AWS WAF trực tiếp được. Khi cần WAF, ta đặt một CloudFront distribution phía trước rồi gắn WAF ở đó. Ta chọn HTTP API và chấp nhận đường vòng đó, vì những tính năng REST API thừa ra đều là thứ ta không dùng tới mà vẫn trả tiền.
Code hai handler
POST /links nhận một URL, validate, sinh mã ngắn, trả về. Bài này chưa lưu trữ gì (việc đó dành cho bài 04); ở đây ta tập trung vào phần API. Một hàm phụ lo việc sinh mã và kiểm URL:
import { randomBytes } from "node:crypto";
const ALPHABET =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // base62
export function generateShortCode(length = 7): string {
const bytes = randomBytes(length);
let code = "";
for (let i = 0; i < length; i++) code += ALPHABET[bytes[i] % ALPHABET.length];
return code;
}
export function normalizeUrl(input: unknown): string | null {
if (typeof input !== "string") return null;
try {
const u = new URL(input);
if (u.protocol !== "http:" && u.protocol !== "https:") return null;
return u.toString();
} catch {
return null;
}
}
Bảy ký tự base62 cho khoảng 62⁷, tức hơn ba nghìn tỷ khả năng, đủ thừa và khó đoán. normalizeUrl chỉ chấp nhận http/https, chặn các scheme lạ như ftp: hay javascript:.
Handler tạo link đọc body JSON, validate, rồi trả về mã:
export const handler = async (
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> => {
let parsed: unknown;
try {
parsed = JSON.parse(event.body ?? "{}");
} catch {
return json(400, { error: "body khong phai JSON hop le" });
}
const target = normalizeUrl((parsed as Record<string, unknown>).url);
if (!target) {
return json(400, { error: "thieu hoac sai truong 'url' (chi nhan http/https)" });
}
const code = generateShortCode();
const base = `https://${event.requestContext.domainName}`;
return json(201, { code, shortUrl: `${base}/${code}`, target });
};
Handler resolve đọc code từ path parameter rồi trả về 301:
export const handler = async (
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> => {
const code = event.pathParameters?.code;
if (!code) return { statusCode: 400, body: "thieu code" };
const target = `https://example.com/demo-target-for/${code}`; // bai 06 thay bang tra cuu DB
return { statusCode: 301, headers: { location: target }, body: "" };
};
Về mã trạng thái chuyển hướng: 301 (Moved Permanently) báo cho trình duyệt và proxy rằng link này gắn cố định với đích, nên chúng được phép cache. Với URL shortener điều đó tốt cho hiệu năng nhưng có một đánh đổi về analytics, vì lượt mở lại từ cache có thể không chạm tới server. Bài về analytics sẽ cân nhắc lại giữa 301 và 302; ở đây ta dùng 301 để minh hoạ cơ chế.
Khai báo HTTP API trong SAM
Một tài nguyên AWS::Serverless::HttpApi định nghĩa API và cấu hình CORS; mỗi hàm gắn vào một route qua khối Events kiểu HttpApi:
Resources:
HttpApi:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowOrigins: ["*"]
AllowMethods: [GET, POST, OPTIONS]
AllowHeaders: [content-type]
CreateLinkFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: esbuild
BuildProperties:
EntryPoints: [handlers/create-link.ts]
Properties:
CodeUri: src
Handler: handlers/create-link.handler
Events:
Create:
Type: HttpApi
Properties:
ApiId: !Ref HttpApi
Path: /links
Method: POST
ResolveLinkFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: esbuild
BuildProperties:
EntryPoints: [handlers/resolve-link.ts]
Properties:
CodeUri: src
Handler: handlers/resolve-link.handler
Events:
Resolve:
Type: HttpApi
Properties:
ApiId: !Ref HttpApi
Path: /{code}
Method: GET
Path: /{code} khai báo một biến đường dẫn; API Gateway bắt phần đó và đưa vào event.pathParameters.code. CodeUri: src đặt gốc build ở src để các handler trong src/handlers import được hàm dùng chung trong src/lib.
client
│
▼
┌──────────────── HTTP API ($default stage) ─────────────────┐
│ POST /links ──────────────▶ CreateLinkFunction (Lambda) │
│ GET /{code} ──────────────▶ ResolveLinkFunction (Lambda)│
│ OPTIONS * ── CORS preflight, API Gateway tự trả lời ──┘
└─────────────────────────────────────────────────────────────┘
Gọi thử trên AWS
Sau sam build và sam deploy, output in ra URL gốc của API. POST một URL hợp lệ trả về 201 kèm mã:
$ curl -X POST "$API/links" -H 'content-type: application/json' \
-d '{"url":"https://docs.aws.amazon.com/lambda/latest/dg/welcome.html"}'
{"code":"BCczd3D","shortUrl":"https://iwx60qdop4.execute-api.ap-southeast-1.amazonaws.com/BCczd3D","target":"https://docs.aws.amazon.com/lambda/latest/dg/welcome.html"}
URL sai scheme bị chặn ngay với 400:
$ curl -X POST "$API/links" -H 'content-type: application/json' -d '{"url":"ftp://nope"}'
{"error":"thieu hoac sai truong 'url' (chi nhan http/https)"}
Mở một mã ngắn trả về 301 kèm header location (cờ -i để thấy header):
$ curl -i "$API/aK9xQ2z"
HTTP/2 301
location: https://example.com/demo-target-for/aK9xQ2z
API Gateway bắt aK9xQ2z vào path parameter, gọi handler, handler trả 301 và trình duyệt sẽ đi tới location.
CORS: ai trả lời preflight
Khi dashboard (chạy ở một origin khác) gọi API bằng JavaScript, trình duyệt gửi trước một request OPTIONS để hỏi xem origin của nó có được phép không. Đây là CORS preflight. Với cấu hình CorsConfiguration ở trên, chính API Gateway trả lời preflight, và request đó không chạm tới Lambda nào:
$ curl -i -X OPTIONS "$API/links" -H 'Origin: https://example.com' \
-H 'Access-Control-Request-Method: POST'
HTTP/2 204
access-control-allow-origin: *
access-control-allow-methods: GET,OPTIONS,POST
access-control-allow-headers: content-type
API Gateway trả 204 cùng các header Access-Control-Allow-*. Vì preflight được xử lý ở tầng gateway, handler của bạn không phải bận tâm tới nó, và cũng không bị tính một lần gọi Lambda cho mỗi preflight. Ở production, AllowOrigins: ["*"] nên thu hẹp lại đúng domain của dashboard; ta để * lúc đang dựng cho tiện, và sẽ siết ở bài bảo mật.
🧹 Dọn dẹp
$ sam delete --stack-name url-shortener --no-prompts --region ap-southeast-1
Deleted successfully
Cả HTTP API, hai hàm Lambda và các IAM role đi kèm biến mất cùng stack.
Tổng kết
Ta đã chọn HTTP API vì nó rẻ, đủ dùng, và có JWT authorizer gốc cho Cognito ở bài sau. Hai route đầu tiên đã chạy thật: tạo link với validate, và chuyển hướng 301 đọc từ path parameter. CORS preflight do API Gateway tự lo nên handler gọn và không tốn thêm lần gọi Lambda.
Mã ngắn sinh ra ở bài này chưa đi đâu cả, vì chưa có chỗ lưu. Bài sau lấp đúng khoảng đó: thiết kế bảng DynamoDB. Ta sẽ bắt đầu từ cách nghĩ ngược đời của DynamoDB, đi từ access pattern ra thiết kế bảng, thay vì từ bảng ra truy vấn như với cơ sở dữ liệu quan hệ.