How to Wire React to Supabase CRUD (Using the pg Client)
Supabase wraps Postgres with an auto-generated REST layer, but the underlying CRUD shape is just SQL. Building that layer yourself with the Node.js pg client gives you full control over queries and a drop-in swap path to @supabase/supabase-js when you're ready.
The CRUD shape Supabase mirrors
Supabase's PostgREST methods (supabase.from('profiles').select(), .insert(),
.update(), .delete()) each map directly to a SQL statement your pg client can issue.
Building the layer yourself first means you understand exactly what the abstraction does -
and swapping clients later is just changing the connector, not the logic.
For a profiles table (id INT PK, name TEXT, bio TEXT), the four functions are:
const { Client } = require("pg");
async function _client() {
const c = new Client({ host: "127.0.0.1", database: "app", user: "postgres" });
await c.connect();
return c;
}
async function createProfile(id, name, bio) {
const c = await _client();
try {
await c.query(
"INSERT INTO profiles (id, name, bio) VALUES ($1, $2, $3)",
[id, name, bio]
);
} finally { await c.end(); }
}
async function getProfile(id) {
const c = await _client();
try {
const r = await c.query("SELECT * FROM profiles WHERE id = $1", [id]);
return r.rows[0] || null;
} finally { await c.end(); }
}
async function updateProfile(id, name, bio) {
const c = await _client();
try {
const r = await c.query(
"UPDATE profiles SET name=$2, bio=$3 WHERE id=$1 RETURNING *",
[id, name, bio]
);
return r.rows[0] || null;
} finally { await c.end(); }
}
async function deleteProfile(id) {
const c = await _client();
try {
await c.query("DELETE FROM profiles WHERE id = $1", [id]);
} finally { await c.end(); }
}
module.exports = { createProfile, getProfile, updateProfile, deleteProfile };
Why parameterized queries ($1, $2, ...)
Never build SQL by concatenating user input. $1, $2 placeholders pass values
separately from the query string - the driver handles escaping, so SQL injection is
structurally impossible regardless of what the caller passes in.
Supabase's client enforces this automatically; rolling your own layer means you have to enforce it yourself. Every query above uses placeholders.
RETURNING * on UPDATE
Adding RETURNING * to the UPDATE statement gets the full updated row back in a single
round trip, so the caller doesn't need a second SELECT. This mirrors what Supabase returns
from .update().select().
Swap path to @supabase/supabase-js
Once the pg layer works, replacing it with the Supabase client is mechanical:
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// getProfile equivalent
const { data } = await supabase.from("profiles").select("*").eq("id", id).single();
The query logic stays the same - only the client changes. Production also layers Row-Level Security policies so each user only reads their own rows, and realtime channels to push changes to React state as they happen.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Writing INSERT, SELECT, UPDATE, and DELETE with parameterized queries via the pg client
- Using RETURNING * on UPDATE to retrieve the modified row in one round trip
- Exporting CRUD functions that mirror the Supabase PostgREST shape
FAQ
How do I implement Supabase CRUD with the pg client in Node.js?
Create a pg Client per operation, issue parameterized INSERT/SELECT/UPDATE/DELETE queries using $1/$2 placeholders, and export the functions. The shape matches what @supabase/supabase-js exposes, so swapping clients later is straightforward.
Why use parameterized queries instead of string concatenation in SQL?
Parameterized queries ($1, $2, ...) send the query and values separately - the driver handles escaping, making SQL injection structurally impossible. Never interpolate user-controlled values directly into a query string.
How do I get the updated row back after an UPDATE in PostgreSQL?
Add RETURNING * to the UPDATE statement. The pg client returns the updated row in r.rows[0], giving you the new data in one round trip without a separate SELECT.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →