How to Build a REST API with Node.js, Express and PostgreSQL
Every modern web application needs a backend API. Whether you are building a task manager, a blog engine, a society management portal, or an e-commerce store, the pattern is the same: your frontend talks to an API, and the API talks to a database.
In this tutorial, we will build a complete REST API using Node.js and Express, connected to a PostgreSQL database using Supabase. By the end, you will have a working API that can create, read, update, and delete data — the foundation for any full stack application.
What We Are Building
We will build a simple notes API. Users can sign up, log in, and manage their personal notes. This covers the most common API patterns you will use in real projects.
Step 1: Project Setup
Create a new directory and initialize a Node.js project:
mkdir notes-api
cd notes-api
npm init -y
npm install express cors dotenv pg bcrypt jsonwebtoken
npm install -D nodemon
This installs Express for the server, cors for cross-origin requests, dotenv for environment variables, pg for PostgreSQL, bcrypt for password hashing, and jsonwebtoken for authentication tokens.
Create a .env file:
PORT=3000
DATABASE_URL=your_supabase_connection_string
JWT_SECRET=your_random_secret_key
Step 2: Database Setup with Supabase
Sign up for a free Supabase account at supabase.com. Create a new project. Once it is ready, go to the SQL Editor and run this query to create our notes table:
CREATE TABLE notes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
user_id UUID REFERENCES auth.users(id) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
If you are not using Supabase Auth, create a simple users table instead:
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Copy your connection string from Supabase Settings → Database → Connection string and paste it into your .env file.
Step 3: Connect to PostgreSQL
Create a db.js file to manage the database connection:
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
module.exports = {
query: (text, params) => pool.query(text, params),
};
Step 4: Build the Express Server
Create an index.js file:
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.json());
const notesRouter = require('./routes/notes');
const authRouter = require('./routes/auth');
app.use('/api/auth', authRouter);
app.use('/api/notes', notesRouter);
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Something went wrong' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 5: Create the Auth Routes
Create a routes/auth.js file for user registration and login:
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const db = require('../db');
const router = express.Router();
router.post('/register', async (req, res) => {
const { email, password } = req.body;
const hashed = await bcrypt.hash(password, 10);
const result = await db.query(
'INSERT INTO users (email, password) VALUES ($1, $2) RETURNING id, email',
[email, hashed]
);
const token = jwt.sign({ id: result.rows[0].id }, process.env.JWT_SECRET);
res.status(201).json({ user: result.rows[0], token });
});
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const result = await db.query('SELECT * FROM users WHERE email = $1', [email]);
const user = result.rows[0];
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET);
res.json({ user: { id: user.id, email: user.email }, token });
});
module.exports = router;
Step 6: Create the Notes Routes (CRUD)
Create a middleware/auth.js file to protect routes:
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
const header = req.headers.authorization;
if (!header) return res.status(401).json({ error: 'No token provided' });
try {
const decoded = jwt.verify(header.split(' ')[1], process.env.JWT_SECRET);
req.userId = decoded.id;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
};
Now create routes/notes.js:
const express = require('express');
const db = require('../db');
const auth = require('../middleware/auth');
const router = express.Router();
router.use(auth);
router.get('/', async (req, res) => {
const result = await db.query(
'SELECT * FROM notes WHERE user_id = $1 ORDER BY created_at DESC',
[req.userId]
);
res.json(result.rows);
});
router.post('/', async (req, res) => {
const { title, content } = req.body;
const result = await db.query(
'INSERT INTO notes (title, content, user_id) VALUES ($1, $2, $3) RETURNING *',
[title, content, req.userId]
);
res.status(201).json(result.rows[0]);
});
router.put('/:id', async (req, res) => {
const { title, content } = req.body;
const result = await db.query(
'UPDATE notes SET title = $1, content = $2, updated_at = NOW() WHERE id = $3 AND user_id = $4 RETURNING *',
[title, content, req.params.id, req.userId]
);
if (!result.rows.length) return res.status(404).json({ error: 'Note not found' });
res.json(result.rows[0]);
});
router.delete('/:id', async (req, res) => {
const result = await db.query(
'DELETE FROM notes WHERE id = $1 AND user_id = $2 RETURNING id',
[req.params.id, req.userId]
);
if (!result.rows.length) return res.status(404).json({ error: 'Note not found' });
res.json({ message: 'Note deleted' });
});
module.exports = router;
Step 7: Test Your API
Start the server:
npm start
Test the endpoints using curl or Postman:
# Register a user
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"mypassword"}'
# Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"mypassword"}'
# Create a note (use the token from login)
curl -X POST http://localhost:3000/api/notes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"title":"My first note","content":"Hello world!"}'
# Get all notes
curl http://localhost:3000/api/notes \
-H "Authorization: Bearer YOUR_TOKEN"
Step 8: Deploy the API
Deploy your Express API on Render (render.com) for free:
- Push your code to a GitHub repository
- In Render dashboard, click "New +" → "Web Service"
- Connect your GitHub repository
- Set the start command:
node index.js - Add environment variables (DATABASE_URL, JWT_SECRET) in Render dashboard
- Deploy
Your API will be live at a .onrender.com URL within minutes.
Conclusion
You now have a production-ready REST API with authentication, database integration, and full CRUD operations. This is the same pattern used by real-world applications. You can extend it with more features: file uploads, pagination, search, email notifications, or real-time updates with WebSockets.
Building APIs is a core full stack skill. Practice by building different backends for your project ideas. Need help with your API or full stack project? Contact Aarti Tech Services for development support and mentorship.