This website uses cookies

Our website, platform and/or any sub domains use cookies to understand how you use our services, and to improve both your experience and our marketing relevance.

Next.js API Routes: How They Work and How to Build REST Endpoints

Updated on May 15, 2026

14 Min Read

Key Takeaways

  • Unified Codebase: Next.js API routes allow you to build and manage backend endpoints directly within your frontend project.
  • Standard Methods: Endpoints manage data by mapping standard HTTP methods (GET, POST, PUT, DELETE) to specific backend actions.
  • API Reliability: Regardless of the specific database or tools used, a secure endpoint requires strict input validation and proper error handling.

REST endpoints are used by apps to connect the frontend to the backend. Whichever web app you use, for example Amazon where you load data, fill up a form, or update items in your shopping cart, all the actions happen via an API.

And Next.js makes creating endpoints using API routes very easy. What I mean by this is that you don’t need a separate backend project. Everything stays inside the same Next.js app, which keeps things simple, especially if you want to build features fast.

In this blog, I’ll also cover what API routes are and how they work. Then I’ll build a small Tasks API with five endpoints. To it, I’ll add validation and basic error handling as well.

By the end, you’ll have a working example that I’ll also push to my GitHub so you can reuse it in your own projects.

What Are Next.js API Routes?

API routes are just files that handle incoming requests in Next.js. These routes files live inside your project, and each file acts as a backend endpoint. So…when a request hits a specific URL, Next.js runs the file that matches automatically.

So this way, as I said earlier, there’s no need to set up a separate backend.

The way it works is pretty straightforward. Inside the app folder, you create a route.ts file inside a folder. The folder name becomes part of the URL.

So for example, app/api/users/route.ts handles the requests to /api/users.

Still with me?

Inside our route.ts file, we export functions like GET, POST, PUT, PATCH, or DELETE. When a request comes in, Next.js runs the function that matches the HTTP method. That is all there is to it.

When to Use Next.js API Routes

Next.js API routes are good for most, but… not for every situation. In my opinion, here’s when they make sense and when they absolutely don’t.

Makes sense when:

  • you’re building a full stack app and want to keep the frontend/backend in one project
  • the API is mostly used for your own frontend and not as a public API for third parties
  • you want quick endpoints for actions like form submissions, database reads, etc
  • you’re deploying to Vercel or any other serverless platform that runs Node.js

Doesn’t makes sense when:

  • you need long-running tasks like large file uploads that can halt due to timeouts
  • your backend is heavy and would be better off in a dedicated server
  • you need WebSockets or real-time connections (API routes are stateless)
  • you’re building a public API that needs more control over things like rate limiting, caching, and versioning

For small to medium scale projects, like the ones that have the frontend and backend tightly coupled, Next.js API routes are worth using.

Common HTTP Methods

Each HTTP method has a specific job when used with the REST APIs. Let me quickly explain what each one does.

Method What It Does Example Use Case
GET Fetches data from a server Gets all tasks or a single task
POST Creates new data on a server Adds a new task
PUT Updates existing data (full replace) Updates a task’s title, status, etc
PATCH Updates part existing data (partial replace) Marks a task as completed
DELETE Removes data from a server Deletes a task

In the mini project I’ll build next, I’ll use GET, POST, PUT, and DELETE. PATCH is more or less like PUT but used for partial updates. I won’t cover it separately.

How to Build REST Endpoints Using Next.js API Routes

To show you how REST endpoints work using Next.js API routes, I’ll create a mini project. It’ll basically be a backend for a to-do kind of app with five endpoints that let you:

  • fetch all of the tasks
  • fetch just one task
  • create a task
  • update it
  • and delete the task

This will cover the main HTTP methods you’d use in any REST API. I’ll skip building a UI for this project and instead show you the API requests firing through Postman.

What I’ll Be Using

  • Node.js
  • VS Code
  • Postman
  • Next.js, Prisma, and SQLite

Step 1: Setting Up the Next.js Project

The first thing I need to get started is a Next.js project. But…before I create that, I need Node.js on my machine.

Installing Node.js

I can go to nodejs.org and get the LTS version. I’m getting the standalone binary version because my office laptop has IT restrictions.

Downloading the node.js long-term support version or standalone binary installer.

