API Routes & Route Handlers
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
route.tsFile defines GET/POST/PUT/DELETE endpoints- Zod Validates Request Bodies and Query Parameters
NextResponseResponse Formatting (json/redirect/rewrite)middleware.tsConfiguring matchers and request rewriting- CORS Cross-Origin Configuration and Rate Limiting Implementation
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.tsto centrally manage authentication, logging, and CORS—a single file controls the entire lifecycle of all requests.
// 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
// 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
// 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
// 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: ⭐⭐)
Output:
POST app/api/items/route.ts → Creates a new resource, returns created item with status 201.
// 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 })
}
Output:
GET app/api/todos/route.ts → Returns JSON data with status 200.
POST app/api/todos/route.ts → Creates a new resource, returns created item with status 201.
// 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
// 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
// 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: ⭐⭐)
Output:
GET app/api/items/route.ts → Validates query params with Zod, returns filtered results as JSON.
// 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 })
}
Output:
POST app/api/users/route.ts → Validates request body with Zod, creates resource, returns 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
// app/api/auth/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ status: 'healthy', uptime: process.uptime() })
}
(2) Redirect
// 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)
// 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: ⭐)
Output:
Middleware intercepts requests and rewrites URLs based on conditions.
// 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 })
}
}
Output:
GET app/api/response-demo/route.ts → Returns JSON data with status 200.
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.
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
// 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
// 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: ⭐)
Output:
Middleware intercepts requests and redirects based on conditions.
// 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*',
}
Output:
[${new Date(
7. CORS and Rate Limiting
(1) CORS Configuration
// 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
// 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
// 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
}
// 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: ⭐⭐⭐)
Output:
GET app/api/rate-limited/route.ts → Returns data as JSON. Rate-limited to prevent abuse.
// 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',
}})
}
Output:
POST app/api/secure/route.ts → Validates request body with Zod, creates resource, returns 201.
8. Complete Example: RESTful API Service
// 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 })
}
// 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 })
}
// 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
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.NextResponse.json and simply new Response()?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.middleware.ts read the database?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.upstash/ratelimit) to support state sharing across multiple instances.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
route.tsThe file defines RESTful endpoints using functions such as GET, POST, PUT, and DELETE- Zod validates the request body and query parameters, then returns a 400 status code and detailed error information
NextResponseSupports four response formats: json(), redirect(), next(), and rewrite()- middleware.ts handles authentication, logging, and CORS uniformly before requests reach pages or APIs
- The
matcherconfiguration controls the path matching scope of the middleware - Configuring CORS via response headers in middleware or route handlers
- Rate Limiting: Implemented using an in-memory map or Redis (Upstash Ratelimit is recommended for production environments)
📝 Exercises
-
Basic Exercise (⭐): Create
app/api/health/route.tsto return JSON health check information (status, timestamp, uptime). Add middleware to log each API call and its response time. -
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. -
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.



