404 Not Found

404 Not Found


nginx

API Routes and Route Handlers: Building RESTful Endpoints

Route Handlers form the API layer of Next.js—when Server Actions aren’t enough, standard RESTful endpoints remain the cornerstone of the modern web.

1. What You'll Learn


2. A True Story of a DevOps Engineer

(1) Pain Point: The team has implemented the authentication logic eight times across five pages.

While reviewing the TaskFlow code, Diana noticed that every API route began with the same 15 lines of authentication code—parsing the session from the cookie, validating the token, and returning a 401 response. Eight API endpoints × 15 lines = 120 lines of duplicate code. To make matters worse, three endpoints lacked authentication checks, directly exposing user data to requests from unauthenticated users.

Question Data
Duplicate Authentication Code 120 lines (8 endpoints × 15 lines)
Unprotected endpoints 3
Security audit failed 2 times
Repair Time 4 hours per session

(2) The Middleware Solution

Use middleware.ts to centrally manage authentication, logging, and CORS—a single file controls the entire lifecycle of all requests.

TS
// middleware.ts — Single Sign-On + Log
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session-token')?.value

  // Inspection API Request Authentication
  if (request.nextUrl.pathname.startsWith('/api/')) {
    if (!token) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
  }

  // Add a safety helmet
  const response = NextResponse.next()
  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('X-Content-Type-Options', 'nosniff')

  return response
}

export const config = {
  matcher: '/api/:path*',
}

(3) Revenue

Dimension Before (without middleware) After (with middleware)
Verification Code 120 lines (spaced) 15 lines (condensed)
Unprotected endpoints 3 0
Security Audit ❌ Failed ✅ Passed
New endpoint workload 15 lines of authentication + logic Logic only

3. Route Handlers Basics

Route Handlers are defined in the app/api/**/route.ts file, with one exported asynchronous function corresponding to each HTTP method:

HTTP Method Exported Function Route File
GET export async function GET() app/api/items/route.ts
POST export async function POST() app/api/items/route.ts
PUT export async function PUT() app/api/items/[id]/route.ts
DELETE export async function DELETE() app/api/items/[id]/route.ts
PATCH export async function PATCH() app/api/items/[id]/route.ts

(1) Basic GET Endpoint

TS
// app/api/items/route.ts
import { NextResponse } from 'next/server'

export async function GET() {
  const items = await db.item.findMany()
  return NextResponse.json(items)
}

(2) Dynamic Routing Parameters

TS
// app/api/items/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const item = await db.item.findUnique({ where: { id: Number(params.id) } })
  if (!item) {
    return NextResponse.json({ error: 'Item not found' }, { status: 404 })
  }
  return NextResponse.json(item)
}

(3) POST to Create a Resource

TS
// app/api/items/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const body = await request.json()
  const item = await db.item.create({ data: body })
  return NextResponse.json(item, { status: 201 })
}

▶ Example: Complete CRUD Route Handler (Difficulty: ⭐⭐)

TS
// app/api/todos/route.ts — GET + POST
import { NextRequest, NextResponse } from 'next/server'

const API = 'https://jsonplaceholder.typicode.com'

export async function GET() {
  const todos = await fetch(`${API}/todos?_limit=5`).then(r => r.json())
  return NextResponse.json(todos)
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  const todo = await fetch(`${API}/todos`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  }).then(r => r.json())
  return NextResponse.json(todo, { status: 201 })
}
TS
// app/api/todos/[id]/route.ts — GET + PUT + DELETE
import { NextRequest, NextResponse } from 'next/server'

const API = 'https://jsonplaceholder.typicode.com'

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const todo = await fetch(`${API}/todos/${params.id}`).then(r => r.json())
  if (!todo || todo.id === undefined) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 })
  }
  return NextResponse.json(todo)
}

