In the first post of this series, we kept things simple and set up authentication with Supabase in a React + TypeScript app. We covered signup, login, and logout flows styled with Tailwind CSS.
In this post, we'll go a step further and explore how to write custom APIs using Supabase Edge Functions and consume them in React. This is where Supabase starts to shine because you're not limited to just auth and database, you can extend it just like a traditional backend.
What are Edge Functions?
Supabase Edge Functions are serverless functions powered by Deno that run at the network edge, close to end users, to process API requests, personalize content, or handle dynamic logic with minimal latency.
- They are fast (deployed globally to the edge).
- Secure – run with role-based access and API keys.
- Flexible – you can handle business logic, external APIs, webhooks, or even transform data before it reaches your frontend.
Step 1: Setting up Supabase CLI
First, install the Supabase CLI if you don’t already have it:
npm install supabase --save-dev
Log in to your Supabase account:
npx supabase login
And link your project:
npx supabase link --project-ref your-project-ref
Step 2: Create an Edge Function
Here we will write a basic api to get all users list. Let's go!
Create a function called get-all-users:
npx supabase functions new get-all-users
This will generate a functions/get-all-users/index.ts file. Open it and add:
functions/get-all-users/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
// CORS header helper
function getCorsHeaders(origin: string) {
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
}
// Initialize Supabase client with environment variables
const supabase = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_ANON_KEY") ?? ""
);
serve(async (req) => {
const origin = req.headers.get("origin") || "*";
// Handle preflight CORS
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: getCorsHeaders(origin),
});
}
try {
// Extract userId from query params (example: /?userId=123)
const url = new URL(req.url);
const userId = url.searchParams.get("userId");
if (!userId) {
return new Response(JSON.stringify({ error: "Missing userId" }), {
status: 400,
headers: getCorsHeaders(origin),
});
}
// Query Supabase to get all users list
const { data, error } = await supabase
.from("users")
.select("*")
.eq("auth_user_id", userId);
if (error) throw error;
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
...getCorsHeaders(origin),
},
});
} catch (err) {
return new Response(JSON.stringify({ error: String(err.message) }), {
status: 500,
headers: getCorsHeaders(origin),
});
}
});
Deploy it:
npx supabase functions deploy get-all-users
Step 3: Call the Function from React
In your React app, you can call this function using Supabase client:
src/api/users.ts
import { supabase } from "./supabaseClient";
export const getAllUsers = async () => {
const { data, error } = await supabase.functions.invoke("get-all-users");
if (error) {
console.error("Error calling get-all-users:", error);
return null;
}
return data;
};
And use it in your component:
src/components/Users.tsx
import React, { useEffect, useState } from "react";
import { getAllUsers } from "../api/users";
interface User {
id: string;
full_name: string;
email: string;
dob?: string;
profession?: string;
}
const Users: React.FC = () => {
const [users, setUsers] = useState<User | undefined>("");
useEffect(() => {
const fetchAllUsers = async () => {
const data = await getAllUsers();
if (data) setUsers(JSON.parse(JSON.stringify(data)));
};
fetchAllUsers();
}, []);
return (
<div className="overflow-x-auto rounded-lg shadow-md border border-gray-200">
<table className="min-w-full border-collapse bg-white text-sm">
<thead>
<tr className="bg-gray-100 text-left">
<th className="px-4 py-2 border-b">Name</th>
<th className="px-4 py-2 border-b">Email</th>
<th className="px-4 py-2 border-b">Date of Birth</th>
<th className="px-4 py-2 border-b">Profession</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr
key={user.id}
className="hover:bg-gray-50 transition-colors duration-150"
>
<td className="px-4 py-2 border-b">{user.full_name}</td>
<td className="px-4 py-2 border-b">{user.email}</td>
<td className="px-4 py-2 border-b">{user.dob || "N/A"}</td>
<td className="px-4 py-2 border-b">{user.profession || "N/A"}</td>
</tr>
))}
</tbody>
</table>
</div> );
};
export default Users;
Step 4: Securing Edge Functions
By default, functions are secured and require a service role or authenticated user token.
You can make a function public by adding this to supabase/config.toml:
[functions.get-all-users]
verify_jwt = false
Then redeploy:
npx supabase functions deploy get-all-users
Real-world Use Cases
Edge Functions are great for:
Sending transactional emails (e.g., SendGrid, Resend).
Integrating 3rd party APIs securely.
Running cron jobs or scheduled tasks.
Custom data transformations before saving to PostgreSQL.
Wrapping up
In this post, we learned how to:
Create and deploy a Supabase Edge Function.
Call it from a React + TypeScript app.
Secure or expose it depending on your use case.
Edge Functions give your Supabase project the flexibility of a full backend, while keeping the simplicity of BaaS.
In the next post, we’ll explore Realtime Channels to build live notifications and updates.
If you found this useful, please share it with your dev friends!
Top comments (0)