When developers think of Next.js, they often associate it with SEO-friendly static websites or React-based frontends. But what many miss is how Next.js can also be used to build full-featured backend APIs – all within the same project.
I’ve recently written an article on working with Next.js API and deploying it to production. In this case, I would’ve used a JSON file as a mini-database.
But JSON or any type of file storage isn’t fit for a production application. This is because file-based storage isn’t designed for concurrent access, so multiple users writing data at the same time can cause corruption or loss.
It also lacks indexing and query capabilities, making it slow as data grows. Backups, security, and scalability are also harder to manage compared to a proper database.
In short, while JSON files work for demos or prototypes, production systems need a database that can handle concurrency, large datasets, complex queries, and reliable persistence.
So in this article, we’ll walk through how to build a REST API with Next.js, store data in a Sevalla-managed database, and deploy the whole project to production using Sevalla’s PaaS infrastructure.
Table of Contents
What is Next.js?
Next.js is an open-source React framework developed by Vercel. It’s known for server-side rendering, static generation, and seamless routing. But beyond its frontend superpowers, it allows developers to build backend logic and APIs through its file-based routing system. This makes Next.js a great choice for building full-stack apps.
Installation and Setup
To get started, make sure Node.js and NPM are installed.
$ node --version
v22.16.0
$ npm --version
10.9.2
Now, create a new Next.js project:
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 NextJS API
Now that we’ve set up our API template, let’s write a basic REST API with two endpoints: one to create data and one to view data
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.
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 / (this is where the response “hello world” was generated from)
The route.ts file under /users handles CRUD operations for /users
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 two routes:
GET /users which lists all users
POST /users which creates a new user
For this project, we’ll use a database to store our records. We’re not going to install a database on our local machine. Instead, we’ll provision the database in the cloud and use it with our API. This approach is common in test / prod environments to ensure data consistency.
Provisioning a Database in 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 we’ll use it to connect to a database as well as deploy our app to the cloud.
If you are new to Sevalla, you can sign up using your GitHub account to enable direct deploys from your GitHub. 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 “Databases”.
Now let’s create a PostgreSQL database.
Use the default settings. Once the database is created, it will disable the external connections by default for security to ensure no one outside our server can connect to it. Since we want to test our connection from our local machine, let’s enable an external connection.
The value we need to connect to the database from our local endpoint is “url” under external connection. Create a file called .env in the project and paste the URL in the below format:
PGSQL_URL=postgres://<username>:<password>-@asia-east1-001.proxy.kinsta.app:30503/<db_name>
The reason we use .env is to store environment variables specific to the environment. In production, we won’t need this file (never push .env files to GitHub). Sevalla will give us the option to add environment variables via the GUI when we deploy the app.
Now let’s test our database connection. Install the pg
package for Node to interact with PostgreSQL. Let’s also install the TypeScript extension for pg
to support TypeScript definitions.
$ npm i pg
$ npm install --save-dev @types/pg
Change the route.ts that served “hello world” to the below:
// app/api/your-endpoint/route.ts
import { NextResponse } from "next/server";
import { Client } from "pg";
export async function GET() {
const client = new Client({
connectionString: process.env.PGSQL_URL,
});
try {
await client.connect();
await client.end();
return NextResponse.json({ message: "Connected to database" });
} catch (error) {
console.error("Database connection error:", error);
return NextResponse.json({ message: "Connection failed" }, { status: 500 });
}
}
Now when your app and go to localhost:3000, it should say “connected to database”.
Great. Now let’s write our two routes, one to create data and the other to view the data we created. Use this code under users/route.ts:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { Client } from "pg";
// Define the structure of a User object
interface User {
id: string;
name: string;
email: string;
age: number;
}
// Create a PostgreSQL client
function getClient() {
return new Client({
connectionString: process.env.PGSQL_URL,
});
}
// Fetch all users from the database
async function readUsers(): Promise<User[]> {
const client = getClient();
await client.connect();
try {
const result = await client.query("SELECT id, name, email, age FROM users");
return result.rows;
} finally {
await client.end();
}
}
// Insert or update users in the database
async function writeUsers(users: User[]) {
const client = getClient();
await client.connect();
try {
const insertQuery = `
INSERT INTO users (id, name, email, age)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
email = EXCLUDED.email,
age = EXCLUDED.age;
`;
for (const user of users) {
await client.query(insertQuery, [user.id, user.name, user.email, user.age]);
}
} finally {
await client.end();
}
}
// Handle GET request: return list of users
export async function GET() {
try {
const users = await readUsers();
return NextResponse.json(users);
} catch (err) {
console.error("Error reading users from DB:", err);
return NextResponse.json({ error: "Failed to fetch users" }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const users: User[] = Array.isArray(body) ? body : [body];
await writeUsers(users);
return NextResponse.json({ success: true, count: users.length });
} catch (err) {
console.error("Error writing users to DB:", err);
return NextResponse.json({ error: "Failed to write users" }, { status: 500 });
}
}
Now when you go to localhost:3000/users, it will give you an error because the users table does exist. So let’s create one.
In the database UI, click on “Studio”. You’ll get a visual editor for your database where you can manage your data directly (pretty cool, right?).
Press the “+” icon and choose “create table”. Create the table with the schema below. Click the “add column” link to create new columns.
Click “create table and you should see the table created as below:
Let’s add a dummy record using “add record” button to use it to test our API. The id field should be in UUID format (and you can generate one here).
Now let’s test our API.
You should see the user you created as the response to the localhost:3000/users query. Now let’s create a new user using our API.
We’ll use Postman for this since its easy to create POST requests using Postman. We’ll send a sample data under “body” → “raw” → “JSON”.
The response from Postman should be as below:
Now going to localhost:3000/users, you should see the new record created.
Great job! Now let’s get this app live.
Deploying to Sevalla
Push your code to GitHub or fork my repository. Now lets go to Sevalla and create a new app.
Choose your repository from the dropdown and check “Automatic deployment on commit”. This will ensure that the deployment is automatic every time you push code. Choose “Hobby” under the resources section.
Click “Create” and not “Create and deploy”. We haven’t added our PostgreSQL URL as an environment variable, so the app will crash if you try to deploy it.
Go to the “Environment variables” section and add the key “PGSQL_URL” and the URL in the value field.
Now go back to the “Overview” section and click “Deploy now”.
Once deployment is complete, click “Visit app” to get the live URL of your API. You can replace localhost:3000 with the new URL in Postman and test your API.
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
Conclusion
Next.js is no longer just a frontend framework. It’s a powerful full-stack platform that lets you build and deploy production-ready APIs with minimal friction. By pairing it with Sevalla’s developer-friendly infrastructure, you can go from local development to a live, cloud-hosted API in minutes.
In this tutorial, you learned how to set up a Next.js API project, connect it to a cloud-hosted PostgreSQL database on Sevalla, and deploy everything seamlessly. Whether you’re building a small side project or a full-scale application, this stack gives you the speed, structure, and scalability to move fast without losing flexibility.
Hope you enjoyed this article. I’ll see you soon with another one. You can connect with me here or visit my blog.
Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & MoreÂ