Skip to main content

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 posts
  • POST /api/posts → create a post
  • PUT /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 1
  • PUT /api/posts/1 → update post 1
  • DELETE /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:

CodeMeaningUse Case
200OKSuccessful GET, PUT
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestInvalid data
401UnauthorizedMissing auth
404Not FoundResource doesn't exist
500Server ErrorUnexpected 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.ts files define API endpoints; export GET, POST, PUT, DELETE functions for each HTTP method.
  • Dynamic routes use [id] segments; parameters are passed via the params prop.
  • Request data comes from request.json() (body), searchParams (query), and request.headers.
  • Responses are NextResponse.json(data) or NextResponse.json(data, { status: 201 }) with status codes.
  • Client Components call APIs with fetch from useEffect or 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