Simple Postgres‑Backed CRUD App with Supabase & TypeScript

A 45‑minute beginner‑friendly clone of Supabase’s core CRUD features

Welcome to another official UVF IT build guide! In this tutorial you’ll spin up a tiny TypeScript‑based CRUD app that talks to a free‑tier Supabase Postgres database – the open‑source Firebase‑like platform that powers countless apps.

We’ll keep the stack lean: Node.js, the Supabase client library, and a single table for notes. Follow the five short phases and you’ll have a working API and a tiny UI in under an hour.

Before you start…

Component Overview
YOUR MACHINE SUPABASE CLOUD fetch API calls HTTP CRUD requests Node/TS Server Express + Supabase client Browser UI HTML + fetch Supabase Postgres Auto‑generated REST API
1

Phase 1: Scaffold the project

We start with a clean npm workspace so every dependency is tracked. TypeScript gives us static types, Express will host our tiny API, and the Supabase client lets us talk to the remote database.

All commands are run from your terminal in an empty folder where you want the project to live.

mkdir supabase-crud && cd supabase-crud
npm init -y
npm install express @supabase/supabase-js
npm install -D typescript ts-node-dev @types/express
npx tsc --init --rootDir src --outDir dist --esModuleInterop true --module commonjs --target es2020
2

Phase 2: Define the data model in Supabase

Create a free Supabase project at https://app.supabase.com and note the Project URL and anon public key – you’ll store them in a .env file later. Then add a simple notes table with three columns: id (uuid primary key), title (text), and content (text).

You can create the table via the Supabase dashboard UI or, if you have the CLI installed, run a single SQL command.

# Install the Supabase CLI if you haven't already
npm install -g supabase
# Log in to your Supabase account
supabase login
# Initialize a local project linked to your remote Supabase instance
supabase init --project-ref YOUR_PROJECT_REF
# Create the notes table using a raw SQL file (notes.sql)
echo "create table notes (id uuid default uuid_generate_v4() primary key, title text not null, content text);" > notes.sql
supabase db remote set
supabase db push -f notes.sql
3

Phase 3: Build the CRUD API

We spin up a tiny Express server that simply proxies requests to Supabase’s auto‑generated REST endpoint. This keeps the codebase tiny while still giving us full CRUD power over a "notes" table. The server reads the Supabase URL and anon key from environment variables, creates a Supabase client, and defines routes for create, read, update, and delete. By keeping everything in one file we make it easy to follow for beginners.

src/server.ts

import express from 'express';
import cors from 'cors';
import { createClient } from '@supabase/supabase-js';

const app = express();
app.use(cors());
app.use(express.json());

const supabaseUrl = process.env.SUPABASE_URL || '';
const supabaseKey = process.env.SUPABASE_ANON_KEY || '';
const supabase = createClient(supabaseUrl, supabaseKey);
const TABLE = 'notes';

// Create a note
app.post('/notes', async (req, res) => {
  const { title, content } = req.body;
  const { data, error } = await supabase.from(TABLE).insert({ title, content });
  if (error) return res.status(400).json({ error: error.message });
  res.status(201).json(data);
});

// Read all notes
app.get('/notes', async (_req, res) => {
  const { data, error } = await supabase.from(TABLE).select('*');
  if (error) return res.status(400).json({ error: error.message });
  res.json(data);
});

// Update a note
app.put('/notes/:id', async (req, res) => {
  const { id } = req.params;
  const { title, content } = req.body;
  const { data, error } = await supabase.from(TABLE).update({ title, content }).eq('id', id);
  if (error) return res.status(400).json({ error: error.message });
  res.json(data);
});

// Delete a note
app.delete('/notes/:id', async (req, res) => {
  const { id } = req.params;
  const { data, error } = await supabase.from(TABLE).delete().eq('id', id);
  if (error) return res.status(400).json({ error: error.message });
  res.json(data);
});

app.listen(3000, () => console.log('API listening on http://localhost:3000'));
4

Phase 4: Add a minimal UI

A single HTML page will call our local API to display notes and let you add new ones. The UI uses the Fetch API, so no extra libraries are required, keeping the bundle size at zero. When the page loads it fetches the note list, renders each entry, and provides a tiny form for creating notes. This demonstrates the full round‑trip from UI to Supabase and back.

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Supabase Notes</title>
  <style>body{font-family:sans-serif;padding:20px;}ul{list-style:none;padding:0;}li{margin:5px 0;}</style>
</head>
<body>
  <h1>Notes</h1>
  <ul id="notes"></ul>
  <h2>Add a note</h2>
  <form id="noteForm">
    <input type="text" id="title" placeholder="Title" required />
    <input type="text" id="content" placeholder="Content" required />
    <button type="submit">Add</button>
  </form>
  <script>
    const api = 'http://localhost:3000/notes';
    const notesEl = document.getElementById('notes');
    const form = document.getElementById('noteForm');

    async function loadNotes() {
      const res = await fetch(api);
      const notes = await res.json();
      notesEl.innerHTML = notes.map(n => `<li>${n.title}: ${n.content}</li>`).join('');
    }

    form.addEventListener('submit', async e => {
      e.preventDefault();
      const title = document.getElementById('title').value;
      const content = document.getElementById('content').value;
      await fetch(api, {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title,content})});
      form.reset();
      loadNotes();
    });

    loadNotes();
  </script>
</body>
</html>
5

Phase 5: Run & verify locally

Now we start the Express server and serve the static UI with a lightweight static file server. The API runs on port 3000 while the UI is served on port 4000, so they don’t clash. After launching both, open the UI in a browser, add a note, and watch it appear instantly—proving the full stack works end‑to‑end.

# Install dependencies (run once)
npm install express cors @supabase/supabase-js
# Compile TypeScript (if using ts-node, you can skip this)
npx tsc src/server.ts --outDir dist
# Start the API server
node dist/server.js &
# Serve the UI with a simple static server (you can use serve, http-server, or python)
npx serve public --port 4000
# The API is now listening on http://localhost:3000 and the UI on http://localhost:4000

Verifying the Stack

CheckCommand / Why it matters
Supabase env vars are set (SUPABASE_URL & SUPABASE_ANON_KEY)echo $SUPABASE_URL && echo $SUPABASE_ANON_KEY
GET /notes returns a JSON array (empty at first)curl http://localhost:3000/notes
UI at http://localhost:4000 shows the note list and can add a noteOpen the URL in a browser, add a note, and see it appear

You did it!

Your minimal Supabase‑backed CRUD app is now running locally. Extend it with auth, real‑time listeners, or deploy the server to Vercel for a full‑stack demo.