Once downloaded, I’ll unzip the file in my downloads folder.

C:\Users\abdulrehman\Downloads\node-v24.15.0-win-x64\node-v24.15.0-win-x64

File explorer view of the unzipped node.js standalone binary folder path.

Opening the project folder in Command Prompt

Now I can go ahead and create a new folder for my project. I’ll call it: tasks-api.

Next I’ll open the command prompt. Here I’ll be running all the commands.

Command prompt window open and ready inside the new next.js project directory.

Once command prompt opens, I’ll go to my project folder:

cd C:\Users\abdulrehman\Desktop\tasks-api

Pointing Command Prompt to the Node binary

Since I downloaded the standalone binary version, command prompt doesn’t know where Node is on my machine. So I need to tell it.

To to this, I’ll run this command with the path where I unzipped my Node folder:

set PATH=%PATH%;C:\Users\abdulrehman\Downloads\node-v24.15.0-win-x64\node-v24.15.0-win-x64

To confirm everything is working for me, I’ll run these two commands one after the other:

node -v
npm -v

Since I can see the version numbers for Node and npm, everything is A-OK.

Console output confirming the version numbers for node and npm are installed correctly.

Creating the Next.js project

With Node and npm now ready, I’ll can create my Next.js project by running this command:

npx create-next-app@latest .

I added the dot at the end of the command to tell Next.js to set up the project inside my current folder, the tasks-api one, instead of creating a new folder.

After running the command, I’ll just hit enter to accept a few on-screen prompts.

Command prompt running the 'npx create-next-app@latest .' command with setup prompts displayed

After I do that, Next.js starts installing all the dependencies. Once it finishes, it shows a Success! message confirming the project is created.

 'success' message displayed in the console after next.js finishes installing dependencies.

Opening the project in VS Code

Now I’ll open our project in VS code and go to File > Open Folder, and select tasks-api folder.

The app/ folder is where all routes live. I’ll create my API endpoints in this same folder later on.

VS code file explorer showing the newly generated next.js project structure, highlighting the app folder.

Running the project

Now I can hop back in command prompt and start the development server by running this command:

npm run dev

Command prompt output showing 'npm run dev' starting the next.js development server.

After starting, I can verify by going to: http://localhost:3000. Since I can see the default Next.js welcome page, that means it is working.

Web browser displaying the default next.js welcome page at localhost:3000.

Step 2: Installing and Setting Up Prisma with SQLite

Now I need a database to store my tasks. For this tutorial, I’ll use SQLite and Prisma.

Installing Prisma

To install Prisma v6, I need to first stop the development server that is running. To do that, I’ll CTRL + C.

Then in the same command prompt window, I’ll run:

npm install prisma@^6 --save-dev

Console showing the command to install the prisma development dependency.

After installing, I also need Prisma Client. This will be the library my code will use to query the database. To install it, I’ll use this command:

npm install @prisma/client@^6

Console showing the command to install the prisma client package.

Initializing Prisma

Next, I’ll set up Prisma in my project by running:

npx prisma init --datasource-provider sqlite

Command prompt running 'npx prisma init' to set up prisma using sqlite as the data source.

This command:

  • Creates a folder called prisma/ with a file inside called schema.prisma.
  • Also creates a .env file at the root of the project.

VS code showing the new prisma folder and the generated .env file in the project root.

Checking the generated files

Now I’ll open the .env file in VS Code and look for this line:

DATABASE_URL="file:./dev.db"

VS code view of the .env file setting the database url to dev.db for sqlite.

This tells Prisma to create SQLite database as a file called dev.db inside the prisma/ folder.

I’ll also open prisma/schema.prisma to make sure I see this:

generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

VS code displaying the initial generated generator and datasource blocks in schema.prisma.

Everything checks. I can safely move forward.

Step 3: Defining the Task Model and Running the First Migration

Now I’ll tell Prisma that each task in my database will have an ID, a title, a description, a completion status, and a timestamp.

Adding the Task model

To do this, I’ll open prisma/schema.prisma in VS Code and add the following:

model Task {
  id          Int      @id @default(autoincrement())
  title       String
  description String?
  completed   Boolean  @default(false)
  createdAt   DateTime @default(now())
}

