Skip to main content

Next.js API Routes

Answer

API Routes let you create API endpoints directly within your Next.js application, eliminating the need for a separate backend server for simple APIs.

Pages Router API Routes

// pages/api/hello.js → /api/hello

export default function handler(req, res) {
res.status(200).json({ message: 'Hello World' });
}

// With method handling
export default function handler(req, res) {
if (req.method === 'GET') {
res.status(200).json({ users: [] });
} else if (req.method === 'POST') {
const user = req.body;
// Create user
res.status(201).json(user);
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

App Router Route Handlers

// app/api/hello/route.js → /api/hello

export async function GET(request) {
return Response.json({ message: "Hello World" });
}

export async function POST(request) {
const body = await request.json();
return Response.json(body, { status: 201 });
}

// Other methods: PUT, DELETE, PATCH, HEAD, OPTIONS

Request Object

// PAGES ROUTER
export default function handler(req, res) {
const { query, cookies, headers, body } = req;
const { id } = query;
}

// APP ROUTER
export async function GET(request) {
const searchParams = request.nextUrl.searchParams;
const id = searchParams.get("id");
const cookie = request.cookies.get("session");
const headerValue = request.headers.get("authorization");
}

Dynamic Routes

// PAGES ROUTER
// pages/api/users/[id].js
export default function handler(req, res) {
const { id } = req.query;
res.json({ id });
}

// APP ROUTER
// app/api/users/[id]/route.js
export async function GET(request, { params }) {
const { id } = params;
return Response.json({ id });
}

Catch-All Routes

// PAGES: pages/api/[...slug].js
// URL: /api/a/b/c → req.query.slug = ['a', 'b', 'c']

// APP: app/api/[...slug]/route.js
export async function GET(request, { params }) {
const { slug } = params; // ['a', 'b', 'c']
return Response.json({ slug });
}

Headers and Cookies

// APP ROUTER
import { headers, cookies } from "next/headers";

export async function GET() {
const headersList = headers();
const cookieStore = cookies();

const auth = headersList.get("authorization");
const session = cookieStore.get("session");

// Set cookies
cookies().set("name", "value", { httpOnly: true });

return new Response("OK");
}

// Response with custom headers
export async function GET() {
return new Response("OK", {
headers: {
"Content-Type": "application/json",
"Cache-Control": "max-age=60",
},
});
}

Error Handling

// Pages Router
export default async function handler(req, res) {
try {
const data = await fetchData();
res.status(200).json(data);
} catch (error) {
res.status(500).json({ error: "Internal Server Error" });
}
}

// App Router
export async function GET() {
try {
const data = await fetchData();
return Response.json(data);
} catch (error) {
return Response.json({ error: "Internal Server Error" }, { status: 500 });
}
}

Streaming Response

// App Router supports streaming
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(`Data ${i}\n`);
await new Promise((r) => setTimeout(r, 1000));
}
controller.close();
},
});

return new Response(stream, {
headers: { "Content-Type": "text/plain" },
});
}

CORS Configuration

// App Router
export async function GET() {
return Response.json(
{ data: "value" },
{
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
},
}
);
}

// Handle preflight
export async function OPTIONS() {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}

Common Patterns

// POST with form data
export async function POST(request) {
const formData = await request.formData();
const name = formData.get("name");
return Response.json({ name });
}

// File upload handling
export async function POST(request) {
const formData = await request.formData();
const file = formData.get("file");
const bytes = await file.arrayBuffer();
// Save bytes to storage
}

// Protected route
export async function GET(request) {
const token = request.headers.get("authorization")?.split(" ")[1];

if (!token || !validateToken(token)) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

return Response.json({ data: "protected" });
}

Key Points

  • Pages Router: pages/api/ with handler function
  • App Router: app/api/*/route.js with method exports
  • Dynamic routes work with [param] syntax
  • App Router uses Web standard Request/Response
  • No need for separate backend for simple APIs
  • Can access cookies, headers, and body