Route Handlers use standard Web Request and Response APIs, making your code portable and familiar. Next.js extends these with NextRequest and NextResponse for convenience features like cookies, redirects, and rewrites. Master request/response objects and you'll handle headers, authentication, content types, and streaming with confidence!
Request Object
Web Request API Basics
export async function GET(request: Request) {
// Request properties
console.log('Method:', request.method); // 'GET'
console.log('URL:', request.url); // Full URL
console.log('Headers:', request.headers); // Headers object
// Parse URL
const url = new URL(request.url);
console.log('Pathname:', url.pathname); // '/api/info'
console.log('Search:', url.search); // '?key=value'
console.log('Host:', url.host); // 'localhost:3000'
// Read headers
const contentType = request.headers.get('content-type');
const authorization = request.headers.get('authorization');
const userAgent = request.headers.get('user-agent');
return Response.json({
method: request.method,
url: request.url,
contentType,
userAgent,
});
}
// ✅ Standard Web Request
// ✅ Portable to any runtime
// ✅ Familiar APIReading Request Headers
export async function GET(request: Request) {
// Get single header
const authorization = request.headers.get('authorization');
if (!authorization) {
return Response.json(
{ error: 'Authorization header required' },
{ status: 401 }
);
}
// Parse Bearer token
const token = authorization.replace('Bearer ', '');
// Verify token
try {
const payload = await verifyToken(token);
return Response.json({ user: payload });
} catch (error) {
return Response.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
}
// Common headers:
// - 'authorization': Auth credentials
// - 'content-type': Body type
// - 'accept': Accepted response types
// - 'user-agent': Client info
// - 'referer': Source page
// - 'cookie': Cookies string
// ✅ headers.get(name) - case-insensitive
// ✅ Returns null if not found
// ✅ Always validate before useReading Request Body
export async function POST(request: Request) {
// Method 1: JSON body
const jsonBody = await request.json();
console.log('JSON:', jsonBody);
// Method 2: Form data
const formData = await request.formData();
console.log('Form field:', formData.get('fieldName'));
// Method 3: Plain text
const textBody = await request.text();
console.log('Text:', textBody);
// Method 4: Array buffer (binary)
const buffer = await request.arrayBuffer();
console.log('Buffer size:', buffer.byteLength);
// Method 5: Blob
const blob = await request.blob();
console.log('Blob type:', blob.type);
return Response.json({ success: true });
}
// ⚠️ Body can only be read ONCE
// Choose the right method for content-type:
// - application/json → request.json()
// - multipart/form-data → request.formData()
// - text/plain → request.text()
// - application/octet-stream → request.arrayBuffer()
// ✅ Each method is async
// ✅ Body stream consumed after readingRequest Body Streaming
export async function POST(request: Request) {
const reader = request.body?.getReader();
if (!reader) {
return Response.json(
{ error: 'No request body' },
{ status: 400 }
);
}
let totalBytes = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.length;
console.log(`Received ${totalBytes} bytes`);
// Process chunk...
}
return Response.json({
message: 'Upload complete',
totalBytes,
});
}
// ✅ Stream large uploads
// ✅ Process data in chunks
// ✅ Memory efficientNextRequest - Extended Request
NextRequest Features
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
// NextRequest extends Request with:
// 1. Cookies helper
const sessionCookie = request.cookies.get('session');
console.log('Session:', sessionCookie?.value);
// 2. URL helpers
console.log('Next URL:', request.nextUrl.pathname);
console.log('Search params:', request.nextUrl.searchParams.get('key'));
// 3. Geo location (when deployed)
console.log('Country:', request.geo?.country);
console.log('City:', request.geo?.city);
console.log('Latitude:', request.geo?.latitude);
// 4. IP address
console.log('IP:', request.ip);
// 5. All standard Request properties still available
console.log('Headers:', request.headers.get('user-agent'));
return Response.json({
pathname: request.nextUrl.pathname,
ip: request.ip,
country: request.geo?.country,
});
}
// ✅ NextRequest = Request + Next.js features
// ✅ Use when you need cookies/geo/IP
// ✅ Otherwise, Request is fineReading Cookies with NextRequest
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
// Get single cookie
const session = request.cookies.get('session');
if (!session) {
return Response.json(
{ error: 'Not authenticated' },
{ status: 401 }
);
}
console.log('Cookie name:', session.name); // 'session'
console.log('Cookie value:', session.value); // Token string
// Get all cookies
const allCookies = request.cookies.getAll();
console.log('All cookies:', allCookies);
// Check if cookie exists
const hasSession = request.cookies.has('session');
// Verify session
const user = await verifySession(session.value);
return Response.json({ user });
}
// ✅ request.cookies.get(name) - single cookie
// ✅ request.cookies.getAll() - all cookies
// ✅ request.cookies.has(name) - check existence
// ✅ Returns { name, value } objectNextURL Helpers
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
// NextURL is parsed URL with helpers
const { nextUrl } = request;
// Path info
console.log('Pathname:', nextUrl.pathname); // '/api/url-info'
console.log('Search:', nextUrl.search); // '?key=value'
console.log('Hash:', nextUrl.hash); // '#section'
// Search params (easier than new URL())
const key = nextUrl.searchParams.get('key');
const page = nextUrl.searchParams.get('page') || '1';
// Base URL
console.log('Origin:', nextUrl.origin); // 'http://localhost:3000'
console.log('Host:', nextUrl.host); // 'localhost:3000'
// Clone and modify
const redirectUrl = nextUrl.clone();
redirectUrl.pathname = '/new-path';
redirectUrl.searchParams.set('from', 'api');
return Response.json({
pathname: nextUrl.pathname,
params: Object.fromEntries(nextUrl.searchParams),
});
}
// ✅ nextUrl.pathname - current path
// ✅ nextUrl.searchParams - easy param access
// ✅ nextUrl.clone() - create modified copyResponse Object
Creating Responses
export async function GET(request: Request) {
// 1. JSON response (most common)
return Response.json({ message: 'Hello' });
// 2. JSON with status and headers
return Response.json(
{ error: 'Not found' },
{
status: 404,
headers: {
'X-Custom-Header': 'Value',
},
}
);
// 3. Plain text
return new Response('Hello, World!', {
status: 200,
headers: {
'Content-Type': 'text/plain',
},
});
// 4. HTML
return new Response('<h1>Hello</h1>', {
headers: {
'Content-Type': 'text/html',
},
});
// 5. Redirect (use NextResponse)
// return NextResponse.redirect(new URL('/path', request.url));
// 6. No content
return new Response(null, { status: 204 });
}
// ✅ Response.json() for JSON
// ✅ new Response() for other types
// ✅ Set status and headers in second argSetting Response Headers
export async function GET(request: Request) {
const fileContent = await getFileContent();
return new Response(fileContent, {
status: 200,
headers: {
// Content type
'Content-Type': 'application/pdf',
// Download filename
'Content-Disposition': 'attachment; filename="document.pdf"',
// Cache control
'Cache-Control': 'public, max-age=3600',
// CORS
'Access-Control-Allow-Origin': '*',
// Custom headers
'X-File-Size': fileContent.length.toString(),
'X-Generated-At': new Date().toISOString(),
},
});
}
// Common headers:
// - Content-Type: MIME type
// - Content-Disposition: Inline or attachment
// - Cache-Control: Caching directives
// - ETag: Cache validation
// - Location: Redirect target (with 3xx status)
// - Set-Cookie: Set cookies
// - Access-Control-*: CORS headers
// ✅ Headers object in second arg
// ✅ Case-insensitive names
// ✅ Values must be stringsContent Types
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const format = searchParams.get('format');
const data = {
message: 'Hello',
timestamp: new Date().toISOString(),
};
switch (format) {
case 'json':
return Response.json(data);
case 'xml':
const xml = `<?xml version="1.0"?>
<response>
<message>${data.message}</message>
<timestamp>${data.timestamp}</timestamp>
</response>`;
return new Response(xml, {
headers: { 'Content-Type': 'application/xml' },
});
case 'csv':
const csv = `message,timestamp
${data.message},${data.timestamp}`;
return new Response(csv, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': 'attachment; filename="data.csv"',
},
});
case 'text':
return new Response(`Message: ${data.message}`, {
headers: { 'Content-Type': 'text/plain' },
});
default:
return Response.json(data);
}
}
// GET /api/content?format=json
// GET /api/content?format=xml
// GET /api/content?format=csv
// ✅ Support multiple formats
// ✅ Set correct Content-Type
// ✅ Use format parameter or Accept headerNextResponse - Extended Response
NextResponse Features
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
// 1. JSON response (same as Response.json)
return NextResponse.json({ message: 'Hello' });
// 2. Redirect
return NextResponse.redirect(new URL('/new-path', request.url));
// 3. Rewrite (proxy to different URL)
return NextResponse.rewrite(new URL('/api/other', request.url));
// 4. Response with cookies
const response = NextResponse.json({ success: true });
response.cookies.set('name', 'value');
return response;
// 5. Next (pass to next middleware/handler)
return NextResponse.next();
}
// ✅ NextResponse = Response + Next.js features
// ✅ Use for redirects, rewrites, cookies
// ✅ Otherwise, Response works fineSetting Cookies
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const body = await request.json();
// Authenticate user
const user = await authenticate(body.email, body.password);
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Create session token
const sessionToken = await createSession(user.id);
// Create response
const response = NextResponse.json({
success: true,
user: {
id: user.id,
email: user.email,
},
});
// Set cookie
response.cookies.set('session', sessionToken, {
httpOnly: true, // Not accessible via JS
secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
sameSite: 'lax', // CSRF protection
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/', // Available site-wide
});
return response;
}
// Cookie options:
// - httpOnly: Prevent JavaScript access (security)
// - secure: HTTPS only (production)
// - sameSite: 'strict' | 'lax' | 'none' (CSRF protection)
// - maxAge: Seconds until expiration
// - expires: Specific expiration date
// - path: Where cookie is available
// - domain: Cookie domain
// ✅ Create response first
// ✅ Then set cookies
// ✅ httpOnly for security
// ✅ secure in productionDeleting Cookies
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const response = NextResponse.json({
success: true,
message: 'Logged out successfully',
});
// Method 1: Delete cookie
response.cookies.delete('session');
// Method 2: Set to expire immediately
response.cookies.set('session', '', {
maxAge: 0,
path: '/',
});
return response;
}
// ✅ response.cookies.delete(name)
// ✅ Or set maxAge: 0
// ✅ Match original pathRedirects
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const target = searchParams.get('target');
if (target === 'home') {
// Redirect to homepage
return NextResponse.redirect(new URL('/', request.url));
}
if (target === 'login') {
// Redirect to login with return URL
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('returnTo', request.url);
return NextResponse.redirect(loginUrl);
}
// External redirect
if (target === 'docs') {
return NextResponse.redirect('https://nextjs.org/docs');
}
// 301 Permanent redirect
return NextResponse.redirect(new URL('/default', request.url), 301);
}
// Redirect status codes:
// - 302 (default): Temporary redirect
// - 301: Permanent redirect
// - 307: Temporary redirect (preserve method)
// - 308: Permanent redirect (preserve method)
// ✅ Use new URL() for internal redirects
// ✅ String URL for external redirects
// ✅ Include status code if neededStreaming Responses
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
// Create readable stream
const stream = new ReadableStream({
async start(controller) {
// Send data in chunks
for (let i = 0; i < 10; i++) {
const chunk = `Chunk ${i}
`;
controller.enqueue(new TextEncoder().encode(chunk));
// Simulate delay
await new Promise(resolve => setTimeout(resolve, 500));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/plain',
'Transfer-Encoding': 'chunked',
},
});
}
// Use cases:
// - Large file downloads
// - Real-time data feeds
// - Server-sent events (SSE)
// - Progressive data loading
// ✅ Memory efficient
// ✅ Start sending before complete
// ✅ Better user experienceRequest/Response Structure
API routes using Request and Response features
Select a file or folder to see details
Advanced Request/Response Examples
Example 1: File Upload with Progress
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
);
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
return NextResponse.json(
{ error: `Invalid file type. Allowed: ${allowedTypes.join(', ')}` },
{ status: 400 }
);
}
// Validate file size (5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
return NextResponse.json(
{ error: 'File too large. Maximum 5MB' },
{ status: 400 }
);
}
// Convert to buffer
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// Save file
const filename = `${Date.now()}-${file.name}`;
await saveFile(filename, buffer);
// Save to database
const upload = await db.uploads.create({
data: {
filename,
originalName: file.name,
mimeType: file.type,
size: file.size,
url: `/uploads/${filename}`,
},
});
return NextResponse.json(upload, { status: 201 });
} catch (error) {
console.error('Upload error:', error);
return NextResponse.json(
{ error: 'Upload failed' },
{ status: 500 }
);
}
}
// ✅ FormData for files
// ✅ Validate type and size
// ✅ Save metadata to databaseExample 2: API with Authentication
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
// Check session cookie
const sessionCookie = request.cookies.get('session');
if (!sessionCookie) {
return NextResponse.json(
{ error: 'Unauthorized - No session' },
{ status: 401 }
);
}
// Verify session
try {
const session = await verifySession(sessionCookie.value);
if (!session || session.expired) {
return NextResponse.json(
{ error: 'Unauthorized - Invalid session' },
{ status: 401 }
);
}
// Get user data
const user = await db.users.findUnique({
where: { id: session.userId },
});
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
// Return protected data
return NextResponse.json({
user: {
id: user.id,
email: user.email,
name: user.name,
},
session: {
createdAt: session.createdAt,
expiresAt: session.expiresAt,
},
});
} catch (error) {
console.error('Auth error:', error);
return NextResponse.json(
{ error: 'Authentication failed' },
{ status: 401 }
);
}
}
// ✅ Read session cookie
// ✅ Verify session validity
// ✅ Return user data if authenticated
// ✅ Proper error responsesExample 3: Content Negotiation
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const data = await getData();
// Check Accept header
const accept = request.headers.get('accept') || '';
// JSON (default)
if (accept.includes('application/json') || !accept) {
return NextResponse.json(data);
}
// XML
if (accept.includes('application/xml')) {
const xml = convertToXML(data);
return new Response(xml, {
headers: { 'Content-Type': 'application/xml' },
});
}
// CSV
if (accept.includes('text/csv')) {
const csv = convertToCSV(data);
return new Response(csv, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': 'attachment; filename="data.csv"',
},
});
}
// Unsupported format
return NextResponse.json(
{ error: 'Unsupported format. Use JSON, XML, or CSV' },
{ status: 406 }
);
}
// ✅ Check Accept header
// ✅ Return requested format
// ✅ 406 for unsupported formatsRequest/Response Best Practices
1. Always Validate Headers
// ✅ GOOD: Check for null
const auth = request.headers.get('authorization');
if (!auth) {
return Response.json({ error: 'Missing auth' }, { status: 401 });
}
// Validate format
if (!auth.startsWith('Bearer ')) {
return Response.json({ error: 'Invalid auth format' }, { status: 401 });
}
// ❌ BAD: Assume header exists
const auth = request.headers.get('authorization');
const token = auth.replace('Bearer ', ''); // Crashes if null!2. Use Proper Cookie Settings
// ✅ GOOD: Secure cookie settings
response.cookies.set('session', token, {
httpOnly: true, // Prevent XSS
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 86400, // 1 day
path: '/',
});
// ❌ BAD: Insecure settings
response.cookies.set('session', token); // No security options!3. Set Correct Content-Type
// ✅ GOOD: Explicit Content-Type
return new Response(xmlData, {
headers: { 'Content-Type': 'application/xml' },
});
// ✅ Response.json() sets Content-Type automatically
return Response.json(data); // Content-Type: application/json
// ❌ BAD: Wrong or missing Content-Type
return new Response(jsonString); // No Content-Type header!4. Use Absolute URLs for Redirects
// ✅ GOOD: Absolute URL
return NextResponse.redirect(new URL('/path', request.url));
// ✅ GOOD: Full URL for external
return NextResponse.redirect('https://example.com');
// ❌ BAD: Relative path
return NextResponse.redirect('/path'); // Error!5. Handle CORS Properly
// ✅ GOOD: CORS headers
export async function GET(request: Request) {
const data = await getData();
return NextResponse.json(data, {
headers: {
'Access-Control-Allow-Origin': 'https://trusted-site.com',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
// Handle preflight
export async function OPTIONS(request: Request) {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': 'https://trusted-site.com',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}Key Takeaways
- Request vs NextRequest - NextRequest adds cookies, geo, IP
- Response vs NextResponse - NextResponse adds cookies, redirects
- request.headers.get() - returns null if missing
- request.json() - parse JSON body (async)
- response.cookies.set() - set cookies with options
- NextResponse.redirect() - redirect with absolute URL
- Content-Type header - set for non-JSON responses
- httpOnly cookies - security best practice
What's Next?
You've mastered Request and Response objects! Next, we'll explore API Error Handling and Status Codes—building robust error responses, using appropriate HTTP status codes, creating consistent error formats, implementing try-catch patterns, and handling edge cases gracefully. You'll build production-ready APIs!
We'll cover error response patterns, status code selection, validation errors, database errors, and comprehensive error handling strategies.
🔒 Security Reminder
Always use httpOnly cookies for sessions, enable secure flag in production, validate all headers before use, set proper CORS headers, and never expose sensitive data in error messages. Security is non-negotiable!