export async function PUT(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const body = await request.json()
  const updated = await fetch(`${API}/todos/${params.id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  }).then(r => r.json())
  return NextResponse.json(updated)
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  await fetch(`${API}/todos/${params.id}`, { method: 'DELETE' })
  return NextResponse.json({ success: true })
}

4. Zod Request Validation

For validating request data in REST APIs, we also recommend using Zod—it’s more concise and type-safe than manual if checks.

(1) Request Body Validation

TS
// app/api/items/route.ts — Zod Verification POST Request Body
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

const createItemSchema = z.object({
  name: z.string().min(2).max(100),
  price: z.number().positive('Price must be positive'),
  category: z.enum(['electronics', 'clothing', 'food']),
  tags: z.array(z.string()).max(5).optional(),
})

export async function POST(request: NextRequest) {
  const body = await request.json()
  const validated = createItemSchema.safeParse(body)

  if (!validated.success) {
    return NextResponse.json(
      { error: 'Validation failed', details: validated.error.flatten().fieldErrors },
      { status: 400 }
    )
  }

  const item = await db.item.create({ data: validated.data })
  return NextResponse.json(item, { status: 201 })
}

(2) Query Parameter Validation

TS
// app/api/items/route.ts — Zod Validate Query Parameters
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

const querySchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(10),
  search: z.string().optional(),
})

export async function GET(request: NextRequest) {
  const { searchParams } = request.nextUrl
  const query = querySchema.safeParse({
    page: searchParams.get('page'),
    limit: searchParams.get('limit'),
    search: searchParams.get('search'),
  })

  if (!query.success) {
    return NextResponse.json(
      { error: 'Invalid query parameters', details: query.error.flatten().fieldErrors },
      { status: 400 }
    )
  }

  const { page, limit, search } = query.data
  const items = await db.item.findMany({
    skip: (page - 1) * limit,
    take: limit,
    where: search ? { name: { contains: search } } : undefined,
  })
  return NextResponse.json({ items, page, limit })
}

▶ Example: Zod Validation Complete API (Difficulty: ⭐⭐)

TS
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { revalidateTag } from 'next/cache'

const userSchema = z.object({
  name: z.string().min(2, 'Name too short'),
  email: z.string().email('Invalid email'),
  role: z.enum(['admin', 'user', 'viewer']).default('user'),
})

export async function POST(request: NextRequest) {
  const body = await request.json()
  const validated = userSchema.safeParse(body)

  if (!validated.success) {
    return NextResponse.json(
      { error: 'Validation failed', fields: validated.error.flatten().fieldErrors },
      { status: 400 }
    )
  }

  const user = await fetch('https://jsonplaceholder.typicode.com/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(validated.data),
  }).then(r => r.json())

  revalidateTag('users')
  return NextResponse.json(user, { status: 201 })
}

5. NextResponse Response Format

NextResponse is a dedicated response tool provided by Next.js that supports multiple response formats:

Method Purpose Example
NextResponse.json(data, opts?) JSON Response NextResponse.json({ id: 1 }, { status: 201 })
NextResponse.redirect(url) Redirect NextResponse.redirect(new URL('/login', request.url))
NextResponse.next() Continue the middleware chain return NextResponse.next()
NextResponse.rewrite(url) Server-Side URL Rewriting NextResponse.rewrite(new URL('/fallback', request.url))

(1) JSON Response

TS
// app/api/auth/route.ts
import { NextResponse } from 'next/server'

export async function GET() {
  return NextResponse.json({ status: 'healthy', uptime: process.uptime() })
}

(2) Redirect

TS
// app/api/redirect/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const target = request.nextUrl.searchParams.get('to') ?? '/'
  return NextResponse.redirect(new URL(target, request.url))
}

(3) Rewrite (URL Rewriting)

TS
// middleware.ts — Rewrite /products/old-slug to a new path
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/products/old-')) {
    const newPath = request.nextUrl.pathname.replace('/old-', '/')
    return NextResponse.rewrite(new URL(newPath, request.url))
  }
  return NextResponse.next()
}

▶ Example: Comparison of Response Formats (Difficulty: ⭐)

TS
// app/api/response-demo/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const format = request.nextUrl.searchParams.get('format') ?? 'json'

  switch (format) {
    case 'json':
      return NextResponse.json({ message: 'Hello', timestamp: Date.now() })
    case 'redirect':
      return NextResponse.redirect(new URL('/api/response-demo?format=json', request.url))
    case 'rewrite':
      return NextResponse.rewrite(new URL('/api/response-demo?format=json', request.url))
    default:
      return NextResponse.json({ error: 'Unknown format' }, { status: 400 })
  }
}

6. Middleware

middleware.ts is a request interceptor for Next.js—it runs before each request reaches a page or API, and supports path matching, request rewriting, header injection, and authentication checks.

100%
sequenceDiagram
    participant Client as Browser
    participant MW as middleware.ts
    participant Route as Route Handler/Page

    Client->>MW: Request /api/todos
    MW->>MW: Match matcher Rules
    MW->>MW: Certification Inspection / Header Inject
    alt Through
        MW->>Route: NextResponse.next()
        Route-->>Client: Normal Response
    else Not verified
        MW-->>Client: NextResponse.json(401)
    else Redirect
        MW-->>Client: NextResponse.redirect(/login)
    end
Configuration Type Description
matcher string[] Match path pattern (supports glob)
request.nextUrl URL The URL object for the current request
request.cookies Map Request cookies
request.headers Headers Request Headers
NextResponse.next() Response Continue processing as usual
NextResponse.redirect() Response Redirect to another URL

(1) Matcher Configuration

TS
// middleware.ts — matcher Layout
export const config = {
  matcher: [
    '/api/:path*',           // All API Routing
    '/dashboard/:path*',     // All dashboard Page
    '/((?!_next|static|favicon.ico).*)',  // Exclude static resources
  ],
}

(2) Common Middleware Patterns

TS
// middleware.ts — Summary of Common Patterns
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  // 1. Certification Inspection
  const token = request.cookies.get('session')?.value
  const isAuthPage = pathname.startsWith('/login') || pathname.startsWith('/register')

  if (!token && !isAuthPage && pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // 2. Safety Head
  const response = NextResponse.next()
  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('X-Content-Type-Options', 'nosniff')
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')

  // 3. Add request ID(Track)
  const requestId = crypto.randomUUID()
  response.headers.set('X-Request-Id', requestId)

  return response
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
}

▶ Example: Middleware Logging (Difficulty: ⭐)

TS
// middleware.ts — Request Logs
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const start = Date.now()
  const { method, nextUrl } = request

  console.log(`[${new Date().toISOString()}] ${method} ${nextUrl.pathname}`)

  const response = NextResponse.next()

  // Asynchronous Response Time Logging
  response.headers.set('X-Response-Time', `${Date.now() - start}ms`)

  return response
}

export const config = {
  matcher: '/api/:path*',
}

7. CORS and Rate Limiting

(1) CORS Configuration

TS
// app/api/cors-config/route.ts — For a single endpoint CORS
import { NextRequest, NextResponse } from 'next/server'

const corsHeaders = {
  'Access-Control-Allow-Origin': 'https://your-app.com',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  'Access-Control-Max-Age': '86400',
}

export async function OPTIONS() {
  return NextResponse.json({}, { headers: corsHeaders })
}

export async function GET() {
  return NextResponse.json(
    { data: 'CORS enabled' },
    { headers: corsHeaders }
  )
}

(2) Global CORS Middleware

TS
// middleware.ts — Global CORS
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/')) {
    const response = NextResponse.next()
    response.headers.set('Access-Control-Allow-Origin', '*')
    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
    response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')

    // Pre-inspection requests are returned immediately
    if (request.method === 'OPTIONS') {
      return new Response(null, { status: 204, headers: response.headers })
    }

    return response
  }
  return NextResponse.next()
}

export const config = {
  matcher: '/api/:path*',
}

(3) Rate Limiting

TS
// lib/rate-limit.ts — Simple Memory Rate Limiter
const rateMap = new Map<string, { count: number; resetAt: number }>()

export function rateLimit(key: string, maxRequests: number, windowMs: number): boolean {
  const now = Date.now()
  const record = rateMap.get(key)

  if (!record || now > record.resetAt) {
    rateMap.set(key, { count: 1, resetAt: now + windowMs })
    return true  // Allow
  }

  if (record.count >= maxRequests) {
    return false  // Restrictions
  }

  record.count++
  return true
}
TS
// app/api/rate-limited/route.ts — Usage Rate Limiter
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'

export async function GET(request: NextRequest) {
  const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
  const allowed = rateLimit(ip, 10, 60_000)  // 10 per minute

  if (!allowed) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429, headers: { 'Retry-After': '60' } }
    )
  }

  return NextResponse.json({ message: 'Success', timestamp: Date.now() })
}

▶ Example: Complete CORS + Rate Limiting (Difficulty: ⭐⭐⭐)

TS
// app/api/secure/route.ts — CORS + Rate Limit + Auth
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
import { z } from 'zod'

const postSchema = z.object({
  title: z.string().min(2).max(200),
  content: z.string().min(10),
})

export async function POST(request: NextRequest) {
  // 1. Rate Limit
  const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
  if (!rateLimit(ip, 20, 60_000)) {
    return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 })
  }

  // 2. Auth check
  const auth = request.headers.get('authorization')
  if (!auth?.startsWith('Bearer ') || auth.slice(7) !== process.env.API_KEY) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // 3. Validation
  const body = await request.json()
  const validated = postSchema.safeParse(body)
  if (!validated.success) {
    return NextResponse.json(
      { error: 'Validation failed', details: validated.error.flatten().fieldErrors },
      { status: 400 }
    )
  }

  // 4. Process
  const post = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    body: JSON.stringify({ ...validated.data, userId: 1 }),
  }).then(r => r.json())

  return NextResponse.json(post, { status: 201, headers: {
    'Access-Control-Allow-Origin': '*',
  }})
}

export async function OPTIONS() {
  return NextResponse.json({}, { headers: {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  }})
}

8. Complete Example: RESTful API Service

TS
// app/api/posts/route.ts — Posts CRUD API
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { revalidateTag } from 'next/cache'

// ======== Schemas ========
const createPostSchema = z.object({
  title: z.string().min(2).max(200),
  body: z.string().min(10).max(5000),
  userId: z.number().int().positive(),
})

const querySchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(50).default(10),
})

const API = 'https://jsonplaceholder.typicode.com'

// ======== GET /api/posts?page=1&limit=10 ========
export async function GET(request: NextRequest) {
  const query = querySchema.safeParse({
    page: request.nextUrl.searchParams.get('page'),
    limit: request.nextUrl.searchParams.get('limit'),
  })

  if (!query.success) {
    return NextResponse.json(
      { error: 'Invalid query', details: query.error.flatten().fieldErrors },
      { status: 400 }
    )
  }

  const { page, limit } = query.data
  const posts = await fetch(`${API}/posts?_page=${page}&_limit=${limit}`).then(r => r.json())
  const total = 100  // JSONPlaceholder Total

  return NextResponse.json({
    data: posts,
    pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }
  })
}

// ======== POST /api/posts ========
export async function POST(request: NextRequest) {
  const body = await request.json()
  const validated = createPostSchema.safeParse(body)

  if (!validated.success) {
    return NextResponse.json(
      { error: 'Validation failed', fields: validated.error.flatten().fieldErrors },
      { status: 400 }
    )
  }

  const post = await fetch(`${API}/posts`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(validated.data),
  }).then(r => r.json())

  revalidateTag('posts')
  return NextResponse.json(post, { status: 201 })
}
TS
// app/api/posts/[id]/route.ts — Single Article CRUD
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

const updatePostSchema = z.object({
  title: z.string().min(2).max(200).optional(),
  body: z.string().min(10).max(5000).optional(),
})

const API = 'https://jsonplaceholder.typicode.com'

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const post = await fetch(`${API}/posts/${params.id}`).then(r => {
    if (!r.ok) throw new Error('Not found')
    return r.json()
  }).catch(() => null)

  if (!post) {
    return NextResponse.json({ error: 'Post not found' }, { status: 404 })
  }
  return NextResponse.json(post)
}

export async function PATCH(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const body = await request.json()
  const validated = updatePostSchema.safeParse(body)

  if (!validated.success) {
    return NextResponse.json(
      { error: 'Validation failed', fields: validated.error.flatten().fieldErrors },
      { status: 400 }
    )
  }

  const updated = await fetch(`${API}/posts/${params.id}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(validated.data),
  }).then(r => r.json())

  return NextResponse.json(updated)
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  await fetch(`${API}/posts/${params.id}`, { method: 'DELETE' })
  return NextResponse.json({ success: true, id: params.id })
}
TS
// middleware.ts — Global API Middleware
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'

