Full-Stack Routes: API Routes in App Router
The App Router turns your Next.js project into a full-stack framework. API routes are functions that handle HTTP requests and return responses—typically JSON. Create a file at app/api/posts/route.ts, export a POST function, and you have a JSON API endpoint at /api/posts. No separate backend server, no DevOps—just TypeScript functions deployed alongside your React pages. This article covers building REST APIs, handling requests, and integrating them with your frontend.
Building Your First API Route
An API route is a route.ts file that exports HTTP method functions. Here's a simple endpoint:
// app/api/hello/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
return NextResponse.json({ message: "Hello, World!" });
}
When you visit http://localhost:3000/api/hello, Next.js calls the GET function and returns the JSON response. In your browser:
{ "message": "Hello, World!" }
Handling Different HTTP Methods
Export functions for each HTTP method your endpoint supports:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
// GET /api/posts — list all posts
export async function GET(request: NextRequest) {
const posts = [
{ id: 1, title: "React Hooks", author: "Jane" },
{ id: 2, title: "Next.js Guide", author: "John" },
];
return NextResponse.json(posts);
}
// POST /api/posts — create a new post
export async function POST(request: NextRequest) {
const body = await request.json();
const newPost = {
id: 3,
title: body.title,
author: body.author,
};
return NextResponse.json(newPost, { status: 201 });
}
// PUT /api/posts — update (less common for single endpoint)
export async function PUT(request: NextRequest) {
return NextResponse.json({ error: "Use /api/posts/[id] for updates" });
}
// DELETE /api/posts — delete (less common)
export async function DELETE(request: NextRequest) {
return NextResponse.json(
{ error: "Use /api/posts/[id] to delete specific posts" },
{ status: 405 }
);
}
Your API now supports:
GET /api/posts→ list all postsPOST /api/posts→ create a postPUT /api/posts→ not implemented (405 error)DELETE /api/posts→ not implemented (405 error)
Dynamic API Routes
Use dynamic segments [id] to handle individual resources:
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
// Mock database
const posts: Record<string, { id: number; title: string; author: string }> = {
"1": { id: 1, title: "React Hooks", author: "Jane" },
"2": { id: 2, title: "Next.js Guide", author: "John" },
};
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = posts[params.id];
if (!post) {
return NextResponse.json({ error: "Post not found" }, { status: 404 });
}
return NextResponse.json(post);
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = posts[params.id];
if (!post) {
return NextResponse.json({ error: "Post not found" }, { status: 404 });
}
const body = await request.json();
post.title = body.title || post.title;
post.author = body.author || post.author;
return NextResponse.json(post);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = posts[params.id];
if (!post) {
return NextResponse.json({ error: "Post not found" }, { status: 404 });
}
delete posts[params.id];
return NextResponse.json({ message: "Post deleted" });
}
Now you have:
GET /api/posts/1→ get post 1PUT /api/posts/1→ update post 1DELETE /api/posts/1→ delete post 1
Reading Request Data
URL Query Parameters
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const query = searchParams.get("q");
const limit = searchParams.get("limit") || "10";
return NextResponse.json({
search: query,
limit: parseInt(limit),
});
}
// GET /api/search?q=react&limit=5
// Response: { search: "react", limit: 5 }
JSON Body (POST/PUT)
export async function POST(request: NextRequest) {
const body = await request.json();
return NextResponse.json({
received: body,
});
}
// POST /api/posts with body: { "title": "My Post", "author": "Jane" }
// Response: { received: { title: "My Post", author: "Jane" } }
Form Data
export async function POST(request: NextRequest) {
const formData = await request.formData();
const title = formData.get("title");
const author = formData.get("author");
return NextResponse.json({
title,
author,
});
}
Headers
export async function GET(request: NextRequest) {
const authHeader = request.headers.get("authorization");
if (!authHeader) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
return NextResponse.json({ authorized: true });
}
Building a Complete CRUD API
Let's build a posts API with Create, Read, Update, Delete operations:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
interface Post {
id: number;
title: string;
content: string;
author: string;
createdAt: string;
}
let posts: Post[] = [
{
id: 1,
title: "Getting Started with React",
content: "React is a JavaScript library...",
author: "Jane",
createdAt: "2026-06-01T10:00:00Z",
},
];
let nextId = 2;
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const authorFilter = searchParams.get("author");
const filtered = authorFilter
? posts.filter((p) => p.author.toLowerCase() === authorFilter.toLowerCase())
: posts;
return NextResponse.json(filtered);
}
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.title || !body.content || !body.author) {
return NextResponse.json(
{ error: "Missing required fields: title, content, author" },
{ status: 400 }
);
}
const newPost: Post = {
id: nextId++,
title: body.title,
content: body.content,
author: body.author,
createdAt: new Date().toISOString(),
};
posts.push(newPost);
return NextResponse.json(newPost, { status: 201 });
}
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
let posts: any[] = [
{
id: 1,
title: "Getting Started",
content: "...",
author: "Jane",
createdAt: "2026-06-01T10:00:00Z",
},
];
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = posts.find((p) => p.id === parseInt(params.id));
if (!post) {
return NextResponse.json({ error: "Post not found" }, { status: 404 });
}
return NextResponse.json(post);
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = posts.find((p) => p.id === parseInt(params.id));
if (!post) {
return NextResponse.json({ error: "Post not found" }, { status: 404 });
}
const body = await request.json();
post.title = body.title || post.title;
post.content = body.content || post.content;
post.author = body.author || post.author;
return NextResponse.json(post);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const index = posts.findIndex((p) => p.id === parseInt(params.id));
if (index === -1) {
return NextResponse.json({ error: "Post not found" }, { status: 404 });
}
const deleted = posts.splice(index, 1)[0];
return NextResponse.json({ message: "Post deleted", deleted });
}
Using the API from React
Call your API from Client Components using fetch:
// app/posts/page.tsx
"use client";
import { useState, useEffect } from "react";
export default function PostsPage() {
const [posts, setPosts] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [title, setTitle] = useState("");
const [author, setAuthor] = useState("");
useEffect(() => {
fetch("/api/posts")
.then((res) => res.json())
.then((data) => {
setPosts(data);
setLoading(false);
});
}, []);
const createPost = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title,
author,
content: "New post content",
}),
});
const newPost = await res.json();
setPosts([...posts, newPost]);
setTitle("");
setAuthor("");
};
if (loading) return <div>Loading...</div>;
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-4xl font-bold mb-8">Posts</h1>
<form onSubmit={createPost} className="mb-8 p-4 border rounded">
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-4 py-2 mb-4 border rounded"
/>
<input
type="text"
placeholder="Author"
value={author}
onChange={(e) => setAuthor(e.target.value)}
className="w-full px-4 py-2 mb-4 border rounded"
/>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Create Post
</button>
</form>
<ul className="space-y-4">
{posts.map((post) => (
<li key={post.id} className="p-4 border rounded">
<h2 className="font-bold">{post.title}</h2>
<p className="text-sm text-gray-600">By {post.author}</p>
</li>
))}
</ul>
</div>
);
}
Response Status Codes
Always return appropriate status codes:
| Code | Meaning | Use Case |
|---|---|---|
| 200 | OK | Successful GET, PUT |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid data |
| 401 | Unauthorized | Missing auth |
| 404 | Not Found | Resource doesn't exist |
| 500 | Server Error | Unexpected error |
return NextResponse.json(data, { status: 201 });
return NextResponse.json({ error: "..." }, { status: 400 });
Error Handling
Always handle errors gracefully:
export async function GET(request: NextRequest) {
try {
const data = await fetchFromDatabase();
return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ error: "Failed to fetch data" },
{ status: 500 }
);
}
}
Key Takeaways
route.tsfiles define API endpoints; exportGET,POST,PUT,DELETEfunctions for each HTTP method.- Dynamic routes use
[id]segments; parameters are passed via theparamsprop. - Request data comes from
request.json()(body),searchParams(query), andrequest.headers. - Responses are
NextResponse.json(data)orNextResponse.json(data, { status: 201 })with status codes. - Client Components call APIs with
fetchfromuseEffector event handlers.
Frequently Asked Questions
Can I use middlewares in API routes?
Yes. Use the middleware.ts file at the root of your app to intercept all requests, including API routes. See the Next.js middleware documentation.
How do I add authentication to API routes?
Check the Authorization header and validate tokens:
export async function POST(request: NextRequest) {
const token = request.headers.get("authorization")?.split(" ")[1];
if (!validateToken(token)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// ... handle request
}
Can I connect to a database from an API route?
Yes. Import your database client (Prisma, MongoDB driver, etc.) and query directly:
import prisma from "@/lib/prisma";
export async function GET() {
const posts = await prisma.post.findMany();
return NextResponse.json(posts);
}
Do API routes run on the edge or serverless?
By default, serverless (AWS Lambda or equivalent). For edge regions, use the runtime: "edge" export in your route file.
Can I upload files with API routes?
Yes. Use formData() to read multipart form data:
const formData = await request.formData();
const file = formData.get("file") as File;
// Save file...
Further Reading
- Next.js Route Handlers – official API route documentation.
- HTTP Status Codes – complete status code reference.
- REST API Best Practices – designing clean, scalable APIs.