DEV Community

Dhairya Bhargava
Dhairya Bhargava

Posted on

How to Build a Clean Node.js REST API with Express and Supabase

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 .env file into process.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
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Decoupled Architecture: Express handles request validation, error formatting, and routing logic, while Supabase provides managed PostgreSQL persistence.
  2. Explicit Error Handling: Always inspect the { error } object returned by the Supabase client before dispatching JSON responses to ensure client feedback is informative.
  3. 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)