Building an API
An API lets you create endpoints (URLs) that run code on the server. In Next.js, you have two ways to run server-side code:
- API Routes - traditional endpoints that accept HTTP requests (like from a mobile app or another server)
- Server Actions - special functions you can call directly from your React components
We will cover both in this guide.
HTTP Methods
Section titled “HTTP Methods”When you send a request to a server, you use an HTTP method to tell the server what you want to do. Think of these as “actions”:
- GET - Fetch/read some data (like loading a list of users)
- POST - Send data to create something new (like registering a new user)
- PUT - Replace/update an existing item completely (like updating a full user profile)
- PATCH - Update only part of an existing item (like changing just the email)
- DELETE - Remove something (like deleting a user account)
API Routes
Section titled “API Routes”In Next.js (App Router), you create API routes by making a route.ts file inside the app/api/ folder. Each folder represents part of the URL.
Here is how the folder structure looks:
app└── api ├── products │ └── route.ts # handles /api/products | └── users ├── route.ts # handles /api/users | └── [id] └── route.ts # handles /api/users/123 (any user id)The [id] folder is a dynamic route - the id part can be any value (like a user ID).
Handling Requests and Responses
Section titled “Handling Requests and Responses”Next.js gives you NextRequest (to read the incoming request) and NextResponse (to send back a response). You export functions named after the HTTP method you want to handle.
There are 5 common things you do with data: List, Create, Read one, Update, and Delete.
List (GET all)
Section titled “List (GET all)”Get a list of all users:
// app/api/users/route.tsimport { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) { // fetch users from your database here const users = [ { id: 1, name: "John Doe" }, { id: 2, name: "Jane Doe" }, ];
return NextResponse.json(users, { status: 200 }); // status 200 means "OK"}Create (POST)
Section titled “Create (POST)”Create a new user by sending data in the request body:
// app/api/users/route.tsimport { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) { const { name } = await request.json(); // read the data sent by the client
// check that required fields are present if (!name) { return NextResponse.json({ error: "Name is required" }, { status: 400 }); // 400 = bad request }
// save the new user to your database here const newUser = { id: Date.now(), name };
return NextResponse.json(newUser, { status: 201 }); // 201 = "Created"}Update (PUT)
Section titled “Update (PUT)”Update an existing user by their ID. The ID comes from the URL (e.g. /api/users/5):
// app/api/users/[id]/route.tsimport { NextRequest, NextResponse } from "next/server";
interface Props { params: Promise<{ id: string }>;}
export async function PUT(request: NextRequest, { params }: Props) { const { id } = await params; const { name } = await request.json();
if (!id) { return NextResponse.json({ error: "User ID is required" }, { status: 400 }); } if (!name) { return NextResponse.json({ error: "Name is required" }, { status: 400 }); }
// update the user in your database here const updatedUser = { id, name };
return NextResponse.json(updatedUser, { status: 200 });}Delete (DELETE)
Section titled “Delete (DELETE)”Delete a user by their ID:
// app/api/users/[id]/route.tsimport { NextRequest, NextResponse } from "next/server";
interface Props { params: Promise<{ id: string }>;}
export async function DELETE(request: NextRequest, { params }: Props) { const { id } = await params;
if (!id) { return NextResponse.json({ error: "User ID is required" }, { status: 400 }); }
// delete the user from your database here
return NextResponse.json({ message: `User with ID ${id} has been deleted` }, { status: 200 });}Zod (Data Validation)
Section titled “Zod (Data Validation)”Zod is a library that helps you check that the data you receive is in the right format. Instead of writing lots of if checks yourself, you define a “schema” (a set of rules) and Zod checks the data against it automatically.
Install Zod:
npm install zodFolder structure with a schema file:
app└── api └── users ├── route.ts ├── schema.ts # your Zod validation rules live here | └── [id] └── route.tsDefine your schema:
// app/api/users/schema.tsimport { z } from "zod";
export const userSchema = z.object({ id: z.number().optional(), // id is a number, but not required name: z.string().min(1, "Name is required"), // name must be a non-empty string});Use the schema in your route to validate incoming data:
// app/api/users/route.tsimport { NextRequest, NextResponse } from "next/server";import { userSchema } from "./schema";
export async function POST(request: NextRequest) { const body = await request.json();
// safeParse checks the data without throwing an error const parsedData = userSchema.safeParse(body);
if (!parsedData.success) { // if validation fails, return the errors return NextResponse.json({ error: parsedData.error.errors }, { status: 400 }); }
const newUser = parsedData.data; // the validated and typed data
// save the new user to your database here
return NextResponse.json(newUser, { status: 201 });}There are two ways to validate with Zod:
parse- throws an error if the data is invalid (use inside try/catch)safeParse- returns a result object withsuccess: true/false(easier to handle)
Server Actions
Section titled “Server Actions”Server Actions are functions that run on the server but can be called directly from your React components - no need to create an API route or use fetch. They are great for form submissions and simple data mutations.
To create a Server Action, add "use server" at the top of the function or the file.
Creating a Server Action
Section titled “Creating a Server Action”// app/actions/userActions.ts"use server";
import { revalidatePath } from "next/cache";
export async function createUser(formData: FormData) { const name = formData.get("name") as string;
if (!name) { return { error: "Name is required" }; }
// save the new user to your database here // const newUser = await db.user.create({ data: { name } });
revalidatePath("/users"); // refresh the /users page so it shows the new data return { success: true };}Using a Server Action in a Form
Section titled “Using a Server Action in a Form”// app/users/new/page.tsximport { createUser } from "@/app/actions/userActions";
export default function NewUserPage() { return ( <form action={createUser}> <input type="text" name="name" placeholder="Enter name" required /> <button type="submit">Create User</button> </form> );}That’s it - no fetch, no API route needed. When the user submits the form, createUser runs on the server automatically.
Using a Server Action with Validation (Zod)
Section titled “Using a Server Action with Validation (Zod)”You can combine Server Actions with Zod for clean validation:
// app/actions/userActions.ts"use server";
import { z } from "zod";import { revalidatePath } from "next/cache";
const userSchema = z.object({ name: z.string().min(1, "Name is required") });
export async function createUser(formData: FormData) { const rawData = { name: formData.get("name") };
const parsedData = userSchema.safeParse(rawData);
if (!parsedData.success) { return { error: parsedData.error.errors[0].message }; }
// save user to database here
revalidatePath("/users"); return { success: true };}Using a Server Action with useActionState (for form feedback)
Section titled “Using a Server Action with useActionState (for form feedback)”If you want to show a success or error message after the form submits, use the useActionState hook:
// app/users/new/page.tsx"use client"; // this is needed because we are using a hook
import { useActionState } from "react";import { createUser } from "@/app/actions/userActions";
export default function NewUserPage() { const [state, formAction, isPending] = useActionState(createUser, null);
return ( <form action={formAction}> <input type="text" name="name" placeholder="Enter name" required /> <button type="submit" disabled={isPending}> {isPending ? "Creating..." : "Create User"} </button> {state?.error && <p style={{ color: "red" }}>{state.error}</p>} {state?.success && <p style={{ color: "green" }}>User created!</p>} </form> );}Server Actions vs API Routes - When to Use Which?
Section titled “Server Actions vs API Routes - When to Use Which?”This is a common question. Here is a simple way to think about it:
| Situation | Use |
|---|---|
| Submitting a form in your Next.js app | Server Action |
| Mutating data from a React component | Server Action |
| You need a public URL for the endpoint | API Route |
| A mobile app or external service needs to call it | API Route |
| Another server needs to call it | API Route |
| You want to reuse the same endpoint in multiple places | API Route |
| You need fine-grained control over headers/cookies/status codes | API Route |
Simple Rule
Section titled “Simple Rule”- Staying inside your Next.js app? - Use a Server Action. Less code, no
fetch, works great with forms. - Need an endpoint that the outside world can call? - Use an API Route.
Example - the same task done two ways
Section titled “Example - the same task done two ways”With an API Route (more setup):
// 1. Create the route// app/api/users/route.tsexport async function POST(request: NextRequest) { const { name } = await request.json(); // save to db... return NextResponse.json({ success: true });}
// 2. Call it from your componentconst response = await fetch("/api/users", { method: "POST", body: JSON.stringify({ name }) });With a Server Action (simpler):
// 1. Create the action// app/actions/userActions.ts"use server";export async function createUser(formData: FormData) { const name = formData.get("name"); // save to db...}
// 2. Use it directly in your form - no fetch needed!<form action={createUser}>...</form>Common HTTP Status Codes
Section titled “Common HTTP Status Codes”| Code | Meaning | When to use |
|---|---|---|
| 200 | OK | Successful GET, PUT, DELETE |
| 201 | Created | Successful POST (something was created) |
| 400 | Bad Request | The data sent was invalid or missing |
| 401 | Unauthorized | User is not logged in |
| 403 | Forbidden | User is logged in but not allowed |
| 404 | Not Found | The resource does not exist |
| 500 | Server Error | Something broke on the server |