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:
<span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { promises <span class="hljs-keyword">as</span> fs } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>; <span class="hljs-comment">// Importing promise-based filesystem methods</span>
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</span>; <span class="hljs-comment">// For handling file paths</span>
<span class="hljs-comment">// Define the structure of a User object</span>
<span class="hljs-keyword">interface</span> User {
id: <span class="hljs-built_in">string</span>;
name: <span class="hljs-built_in">string</span>;
email: <span class="hljs-built_in">string</span>;
age: <span class="hljs-built_in">number</span>;
}
<span class="hljs-comment">// Define the path to the users.json file</span>
<span class="hljs-keyword">const</span> usersFile = path.join(process.cwd(), <span class="hljs-string">"app/users/users.json"</span>);
<span class="hljs-comment">// Read users from the JSON file and return them as an array</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">readUsers</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span><<span class="hljs-title">User</span>[]> </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> fs.readFile(usersFile, <span class="hljs-string">"utf-8"</span>);
<span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(data) <span class="hljs-keyword">as</span> User[];
} <span class="hljs-keyword">catch</span> {
<span class="hljs-comment">// If file doesn't exist or fails to read, return empty array</span>
<span class="hljs-keyword">return</span> [];
}
}
<span class="hljs-comment">// Write updated users array to the JSON file</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">writeUsers</span>(<span class="hljs-params">users: User[]</span>) </span>{
<span class="hljs-keyword">await</span> fs.writeFile(usersFile, <span class="hljs-built_in">JSON</span>.stringify(users, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>), <span class="hljs-string">"utf-8"</span>);
}
<span class="hljs-comment">// Handle GET request: return list of users</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();
<span class="hljs-keyword">return</span> NextResponse.json(users);
}
<span class="hljs-comment">// Handle POST request: add a new user</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">request: NextRequest</span>) </span>{
<span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();
<span class="hljs-comment">// Destructure and validate input fields</span>
<span class="hljs-keyword">const</span> { name, email, age } = body <span class="hljs-keyword">as</span> {
name?: <span class="hljs-built_in">string</span>;
email?: <span class="hljs-built_in">string</span>;
age?: <span class="hljs-built_in">number</span>;
};
<span class="hljs-comment">// Return 400 if any required field is missing</span>
<span class="hljs-keyword">if</span> (!name || !email || age === <span class="hljs-literal">undefined</span>) {
<span class="hljs-keyword">return</span> NextResponse.json(
{ error: <span class="hljs-string">"Missing name, email, or age"</span> },
{ status: <span class="hljs-number">400</span> }
);
}
<span class="hljs-comment">// Read existing users</span>
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();
<span class="hljs-comment">// Create new user object with unique ID based on timestamp</span>
<span class="hljs-keyword">const</span> newUser: User = {
id: <span class="hljs-built_in">Date</span>.now().toString(),
name,
email,
age,
};
<span class="hljs-comment">// Add new user to the list and save to file</span>
users.push(newUser);
<span class="hljs-keyword">await</span> writeUsers(users);
<span class="hljs-comment">// Return the newly created user with 201 Created status</span>
<span class="hljs-keyword">return</span> NextResponse.json(newUser, { status: <span class="hljs-number">201</span> });
}
Now the code for the /app/users/[id]/route.ts file:
<span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { promises <span class="hljs-keyword">as</span> fs } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</span>;
<span class="hljs-comment">// Define the User interface</span>
<span class="hljs-keyword">interface</span> User {
id: <span class="hljs-built_in">string</span>;
name: <span class="hljs-built_in">string</span>;
email: <span class="hljs-built_in">string</span>;
age: <span class="hljs-built_in">number</span>;
}
<span class="hljs-comment">// Path to the users.json file</span>
<span class="hljs-keyword">const</span> usersFile = path.join(process.cwd(), <span class="hljs-string">"app/users/users.json"</span>);
<span class="hljs-comment">// Function to read users from the JSON file</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">readUsers</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span><<span class="hljs-title">User</span>[]> </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> fs.readFile(usersFile, <span class="hljs-string">"utf-8"</span>);
<span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(data) <span class="hljs-keyword">as</span> User[];
} <span class="hljs-keyword">catch</span> {
<span class="hljs-comment">// If file doesn't exist or is unreadable, return an empty array</span>
<span class="hljs-keyword">return</span> [];
}
}
<span class="hljs-comment">// Function to write updated users to the JSON file</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">writeUsers</span>(<span class="hljs-params">users: User[]</span>) </span>{
<span class="hljs-keyword">await</span> fs.writeFile(usersFile, <span class="hljs-built_in">JSON</span>.stringify(users, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>), <span class="hljs-string">"utf-8"</span>);
}
<span class="hljs-comment">// GET /users/:id - Fetch a user by ID</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">
request: NextRequest,
{ params }: { params: <span class="hljs-built_in">Promise</span><{ id: <span class="hljs-built_in">string</span> }> },
</span>) </span>{
<span class="hljs-keyword">const</span> id = (<span class="hljs-keyword">await</span> params).id;
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();
<span class="hljs-comment">// Find the user by ID</span>
<span class="hljs-keyword">const</span> user = users.find(<span class="hljs-function">(<span class="hljs-params">u</span>) =></span> u.id === id);
<span class="hljs-comment">// Return 404 if user is not found</span>
<span class="hljs-keyword">if</span> (!user) {
<span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">"User not found"</span> }, { status: <span class="hljs-number">404</span> });
}
<span class="hljs-comment">// Return the found user</span>
<span class="hljs-keyword">return</span> NextResponse.json(user);
}
<span class="hljs-comment">// PUT /users/:id - Update a user by ID</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PUT</span>(<span class="hljs-params">
request: NextRequest,
{ params }: { params: <span class="hljs-built_in">Promise</span><{ id: <span class="hljs-built_in">string</span> }> },
</span>) </span>{
<span class="hljs-keyword">const</span> id = (<span class="hljs-keyword">await</span> params).id;
<span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();
<span class="hljs-comment">// Extract optional fields from request body</span>
<span class="hljs-keyword">const</span> { name, email, age } = body <span class="hljs-keyword">as</span> {
name?: <span class="hljs-built_in">string</span>;
email?: <span class="hljs-built_in">string</span>;
age?: <span class="hljs-built_in">number</span>;
};
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();
<span class="hljs-comment">// Find the index of the user to update</span>
<span class="hljs-keyword">const</span> index = users.findIndex(<span class="hljs-function">(<span class="hljs-params">u</span>) =></span> u.id === id);
<span class="hljs-comment">// Return 404 if user not found</span>
<span class="hljs-keyword">if</span> (index === <span class="hljs-number">-1</span>) {
<span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">"User not found"</span> }, { status: <span class="hljs-number">404</span> });
}
<span class="hljs-comment">// Update the user only with provided fields</span>
users[index] = {
...users[index],
...(name !== <span class="hljs-literal">undefined</span> ? { name } : {}),
...(email !== <span class="hljs-literal">undefined</span> ? { email } : {}),
...(age !== <span class="hljs-literal">undefined</span> ? { age } : {}),
};
<span class="hljs-keyword">await</span> writeUsers(users);
<span class="hljs-comment">// Return the updated user</span>
<span class="hljs-keyword">return</span> NextResponse.json(users[index]);
}
<span class="hljs-comment">// DELETE /users/:id - Delete a user by ID</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DELETE</span>(<span class="hljs-params">
request: NextRequest,
{ params }: { params: <span class="hljs-built_in">Promise</span><{ id: <span class="hljs-built_in">string</span> }> },
</span>) </span>{
<span class="hljs-keyword">const</span> id = (<span class="hljs-keyword">await</span> params).id;
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();
<span class="hljs-comment">// Find the index of the user to delete</span>
<span class="hljs-keyword">const</span> index = users.findIndex(<span class="hljs-function">(<span class="hljs-params">u</span>) =></span> u.id === id);
<span class="hljs-comment">// Return 404 if user not found</span>
<span class="hljs-keyword">if</span> (index === <span class="hljs-number">-1</span>) {
<span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">"User not found"</span> }, { status: <span class="hljs-number">404</span> });
}
<span class="hljs-comment">// Remove user from the array and save updated list</span>
<span class="hljs-keyword">const</span> [deleted] = users.splice(index, <span class="hljs-number">1</span>);
<span class="hljs-keyword">await</span> writeUsers(users);
<span class="hljs-comment">// Return the deleted user</span>
<span class="hljs-keyword">return</span> 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
{<span class="hljs-string">"name"</span>:<span class="hljs-string">"Manish"</span>,<span class="hljs-string">"age"</span>:<span class="hljs-number">30</span>, <span class="hljs-string">"email"</span>:<span class="hljs-string">"manish@example.com"</span>}
I’ll create one more record named “Larry”. Here is the JSON:
{<span class="hljs-string">"name"</span>:<span class="hljs-string">"Larry"</span>,<span class="hljs-string">"age"</span>:<span class="hljs-number">25</span>, <span class="hljs-string">"email"</span>:<span class="hljs-string">"larrry@example.com"</span>}
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.
Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & MoreÂ