Building a backend application usually involves spinning up a local PostgreSQL instance, configuring complex connection pools, and setting up an ORM like Prisma or TypeORM. While that architecture works for enterprise systems, lightweight microservices and prototypes often benefit from a leaner approach.
In this guide, we will build a minimal, production-ready REST API using Node.js, Express, and Supabase. By combining Express routing with the official @supabase/supabase-js client, you get full PostgreSQL persistence with zero database server maintenance.
Prerequisites and Database Setup
Before writing any JavaScript, ensure you have:
- Node.js (v18+) installed.
- An active Supabase project.
In your Supabase project's SQL Editor, run the following schema to establish a simple tasks table:
create table if not exists tasks (
id bigint generated always as identity primary key,
title text not null,
is_complete boolean default false,
created_at timestamptz default now()
);
-- For local development and testing, disable row level security:
alter table tasks disable row level security;
1. Project Initialization
Create a new project directory and install the required dependencies:
mkdir express-supabase-api
cd express-supabase-api
npm init -y
npm install express @supabase/supabase-js dotenv
Here is what each dependency handles:
-
express: Provides HTTP routing and request parsing middleware. -
@supabase/supabase-js: The official SDK communicating with your PostgreSQL instance over HTTPS. -
dotenv: Loads configuration keys from a.envfile intoprocess.env.
2. Environment Configuration
Create a .env file in the project root:
PORT=5000
SUPABASE_URL=[https://your-project-id.supabase.co](https://your-project-id.supabase.co)
SUPABASE_ANON_KEY=your-supabase-publishable-key
Never commit your API keys directly into source control. Always add .env to your .gitignore file.
3. Implementing the Express Server
Create server.js and structure the client initialization alongside clean CRUD routes:
require('dotenv').config();
const express = require('express');
const { createClient } = require('@supabase/supabase-js');
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware to parse incoming JSON bodies
app.use(express.json());
// Initialize the Supabase Client
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
);
// Health check route
app.get('/', (req, res) => {
res.status(200).json({ status: 'API running smoothly' });
});
// GET: Retrieve all tasks
app.get('/api/tasks', async (req, res) => {
try {
const { data, error } = await supabase
.from('tasks')
.select('*')
.order('id', { ascending: true });
if (error) return res.status(400).json({ error: error.message });
return res.status(200).json({ success: true, count: data.length, data });
} catch (err) {
return res.status(500).json({ error: 'Internal Server Error' });
}
});
// POST: Add a new task
app.post('/api/tasks', async (req, res) => {
const { title } = req.body;
if (!title || typeof title !== 'string') {
return res.status(400).json({ error: 'Title is required and must be a string' });
}
try {
const { data, error } = await supabase
.from('tasks')
.insert([{ title, is_complete: false }])
.select();
if (error) return res.status(400).json({ error: error.message });
return res.status(201).json({ success: true, data: data[0] });
} catch (err) {
return res.status(500).json({ error: 'Internal Server Error' });
}
});
app.listen(PORT, () => {
console.log(`Server live on http://localhost:${PORT}`);
});
4. Verifying Endpoints with Postman
Testing the POST Route
To verify database insertion, send a POST request to http://localhost:5000/api/tasks with raw JSON:
{
"title": "Verify Supabase connection"
}
The API responds with HTTP status 201 Created and returns the freshly generated record directly from PostgreSQL:
Testing the GET Route
Send a GET request to http://localhost:5000/api/tasks. The endpoint retrieves all stored rows in ascending order, confirming data integrity across server restarts:
Key Architectural Takeaways
- Decoupled Architecture: Express handles request validation, error formatting, and routing logic, while Supabase provides managed PostgreSQL persistence.
-
Explicit Error Handling: Always inspect the
{ error }object returned by the Supabase client before dispatching JSON responses to ensure client feedback is informative. - Server-Side Security: Rather than having client-facing web applications connect directly to the database, routing queries through an Express backend keeps database orchestration properly sandboxed.


Top comments (0)