Serverless doesn’t mean there’s no server - it means you don’t manage the server or the infrastructure.
It’s a runtime (e.g., Node.js) spun up and torn down on demand. On Vercel’s hobby plan you get ~10s for a request/response cycle, up to 300s on Pro, and 800s on Fluid.
Moral of the story: if you’re doing something serverless, do it fast. Or break it up into smaller functions (FaaS: Function as a Service), which is exactly how, many modern APIs work.
One handy mantra: You pay for execution, not uptime.
Traditional hosting: you pay while the server is online.
Treat endpoints like quick, efficient functions and you’ll be fine, especially on Vercel (we’ve heard horror stories of surprise bills).
In this post, we’ll build a full-stack app (Node.js, Express, Supabase, and EJS) using Vercel, the most popular serverless platform.
Getting Started
Make a Vercel account
free tier works fine.Install the Vercel CLI:
npm i -g vercel
- Log in:
vercel login
Create a New Project Folder
npm init -y
npm i express @supabase/supabase-js ejs
npm i -D @types/node @vercel/node
Create an api
folder, each file here is an endpoint (index
is /
by default):
api/index.ts
Stub out a minimal Express app:
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.status(200).send("hello");
});
app.listen(3000, () => console.log("Server ready on port 3000."));
// Important: export for Vercel
module.exports = app;
Add vercel.json
Think of this as Vercel’s package.json
, it tells Vercel how to handle your project:
{
"version": 2,
"rewrites": [
{
"source": "/(.*)",
"destination": "/api"
}
]
}
Run the Dev Server
vercel dev
Follow the prompts:
? Link to existing project? no
? What’s your project’s name? basic
? In which directory is your code located? ./
If it worked, you’ll see:
Linked to [your-username]-projects/basic (created .vercel and added it to .gitignore)
Ready! Available at http://localhost:3000
That’s the full flow for spinning up a Vercel project.
Connecting Supabase
Mantra #2: Serverless pairs beautifully with edge databases, and Supabase is one of them.
Remember, you’ve only got a limited window between request and response. An edge database keeps latency low and plays perfectly in this setup.
Head over to your Vercel dashboard, pick the project you just created, and find the Storage tab. Select Supabase and follow the prompts, it’s a quick setup.
Once it’s ready:
- Connect, Open in Supabase → Table Editor → New Table -> links.
- Disable Row Level Security (RLS) for now, otherwise it’ll require auth (whole other rabbit hole for another post).
Vercel will automatically handle and store your environment variables. To use them locally, pull them down with:
vercel env pull .env.development.local
You should now see your Supabase keys and related env vars in that file.
Deploying
When you’re ready to ship your first version:
vercel
And that’s it, you’ve got a working FaaS (Function-as-a-Service) app live on Vercel.
Next, let’s level up and build a Linktree clone, the “Hello World” of full-stack apps.
Building a Temu Linktree
Now that Supabase is hooked up and your backend is talking to a database, you are ready to put it to use by building something a bit more fun, a Linktree clone.
The “Hello World” of full-stack apps: a simple UI, a few forms, and some database interactions. Perfect for practicing the serverless + edge DB pattern we just set up.
Quick note on EJS:
EJS is our templating engine for this project. It’s… verbose. HTML is verbose. For example, my add-link.ejs
file is over 600 lines (yes, v0 can get a bit excessive).
To save time, just grab the views
folder from the repo. The templates aren’t the star of the show, your API is.
At this point you should have a project structure like this:
api
views
EJS works by taking special syntax in your .ejs
files, running it through a parser, and outputting HTML with the values injected at runtime.
Example:
<div class="profile-avatar">
<%= (user.username || 'U').charAt(0).toUpperCase() %>
</div>
<h1>@<%= user.username || 'user' %></h1>
<p><%= user.bio || 'Welcome to my link collection!' %></p>
When rendered, those <%= ... %>
bits will be replaced with actual values from your backend.
Frontend’s ready, let’s hook up the backend.
Backend Setup
Open index.ts
and extend your Express app:
import path from "path";
import { createClient } from '@supabase/supabase-js';
const app = express();
app.use(express.urlencoded({ extended: true }));
Create the Supabase client
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY;
if (!SUPABASE_URL) throw new Error('Missing env var SUPABASE_URL');
if (!SUPABASE_ANON_KEY) throw new Error('Missing env var SUPABASE_ANON_KEY');
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
Register the views
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../views'));
Endpoints
Home page:
app.get("/", (req, res) => {
res.render('add-link', {
user: { username: "sk" },
formData: {}, // prevents formData.title from blowing up
errors: {},
baseUrl: process.env.VERCEL_URL
});
});
That object is passed into add-link.ejs
so it can dynamically render HTML.
Add link:
app.post("/links/new", async (req, res) => {
const data = req.body;
const errors = validate(data);
if (Object.keys(errors).length) {
return res.render('add-link', {
user: req.user,
formData: data,
errors
});
}
try {
await saveLink(data); // helper function below
return res.status(302).setHeader('Location', '/dashboard').end();
} catch (err) {
console.error('saveLink failed:', err);
return res.status(500).json({
success: false,
message: err instanceof Error ? err.message : 'Unknown error'
});
}
});
Save link & validation:
async function saveLink(link) {
const { data, error } = await supabase
.from('links')
.insert([{
title: link.title,
url: link.url,
shortCode: link.shortCode,
category: link.category,
description: link.description
}])
.select();
if (error) {
console.error('Supabase insert error:', error);
throw new Error(error.message);
}
return data;
}
function validate(data) {
const errs = {};
if (!data.title) errs.title = 'Title is required';
// ...etc...
return errs;
}
Dashboard:
app.get("/dashboard", async (req, res) => {
let { data: links, error } = await supabase
.from('links')
.select('*');
if (error) {
return res.status(500).json({
success: false,
message: error instanceof Error ? error.message : 'Unknown error'
});
}
res.render('display', {
user: { username: "sk" },
totalClicks: 10,
links
});
});
Navigate to a link:
app.get('/navigate/:nav', (req, res) => {
const raw = req.params.nav;
// Always validate & sanitize here — skipping this = trouble
let url;
try {
url = new URL(raw);
} catch {
return res.status(400).send('Invalid URL');
}
if (!['http:', 'https:'].includes(url.protocol)) {
return res.status(400).send('Only HTTP/S allowed');
}
return res.redirect(url.toString());
});
And there you have it, the most basic full-stack “Hello World.” Nearly any production-grade app will follow this same pattern:
- render a page
- accept data from a form
- store it
- show it back to the user
Test it locally, and when it’s ready:
vercel
You’ve now gone from serverless fundamentals → edge database integration → working full-stack app in one smooth flow.
Want to add Auth? How To Setup Auth With Vercel Serverless and Supabase.
Ever wondered what it really takes to build low-level Node.js tooling or distributed systems from scratch?
- Learn raw TCP
- Go over a message broker in pure JavaScript
- Go from "can code" to "can engineer"
Check out: How to Go from a 6.5 to an 8.5 Developer
—
Or maybe you're ready to master the dark art of authentication?
- From salting and peppering to mock auth libraries
- Understand tokens, sessions, and identity probes
- Confidently use (or ditch) auth-as-a-service or roll out your own?
Grab: The Authentication Handbook: With Node.js Examples
thanks for reading. 🙏
Top comments (3)
For "intense work" checkout the fluid compute offer:
vercel Fluid Compute
Add Supabase Auth:
How To Setup Auth With Vercel Serverless and Supabase.
Auth at the Edge.
Really clear walkthrough — I like how you tie the serverless mindset (“do it fast”) to practical implementation on Vercel. The Supabase integration and Linktree example make the concepts stick, and the tips on environment variables and validation are a nice touch for anyone going beyond a demo.
interesting information, thank you