When people hear about Next.js, they often think of server-side rendering, React-powered frontends, or SEO-optimised static websites. But there's more to this powerful framework than just front-end development.
Next.js also allows developers to build robust, scalable backend APIs directly inside the same codebase. This is especially valuable for small to mid-sized applications where having a tightly coupled frontend and backend speeds up development and deployment.
In this article, you’ll learn how to build an API using Next.js and deploy it to production using Sevalla. It’s relatively easy to learn how to build something using a tutorial – but the real challenge is to get it into the hands of users. Doing so transforms your project from a local prototype into something real and usable.
Table of Contents
What is Next.js?
Next.js is an open-source React framework built by Vercel. It enables developers to build server-rendered and statically generated web applications.
It essentially abstracts the configuration and boilerplate needed to run a full-stack React application, making it easier for developers to focus on building features rather than setting up infrastructure.
While it started as a solution for frontend challenges in React, it has evolved into a full-stack framework that lets you handle backend logic, interact with databases, and build APIs. This unified codebase is what makes Next.js particularly compelling for modern web development.
Installation & Setup
Let’s install Next.js. Make sure you have Node.js and NPM installed on your system, and that they’re the latest version.
$ node --version
v22.16.0
$ npm --version
10.9.2
Now let’s create a Next.js project. The command to do so is:
$ npx create-next-app@latest
The result of the above command will ask you a series of questions to setup your app:
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Turbopack for `next dev`? No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*
But for this tutorial, we aren’t interested in a full stack app – just an API. So let’s re-create the app using the — - api
flag.
$ npx create-next-app@latest --api
It will still ask you a few questions. Use the default settings and finish creating the app.
Once the setup is done, you can see the folder with your app name. Let’s go into the folder and run the app.
$ npm run dev
Your API template should be running at port 3000. Go to http://localhost:3000 and you should see the following message:
{
"message": "Hello world!"
}
How to Build a REST API
Now that we’ve set up our API template, let's write a basic REST API. A basic REST API is simply four endpoints: Create, Read, Update, Delete (also called as CRUD).
Usually, we’d use a database, but for simplicity’s sake, we’ll use a JSON file in our API. Our goal is to build a REST API that can read and write to this JSON file.
The API code will reside under /app within the project directory. Next.js uses file-based routing for building URL paths.
For example, if you want a URL path /users, you should have a directory called “users” with a route.ts file to handle all the CRUD operations for /users. For /users/:id, you should have a directory called [id] under “users” directory with a route.ts file. The square brackets are to tell Next.js that you expect dynamic values for the /users/:id route.
You should also have the users.json inside the /app/users directory for your routes to read and write data.
Here is a screenshot of the setup. Delete the [slug] directory that comes with the project since it won’t be relevant for us:
The route.ts file at the bottom handles CRUD operations for /
The route.ts file under/users handles CRUD operations for /users
The route.ts file under /users/[id]/ handles CRUD operations under /users/:id where the ‘id’ will be a dynamic value.
The users.json under /users will be our data store.
While this setup can seem complicated for a simple project, it provides a clear structure for large-scale web applications. If you want to go deeper into building complex APIs with Next.js, here is a tutorial you can follow.
The code under /app/route.ts is the default file for our API. You can see it serving the GET request and responding with “Hello World!”:
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Hello world!" });
}
Now we need five routes:
GET /users → List all users
GET /users/:id → List a single user
POST /users → Create a new user
PUT /users/:id → Update an existing user
DELETE /users/:id → Delete an existing user
Here is the code for the route.ts file under /app/users:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { promises as fs } from "fs"; // Importing promise-based filesystem methods
import path from "path"; // For handling file paths
// Define the structure of a User object
interface User {
id: string;
name: string;
email: string;
age: number;
}
// Define the path to the users.json file
const usersFile = path.join(process.cwd(), "app/users/users.json");
// Read users from the JSON file and return them as an array
async function readUsers(): Promise<User[]> {
try {
const data = await fs.readFile(usersFile, "utf-8");
return JSON.parse(data) as User[];
} catch {
// If file doesn't exist or fails to read, return empty array
return [];
}
}
// Write updated users array to the JSON file
async function writeUsers(users: User[]) {
await fs.writeFile(usersFile, JSON.stringify(users, null, 2), "utf-8");
}
// Handle GET request: return list of users
export async function GET() {
const users = await readUsers();
return NextResponse.json(users);
}
// Handle POST request: add a new user
export async function POST(request: NextRequest) {
const body = await request.json();
// Destructure and validate input fields
const { name, email, age } = body as {
name?: string;
email?: string;
age?: number;
};
// Return 400 if any required field is missing
if (!name || !email || age === undefined) {
return NextResponse.json(
{ error: "Missing name, email, or age" },
{ status: 400 }
);
}
// Read existing users
const users = await readUsers();
// Create new user object with unique ID based on timestamp
const newUser: User = {
id: Date.now().toString(),
name,
email,
age,
};
// Add new user to the list and save to file
users.push(newUser);
await writeUsers(users);
// Return the newly created user with 201 Created status
return NextResponse.json(newUser, { status: 201 });
}
Now the code for the /app/users/[id]/route.ts file:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { promises as fs } from "fs";
import path from "path";
// Define the User interface
interface User {
id: string;
name: string;
email: string;
age: number;
}
// Path to the users.json file
const usersFile = path.join(process.cwd(), "app/users/users.json");
// Function to read users from the JSON file
async function readUsers(): Promise<User[]> {
try {
const data = await fs.readFile(usersFile, "utf-8");
return JSON.parse(data) as User[];
} catch {
// If file doesn't exist or is unreadable, return an empty array
return [];
}
}
// Function to write updated users to the JSON file
async function writeUsers(users: User[]) {
await fs.writeFile(usersFile, JSON.stringify(users, null, 2), "utf-8");
}
// GET /users/:id - Fetch a user by ID
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const id = (await params).id;
const users = await readUsers();
// Find the user by ID
const user = users.find((u) => u.id === id);
// Return 404 if user is not found
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
// Return the found user
return NextResponse.json(user);
}
// PUT /users/:id - Update a user by ID
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const id = (await params).id;
const body = await request.json();
// Extract optional fields from request body
const { name, email, age } = body as {
name?: string;
email?: string;
age?: number;
};
const users = await readUsers();
// Find the index of the user to update
const index = users.findIndex((u) => u.id === id);
// Return 404 if user not found
if (index === -1) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
// Update the user only with provided fields
users[index] = {
...users[index],
...(name !== undefined ? { name } : {}),
...(email !== undefined ? { email } : {}),
...(age !== undefined ? { age } : {}),
};
await writeUsers(users);
// Return the updated user
return NextResponse.json(users[index]);
}
// DELETE /users/:id - Delete a user by ID
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const id = (await params).id;
const users = await readUsers();
// Find the index of the user to delete
const index = users.findIndex((u) => u.id === id);
// Return 404 if user not found
if (index === -1) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
// Remove user from the array and save updated list
const [deleted] = users.splice(index, 1);
await writeUsers(users);
// Return the deleted user
return NextResponse.json(deleted);
}
We will have an empty array inside the /app/users.json. You can find all the code here in this repository.
How to Test the API
Now let’s test the API endpoints.
First, lets run the API:
$ npm run dev
You can go to http://localhost:3000/users and can see an empty array since we have not pushed any user information.
From the code, we can see that a user object needs name, email, and age since the id is automatically generated in the POST endpoint.
We will use Postman to simulate requests to the API and ensure that the API behaves as expected.
- GET /users: it will be empty on our first try since we haven’t pushed any data yet.
- POST /users: create a new user. Under “body”, choose “raw” and select “JSON”. This is the data we will be sending the api. The JSON body would be
{"name":"Manish","age":30, "email":"manish@example.com"}
I’ll create one more record named “Larry”. Here is the JSON:
{"name":"Larry","age":25, "email":"larrry@example.com"}
Now let’s look at the users. You should see two entries for our GET request to /users:
Now let’s look at a single user using /users/:id.
Now let’s update Larry’s age to 35. We’ll pass just the age in request body using the PUT request to /users/:id.
Now let’s delete Larry’s record.
If you check /users, you should see only one record:
So we have built and tested our api. Now let’s get this live.
How to Deploy to Sevalla
Sevalla is a modern, usage-based Platform-as-a-service provider and an alternative to sites like Heroku or to your self-managed setup on AWS. It combines powerful features with a smooth developer experience.
Sevalls offers application hosting, database, object storage, and static site hosting for your projects. It comes with a generous free tier, so let’s see how to deploy our API to the cloud using Sevalla.
Make sure you have the code committed to GitHub or fork my repository for this project. If you are new to Sevalla, you can sign up using your GitHub account to enable direct deploys from your GitHub account. Every time you push code to your project, Sevalla will auto-pull and deploy your app to the cloud.
Once you login to Sevalla, click on “Applications”. Now let’s create an app.
If you have authenticated with GitHub, the application creation interface will display a list of repositories. Choose the one you pushed your code into or the nextjs-api project if you forked it from my repository.
Check the box “auto deploy on commit”. This will ensure your latest code is auto-deployed to Sevalla. Now, let’s choose the instance to which we can deploy the application. Each one comes with its own pricing, based on the server's capacity.
Let’s choose the hobby server that costs $5/mo. Sevalla gives us a $50 free tier, so we don’t have to pay for anything unless we exceed this usage tier.
Now, click “Create and Deploy”. This should pull our code from our repository, run the build process, setup a Docker container and then deploy the app. Usually the work of a sysadmin, fully automated by Sevalla.
Wait for a few minutes for all the above to happen. You can watch the logs in the “Deployments” interface.
Now, click on “Visit App” and you will get the live URL (ending with sevalla.app) of your API. You can replace “http://localhost:3000” with the new URL and run the same tests using Postman.
Congratulations – your app is now live. You can do more with your app using the admin interface, like:
Monitor the performance of your app
Watch real-time logs
Add custom domains
Update network settings (open/close ports for security, and so on)
Add more storage
Sevalla also provides resources like Object storage, database, cache, and so on, which are out of scope for this tutorial. But it lets you monitor, manage, and scale your application without the need for a system administrator. That’s the beauty of PaaS systems. Here is a detailed comparison of VPS vs PaaS systems for application hosting.
Conclusion
In this article, we went beyond the typical frontend use case of Next.js and explored its capabilities as a full-stack framework. We built a complete REST API using the App Router and file-based routing, with data stored in a JSON file. Then, we took it a step further and deployed the API to production using Sevalla, a modern PaaS that automates deployment, scaling, and monitoring.
This setup demonstrates how developers can build and ship full-stack applications like frontend, backend, and deployment, all within a single Next.js project. Whether you're prototyping or building for scale, this workflow sets you up with everything you need to get your apps into users’ hands quickly and efficiently.
Hope you enjoyed this article. I ll see you soon with another one. Connect with me on LinkedIn or visit my website.