What you'll build
In this part you'll connect your Express API to a Supabase PostgreSQL database and build a full set of CRUD endpoints for products and categories.
By the end of this part your API will support:
GET /products— list all products with optional category filter and paginationGET /products/:id— get a single product by IDPOST /products— create a product (validates body with Zod)PUT /products/:id— update a productDELETE /products/:id— delete a productGET /categoriesandPOST /categories
Set up Supabase
Go to app.supabase.com, create a project, and run the following in the SQL Editor:
create table categories (
id uuid primary key default gen_random_uuid(),
name text not null unique,
slug text not null unique,
created_at timestamptz default now()
);
create table products (
id uuid primary key default gen_random_uuid(),
name text not null,
description text,
price_ghs numeric(10,2) not null,
sku text unique,
stock integer not null default 0,
category_id uuid references categories(id) on delete set null,
images text[] default '{}',
is_active boolean default true,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Keep updated_at current automatically
create or replace function update_updated_at()
returns trigger as $$
begin new.updated_at = now(); return new; end;
$$ language plpgsql;
create trigger products_updated_at
before update on products
for each row execute function update_updated_at();
Seed some test data:
insert into categories (name, slug) values
('Electronics', 'electronics'),
('Clothing', 'clothing'),
('Food & Drink', 'food-drink');
insert into products (name, description, price_ghs, sku, stock, category_id)
select 'Bluetooth Speaker', 'Portable waterproof speaker with 12h battery', 250.00, 'SPK-001', 15, id
from categories where slug = 'electronics';
Copy your Project URL and service_role key from Settings → API into .env.
Install the client:
npm install @supabase/supabase-js
Build the Supabase client
Create src/lib/supabase.ts. Initialise a Supabase client with SUPABASE_URL and SUPABASE_SERVICE_KEY from process.env. Throw a clear error at startup if either is missing. Export the client as supabase.
Build the products router
Create the products CRUD layer for the product catalog API.
1. src/types.ts — define and export:
- ProductRow: { id: string; name: string; description: string | null; price_ghs: number; sku: string | null; stock: number; category_id: string | null; images: string[]; is_active: boolean; created_at: string; updated_at: string }
- CategoryRow: { id: string; name: string; slug: string; created_at: string }
2. src/controllers/products.controller.ts — export async functions:
- listProducts(req, res, next): query supabase.from("products").select("*, categories(name, slug)"). Support ?category=
Build the categories router
Create src/controllers/categories.controller.ts with listCategories and createCategory. Create src/schemas/category.schema.ts with CreateCategorySchema: { name: string (min 1), slug: string (lowercase, no spaces — use .regex(/^[a-z0-9-]+$/)) }. Create src/routes/categories.router.ts. Mount both routers in src/index.ts under /products and /categories.
Test all endpoints
Start the server (npm run dev) and test:
# List products
curl http://localhost:3000/products
# Get one product (replace ID with an actual UUID from the list)
curl http://localhost:3000/products/<id>
# Create a product
curl -X POST http://localhost:3000/products \
-H "Content-Type: application/json" \
-d '{"name":"Test Item","price_ghs":99.99,"stock":10}'
# Filter by category
curl "http://localhost:3000/products?category=electronics"
# Validation error (missing required field)
curl -X POST http://localhost:3000/products \
-H "Content-Type: application/json" \
-d '{"description":"no name provided"}'
The last command should return a 400 with { "error": "Validation failed", "details": [...] }.
Project structure after Part 2
src/
controllers/
products.controller.ts
categories.controller.ts
lib/
supabase.ts
middleware/
errorHandler.ts
validate.ts
routes/
health.router.ts
products.router.ts
categories.router.ts
schemas/
product.schema.ts
category.schema.ts
types.ts
index.ts
What's next
In Part 3 we add user registration and login with hashed passwords and JWT tokens. Protected routes (create, update, delete) will require a valid token in the Authorization header.
BlocWeave