VS code view showing the added prisma model for the task database table definition.

Running the first migration

Since I added the Task model, I’ll run a migration so Prisma can create a matching table in SQLite.

To do this, I’ll run this command in CMD:

npx prisma migrate dev --name init

The –name init part is just a label for this migration. I could’ve named it anything.

Console output from 'npx prisma migrate dev' creating the database schema.

Quick check

To verify everything is going okay so far, I should see the following files in the prisma/ folder in VS Code:

VS code file explorer showing the new dev.db sqlite database file inside the prisma folder.

Since I can see the files I need in my project folder, I can continue.

The dev.db file in the image above is the actual SQLite database my project will use. It’s empty right now, but it has the Task table ready to receive data.

Step 4: Building the GET and POST Endpoints for /api/tasks

I will now create two endpoints:

  • GET will return all tasks
  • and POST will add a new task

In Next.js, API routes in the App Router are built using a route.ts file inside a folder. The folder name becomes the URL.

Setting Up the Prisma Client

Before I create the endpoints, I need a Prisma Client instance that the rest of the app can import.

Inside the app folder, I’ll create a folder named lib, and inside it, a file called prisma.ts.

VS code file explorer showing the path to the new prisma client setup file in app/lib.

Then in it, I’ll paste this code:

import { PrismaClient } from "@prisma/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

This code prevents Prisma from creating new client on every reload. If I don’t do this, my server could open several connections which can cause performance problems.

Creating the Route File

Inside app folder, I’ll create a folder called api. Inside api, another folder called tasks. Inside tasks, a file called route.ts.

Path would look like this:

app/api/tasks/route.ts

Visual representation of the file path structure for the new next.js api route.

The GET Endpoint

Now I’ll open app/api/tasks/route.ts and add:

import { NextResponse } from "next/server";
import { prisma } from "@/app/lib/prisma";
export async function GET() {
  const tasks = await prisma.task.findMany();
  return NextResponse.json(tasks);
}

VS code showing the typescript code for the next.js api get endpoint to fetch all tasks.

prisma.task.findMany() will fetch all tasks for me.

NextResponse.json() will return them.

The function name GET is what will tell Next.js that it is a GET handler. POST, PUT, and DELETE work the same way.

The POST Endpoint

In the same file, below the GET function, I’ll add the POST function code block:

export async function POST(request: Request) {
  const body = await request.json();
  const task = await prisma.task.create({
    data: {
      title: body.title,
      description: body.description,
    },
  });
  return NextResponse.json(task, { status: 201 });
}

VS code showing the typescript code for the next.js api post endpoint to create a new task.

Testing the Endpoints

To test, I’ll start the dev server:

npm run dev

Open the Postman desktop application.

GET /api/tasks

I’ll set the method to GET, enter http://localhost:3000/api/tasks in the URL bar, and click Send. Since my database is empty, the response I’ll see will be:

[]

Postman desktop application showing a get request to the tasks endpoint returning an empty array.

POST /api/tasks

Next I’ll change the method to POST and keep the same URL.

And also select Body > raw > JSON, paste this code and click Send.

{
  "title": "Write the tutorial",
  "description": "Finish the Next.js API routes guide"
}

Postman interface showing a post request with a raw json body containing task title and description.

I should see this response:

{
  "id": 1,
  "title": "Write the tutorial",
  "description": "Finish the Next.js API routes guide",
  "completed": false,
  "createdAt": "2026-05-12T10:32:14.000Z"
}

Postman showing the successful 201 response and the created task object with id 1.

As you can see in the screenshot above, it worked.

Now I’ll confirm if the task got saved in the database. So I’ll change the method back to GET, and click Send.

Instead of an empty array, this time I should see what got saved earlier.

[
  {
    "id": 1,
    "title": "Write the tutorial",
    "description": "Finish the Next.js API routes guide",
    "completed": false,
    "createdAt": "2026-05-12T10:32:14.000Z"
  }
]

And great, the screenshot below confirms both endpoints are working for me. POST is writing the tasks to the database, and GET is reading them back.

Postman showing a subsequent get request returning the json array containing the single saved task.

Next I need to set up GET by ID, PUT, and DELETE.