export function middleware(request: NextRequest) {
  const { pathname, origin } = request.nextUrl

  if (!pathname.startsWith('/api/')) {
    return NextResponse.next()
  }

  // 1. Rate Limiting
  const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
  if (!rateLimit(ip, 30, 60_000)) {
    return NextResponse.json({ error: 'Too many requests' }, {
      status: 429,
      headers: { 'Retry-After': '60' }
    })
  }

  // 2. Logging
  console.log(`[API] ${request.method} ${pathname}`)

  // 3. CORS
  const response = NextResponse.next()
  response.headers.set('Access-Control-Allow-Origin', '*')
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key')

  if (request.method === 'OPTIONS') {
    return new Response(null, { status: 204, headers: response.headers })
  }

  return response
}

export const config = {
  matcher: '/api/:path*',
}

❓ FAQ

Q What is the difference between a Route Handler and a Server Action?
A A Route Handler is a standard HTTP endpoint defined using route.ts, suitable for third-party API integration, webhooks, and mobile calls. A Server Action is an RPC-style server-side function defined using 'use server', suitable for first-party form operations. The key differences are: Route Handlers require HTTP calls, while Server Actions are function calls; Route Handlers do not include CSRF protection, whereas Server Actions have it built-in.
Q What is the difference between NextResponse.json and simply new Response()?
A NextResponse.json is a convenience method of NextResponse that automatically sets Content-Type: application/json and provides better TypeScript type inference. new Response(JSON.stringify(data), { headers: {'Content-Type': 'application/json'} }) is equivalent. We recommend using NextResponse.json to keep your code concise.
Q Can middleware.ts read the database?
A Yes, but be mindful of performance—middleware runs for every matched request. Database queries increase latency. It is recommended to perform only lightweight operations in middleware (such as parsing cookies, checking headers, and redirecting). Database operations should be placed in route handlers or server actions.
Q Does Route Handler support Edge Runtime?
A Yes, it does. By default, Route Handler runs on the Node.js Runtime, but you can switch to the Edge Runtime via export const runtime = 'edge'. The Edge Runtime offers lower latency but has limited API support (it does not support native Node.js modules such as fs and crypto). The Edge Runtime is suitable for simple proxying, authentication verification, and A/B testing.
Q How can I protect API endpoints from abuse?
A Three layers of protection: ① IP-based rate limiting at the middleware layer; ② API key/JWT authentication at the route handler layer; ③ Global CORS restrictions on allowed origins. In production environments, we recommend using a Redis-backed rate limiter (such as upstash/ratelimit) to support state sharing across multiple instances.
Q How do I retrieve the client's IP address in a Route Handler?
A Use request.headers to retrieve it: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() or request.headers.get('x-real-ip'). Vercel automatically adds these headers during deployment. When developing locally, it may return ::1 or 127.0.0.1.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Create app/api/health/route.ts to return JSON health check information (status, timestamp, uptime). Add middleware to log each API call and its response time.

  2. Advanced Exercise (⭐⭐): Build a complete CRUD API: app/api/books/route.ts (GET to list + POST to create) + app/api/books/[id]/route.ts (GET to view details + PUT to update + DELETE to delete). Use Zod to validate the request body. Add API key authentication in the middleware.

  3. Challenge (⭐⭐⭐): Implement a "short URL service" API: app/api/shorten/route.ts (POST accepts a URL and returns a short URL), app/api/[code]/route.ts (GET retrieves a short URL and performs a 302 redirect). Add rate limiting (10 short links per minute per IP), CORS support, and middleware access logs. Store short codes in a JSON file or an in-memory map.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