Astro กับ Upstash Redis: Caching และ Rate Limiting
#astro13 เม.ย. 2569
Astro กับ Upstash Redis: Caching และ Rate Limiting
Upstash เป็น serverless Redis ที่ออกแบบมาสำหรับ edge environments ด้วย HTTP-based API ทำให้ทำงานได้บน Cloudflare Workers, Vercel Edge, และ Deno Deploy ได้อย่างสมบูรณ์ บทความนี้จะแนะนำการใช้ Upstash Redis สำหรับ caching และ rate limiting ใน Astro
ทำไมต้องใช้ Upstash Redis?
- Serverless: ไม่ต้องจัดการ server
- Edge Compatible: ทำงานได้บน edge runtimes
- HTTP API: ไม่ต้องใช้ TCP connection
- Pay-per-use: จ่ายตามการใช้งานจริง
- Global Replication: ข้อมูลอยู่ใกล้ผู้ใช้
การติดตั้ง
npm install @upstash/redis @upstash/ratelimit
ตั้งค่า environment variables:
# .env
UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token
สร้าง Redis Client
// src/lib/redis.ts
import { Redis } from '@upstash/redis';
export const redis = new Redis({
url: import.meta.env.UPSTASH_REDIS_REST_URL,
token: import.meta.env.UPSTASH_REDIS_REST_TOKEN,
});
// Helper functions
export async function getCache<T>(key: string): Promise<T | null> {
return redis.get<T>(key);
}
export async function setCache<T>(
key: string,
value: T,
ttlSeconds = 3600
): Promise<void> {
await redis.setex(key, ttlSeconds, value);
}
export async function deleteCache(key: string): Promise<void> {
await redis.del(key);
}
export async function deleteCachePattern(pattern: string): Promise<void> {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
}
Caching API Responses
// src/pages/api/posts.ts
import type { APIRoute } from 'astro';
import { getCache, setCache } from '../../lib/redis';
import { getAllPosts } from '../../lib/posts';
export const GET: APIRoute = async ({ url }) => {
const page = url.searchParams.get('page') ?? '1';
const cacheKey = `posts:page:${page}`;
// ลองดึงจาก cache ก่อน
const cached = await getCache(cacheKey);
if (cached) {
return new Response(JSON.stringify(cached), {
headers: {
'Content-Type': 'application/json',
'X-Cache': 'HIT',
},
});
}
// ถ้าไม่มีใน cache ดึงจาก database
const data = await getAllPosts(parseInt(page));
// เก็บใน cache 5 นาที
await setCache(cacheKey, data, 300);
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'X-Cache': 'MISS',
},
});
};
Rate Limiting
// src/lib/rateLimit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { redis } from './redis';
// 10 requests per 10 seconds
export const apiRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true,
prefix: 'api',
});
// 5 requests per minute for auth
export const authRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(5, '60 s'),
analytics: true,
prefix: 'auth',
});
// 100 requests per hour for general
export const generalRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.tokenBucket(100, '1 h', 10),
analytics: true,
prefix: 'general',
});
Middleware สำหรับ Rate Limiting
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { apiRateLimit, authRateLimit } from './lib/rateLimit';
export const onRequest = defineMiddleware(async (context, next) => {
const { pathname } = context.url;
const ip =
context.request.headers.get('x-forwarded-for') ??
context.request.headers.get('x-real-ip') ??
'127.0.0.1';
// Rate limit สำหรับ auth endpoints
if (pathname.startsWith('/api/auth')) {
const { success, limit, remaining, reset } = await authRateLimit.limit(ip);
if (!success) {
return new Response(
JSON.stringify({
error: 'Too many requests',
retryAfter: Math.ceil((reset - Date.now()) / 1000),
}),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
},
}
);
}
}
// Rate limit สำหรับ API endpoints
if (pathname.startsWith('/api/')) {
const { success, remaining } = await apiRateLimit.limit(ip);
if (!success) {
return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), {
status: 429,
headers: { 'Content-Type': 'application/json' },
});
}
const response = await next();
response.headers.set('X-RateLimit-Remaining', remaining.toString());
return response;
}
return next();
});
Session Storage
// src/lib/session.ts
import { redis } from './redis';
import { nanoid } from 'nanoid';
interface Session {
userId: string;
email: string;
role: string;
createdAt: number;
}
export async function createSession(data: Omit<Session, 'createdAt'>): Promise<string> {
const sessionId = nanoid(32);
const session: Session = { ...data, createdAt: Date.now() };
await redis.setex(
`session:${sessionId}`,
7 * 24 * 60 * 60, // 7 days
JSON.stringify(session)
);
return sessionId;
}
export async function getSession(sessionId: string): Promise<Session | null> {
const data = await redis.get<string>(`session:${sessionId}`);
if (!data) return null;
return JSON.parse(data);
}
export async function deleteSession(sessionId: string): Promise<void> {
await redis.del(`session:${sessionId}`);
}
Pub/Sub สำหรับ Real-time Features
// src/lib/pubsub.ts
import { redis } from './redis';
export async function publishEvent(channel: string, data: unknown) {
await redis.publish(channel, JSON.stringify(data));
}
export async function incrementCounter(key: string): Promise<number> {
return redis.incr(key);
}
export async function getLeaderboard(
key: string,
limit = 10
): Promise<{ member: string; score: number }[]> {
const results = await redis.zrange(key, 0, limit - 1, {
rev: true,
withScores: true,
});
const leaderboard = [];
for (let i = 0; i < results.length; i += 2) {
leaderboard.push({
member: results[i] as string,
score: Number(results[i + 1]),
});
}
return leaderboard;
}
สรุป
Upstash Redis กับ Astro เป็นคู่ที่ยอดเยี่ยมสำหรับ serverless applications ด้วย HTTP-based API ที่ทำงานได้บน edge environments, caching ที่ช่วยลด database load, และ rate limiting ที่ป้องกัน abuse ทำให้ application มีทั้ง performance และ security ที่ดี