Step 5: Building the GET, PUT, and DELETE Endpoints for /api/tasks/[id]

Now the end points I need to create, they work on a single task by its ID.

Creating the Dynamic Route File

For this, inside the app/api/tasks folder, I’ll create a new folder named [id]. And inside the folder, I’ll create a file named route.ts.

The path would look like this:

app/api/tasks/[id]/route.ts

[id] in the folder name acts as a placeholder.

The GET by ID Endpoint

Now I’ll open app/api/tasks/[id]/route.ts and add this code:

import { NextResponse } from "next/server";
import { prisma } from "@/app/lib/prisma";
export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const task = await prisma.task.findUnique({
    where: { id: Number(id) },
  });
  if (!task) {
    return NextResponse.json({ error: "Task not found" }, { status: 404 });
  }
  return NextResponse.json(task);
}

The PUT Endpoint

Below the GET function in the same file, I’ll add this:

export async function PUT(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const body = await request.json();
  const task = await prisma.task.update({
    where: { id: Number(id) },
    data: {
      title: body.title,
      description: body.description,
      completed: body.completed,
    },
  });
  return NextResponse.json(task);
}

The DELETE Endpoint

And below the PUT function, I’ll just add this:

export async function DELETE(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  await prisma.task.delete({
    where: { id: Number(id) },
  });
  return NextResponse.json({ message: "Task deleted" });
}

Testing the Endpoints

With the dev server running, I’ll open Postman and test each new end point I created.

Postman showing a get request to the dynamic route /api/tasks/1 returning the single task object.

GET /api/tasks/1

The response should be the task I created earlier:

{
  "id": 1,
  "title": "Write the tutorial",
  "description": "Finish the Next.js API routes guide",
  "completed": false,
  "createdAt": "2026-05-12T10:32:14.000Z"
}

PUT /api/tasks/1

Now I’ll change to PUT with the same URL http://localhost:3000/api/tasks/1. And paste this code:

{
  "title": "Write the tutorial",
  "description": "Finish the Next.js API routes guide",
  "completed": true
}

The response should now show completed: true:

{
  "id": 1,
  "title": "Write the tutorial",
  "description": "Finish the Next.js API routes guide",
  "completed": true,
  "createdAt": "2026-05-12T10:32:14.000Z"
}

Postman showing a put request update, with the response confirming the 'completed: true' status.

DELETE /api/tasks/1

Lastly, I’ll change to DELETE, keeping the same URL http://localhost:3000/api/tasks/1.

The response should be:

{
  "message": "Task deleted"
}

Postman showing a successful delete request and the "task deleted" message in the response body.

And to verify if the delete function worked, I’ll use the GET method with this URL: /api/tasks.

The array should return empty again, as you can see in the GIF below as well.

Postman showing a final get request to /api/tasks returning an empty array after the task was deleted.

So all 5 endpoints are working properly. Next, I’ll add input validation so my API doesn’t accept bad data.

Step 6: Adding Input Validation with Zod

Right now POST and PUT will accept whatever data I input. That is a problem. To fix this, I’ll implement input validation.

For validation I am going to use Zod, which will allow me to define what the data should look like and then check incoming data against it.

Installing Zod

I’ll stop the already running dev server. And then in command prompt, I’ll run this install command:

npm install zod

Console output showing the npm command running to install the zod validation library.

Creating a Validation Schema

Inside the app/lib folder, I’ll create a file called validations.ts.

VS code file explorer showing the path to the newly created validations.ts file.

Inside the file, I’ll add this code:

import { z } from "zod";
export const taskSchema = z.object({
  title: z.string().min(1, "Title is required"),
  description: z.string().optional(),
  completed: z.boolean().optional(),
});

Wiring Validation into POST and PUT

To connect things up, I’ll open route.ts file at app/api/tasks/route.ts and add this below the existing imports:

import { taskSchema } from "@/app/lib/validations";

VS code view of route.ts showing the import statement for the task validation schema.

And replace the existing POST function with this code snippet:

export async function POST(request: Request) {
  const body = await request.json();
  const result = taskSchema.safeParse(body);
  if (!result.success) {
    return NextResponse.json(
      { error: result.error.issues },
      { status: 400 }
    );
  }
  const task = await prisma.task.create({
    data: result.data,
  });
  return NextResponse.json(task, { status: 201 });
}

