Skip to content

Querying with the Client SDK

supabase-js’s query builder is a fluent TypeScript wrapper that builds the exact same PostgREST request you could write by hand, and every call resolves to a { data, error } object instead of throwing.

Everything you saw as a raw query string in the previous lesson has a matching method on the query builder. Pick a table with .from(), choose columns with .select(), filter with .eq() (and its siblings), and sort with .order():

const { data, error } = await supabase
.from('movies')
.select('id, title, year')
.eq('year', 2020)
.order('year', { ascending: false });

Writes work the same way — .insert(), .update(), and .delete() map to the corresponding HTTP verbs PostgREST exposes:

await supabase.from('movies').insert({ title: 'Arrival', year: 2016, director_id: 3 });
await supabase.from('movies').update({ year: 2021 }).eq('id', 1);
await supabase.from('movies').delete().eq('id', 1);

A supabase-js query never throws on a failed request — it always resolves with an object shaped { data, error }. If the request succeeds, error is null and data holds the rows. If it fails, data is null and error is populated. Because there is no exception to catch, you must check error yourself on every call, or a failed request will silently look like an empty result.

error is a PostgrestError with four fields worth knowing:

  • message — a human-readable description of what went wrong.
  • code — either a Postgres error code (e.g. a unique-constraint violation) or a PostgREST-specific code.
  • details — additional context from Postgres, when available.
  • hint — a suggestion for fixing the problem, when Postgres or PostgREST can offer one.
const { data, error } = await supabase.from('movies').select('id, title').eq('year', 2020);
if (error) {
console.error(`query failed [${error.code}]: ${error.message}`);
} else {
console.log(data);
}
Section titled “Embedding related tables through the builder”

Just like the raw REST API, the query builder can embed a related table by naming it inside select() — PostgREST still does the work by following the foreign key, the builder just gives you a typed, chainable way to ask for it:

const { data, error } = await supabase.from('movies').select('title, directors(name)');
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);
async function recentMovies() {
const { data, error } = await supabase
.from('movies')
.select('id, title, year, directors(name)')
.eq('year', 2020)
.order('title', { ascending: true });
if (error) {
console.error('failed to load movies:', error.message);
return [];
}
return data;
}
flowchart LR
  from[".from('movies')"] --> select[".select('id, title, year')"]
  select --> eq[".eq('year', 2020)"]
  eq --> order[".order('year')"]
  order --> http["One HTTP request to PostgREST"]
  http --> result["Result: data and error"]
Building one PostgREST request from chained calls
What shape does a supabase-js query result always have?
Why must you check the error field explicitly instead of relying on a try/catch?
Which combination filters movies to year 2020 and sorts by year descending?
How does the query builder embed a related directors table into a movies query?