Now I’ll go to the other route.ts file at app/api/tasks/[id]/route.ts and use the same validation approach for the PUT function. Add taskSchema import at the top and update PUT function:

export async function PUT(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const body = await request.json();
  const result = taskSchema.safeParse(body);
  if (!result.success) {
    return NextResponse.json(
      { error: result.error.issues },
      { status: 400 }
    );
  }
  const task = await prisma.task.update({
    where: { id: Number(id) },
    data: result.data,
  });
  return NextResponse.json(task);
}

Testing the Validation

To test, I’ll start the dev server again and send a POST to http://localhost:3000/api/tasks with an empty title:

{
  "title": "",
  "description": "Trying to break the API"
}

I’ll see a 400 with an error message that the title is required.

Postman showing a post request failing with a 400 bad request status due to missing title.

If I try again with a valid body, the task will get created with a 201 OK.

Postman showing a post request succeeding with a 201 ok status after passing validation.

And with that, validation is in place for both POST and PUT.

Step 7: Adding Error Handling

To make sure everything doesn’t break when something goes wrong, I’ll need to add try/catch around my database calls.

Wrapping the Endpoints

To do this, I’ll need to go to app/api/tasks/route.ts and wrap the body of the GET function in a try/catch.

In the catch, I’ll return a message like “Failed to fetch tasks”. And apply the same wrapping to POST.

VS code showing the implemented try/catch blocks for error handling in the get and post functions.

Then I’ll go to the app/api/tasks/[id]/route.ts and add the try/catch block to GET by ID, PUT, and DELETE functions.

VS code showing the try/catch block added for error handling in the get by id function.

VS code showing the try/catch block added for error handling in the put function.

Testing the Error Handling

To test, I’ll send a DELETE request to http://localhost:3000/api/tasks/9999. This ID does not exist so instead of a crash, the error handling I implemented will show 404 with a message.

Postman showing a delete request to a non-existent id returning a 404 not found error message.

I’ll test with the PUT request too at /api/tasks/9999 with a valid JSON body. This will also show me 404 with an error message.

Postman showing a put request to a non-existent id returning a 404 not found error message.

And that’s it. All 5 endpoints are working and data is being saved into SQLite. This completes my mini project on REST API using Next.js API routes.

Wrapping Up

To do a quick recap, in this tutorial, I connected Prisma to an SQLite database, built five CRUD endpoints, added validation with Zod and error handling using try/catch.

As I promised earlier, the full project is available on my GitHub. You can clone it and use it as a starting point for your own project.

If you have any questions, let me know in the comments.

Q1: What are API routes in Next.js?

API routes allow creating backend endpoints inside Next.js projects. Any file inside app/api is considered an API endpoint instead of a page. Routes run on the server and handle incoming HTTP requests.

Q2: Does Next.js handle routing?

Yes. Next.js has its own routing system. It supports navigation between pages using the Link component. This makes navigation feel smooth, similar to a single pager app.

Q3: What HTTP methods are supported in Next.js routes?

Inside the routes file, you can have handlers for common HTTP methods like GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS. Next.js fires the matching function based on the request type it receives.

Q4: What’s the difference between API routes and Server Actions?

API routes are backend endpoints that can be called from anywhere, even external services. Server Actions, as the name suggests, run on the server and are mainly used directly inside your React components to handle tasks like form submissions or internal updates.

Share your opinion in the comment section. COMMENT NOW

Share This Article

Abdul Rehman

Abdul is a tech-savvy, coffee-fueled, and creatively driven marketer who loves keeping up with the latest software updates and tech gadgets. He's also a skilled technical writer who can explain complex concepts simply for a broad audience. Abdul enjoys sharing his knowledge of the Cloud industry through user manuals, documentation, and blog posts.

×

Webinar: How to Get 100% Scores on Core Web Vitals

Join Joe Williams & Aleksandar Savkovic on 29th of March, 2021.

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Want to Experience the Cloudways Platform in Its Full Glory?

Take a FREE guided tour of Cloudways and see for yourself how easily you can manage your server & apps on the leading cloud-hosting platform.

Start my tour