What Vercel does after you git push

I take a small Express API from my laptop to a live URL with GitHub and Vercel, and look at what every push creates on the way.

One push, from my terminal to the first request

Every small app gets to the same point. It works on my laptop, and now I need a URL that other people can call. For a small Express API, the shortest path I know is a GitHub repo and Vercel. I never log into a server, and I don't write a line of YAML.

The animation above is one push, from my terminal to the first request that reaches the new version. What makes it work is that every push becomes its own deployment with its own URL, and the production domain just points at one of them.

The app

A tiny REST API that returns a random developer quote. It's one file, app.js:

import express from "express";
const app = express();

const quotes = [
  "Simplicity scales.",
  "Automate what hurts.",
  "You don't need Kubernetes for your portfolio site."
];

app.get("/", (req, res) => res.json({
  quote: quotes[Math.floor(Math.random() * quotes.length)]
}));

app.listen(3000, () => console.log("Server running on port 3000"));

Put it on GitHub

In the same folder:

npm init -y
npm install express

Then add two fields to package.json, so Node reads import and npm start runs the app:

{
  "type": "module",
  "scripts": {
    "start": "node app.js"
  }
}

Add a .gitignore with node_modules and .env in it, and push the folder to a new GitHub repository.

Import it into Vercel

In the Vercel dashboard I create a new project, import the repo and press Deploy. There's nothing to configure. Vercel looks for a file like app.js, index.js or server.js that imports Express, and it's fine with app.listen(3000) [1]. The whole app becomes one Vercel Function, so every route runs in the same function. The one thing that doesn't carry over is express.static(): static files go in a public/ folder instead.

What a push creates

From here on, every push to main is a deploy. GitHub tells Vercel about the commit, Vercel builds it, and the result is a deployment: a copy of the app that never changes, with its own URL like quotes-api-x8f2q5v7c-me.vercel.app [2]. When it's ready, the production domain moves to it. The older deployments stay where they were, each still reachable at its own URL.

A push to any other branch builds a preview deployment. It gets its own URLs too, including one that always shows the latest commit on that branch, like quotes-api-git-feature-me.vercel.app. The production domain doesn't move [3]. The buttons under the animation do the same things, so you can try them in any order.

Pushes, a preview and a rollback

This is why rolling back is instant. Instant Rollback doesn't rebuild anything. It points the production domain at an older deployment that's still there [4]. On the free Hobby plan you can only go back to the deployment right before the current one. On Pro you can pick any earlier production deployment.

There's one catch I didn't expect. After a rollback, Vercel stops moving the domain on its own, so the next push to main builds but doesn't go live, and it can't quietly replace the version you rolled back to. Undo the rollback, or promote a deployment by hand, and pushes go live again [4].

A secret that stays secret

Environment variables live in the project settings, or you can add one from the CLI:

vercel env add API_KEY production

Vercel keeps them encrypted at rest, and a change only reaches new deployments [5]. So after adding a key you redeploy, and until then the live deployment runs without it.

Here's a route that uses the key. It stays on the server and only checks the header the caller sends, so no response ever contains it:

app.get("/secure", (req, res) => {
  const key = process.env.API_KEY;
  if (!key || req.get("x-api-key") !== key) {
    return res.status(401).json({ error: "unauthorized" });
  }
  res.json({ quote: "Only for callers with the key." });
});

The !key part matters more than it looks. Without it, a deployment built before the key existed compares undefined with undefined when a request comes in with no header, and lets it through.

Adding a key, and why it needs a redeploy

To call it:

curl -H "x-api-key: $API_KEY" https://quotes-api.vercel.app/secure

Logs

Every request shows up in the Logs tab with its status code and anything the app writes with console.log, up to 256 lines per request [6]. On the Hobby plan Vercel keeps them for an hour, and on Pro for a day. For anything older you need a log drain to another service, which is a Pro feature [7].

What it costs

Hobby is free and covers a lot for a side project: a million function invocations, 4 hours of active CPU and 100 GB of data transfer a month [7]. It's for personal, non-commercial use only, though. Once the app takes payments, runs ads or sells something, Vercel counts it as commercial, and that needs Pro, at $20 a month per developer [8].

Sources

  1. Vercel, “Express on Vercel”, Vercel documentation.
  2. Vercel, “Accessing Deployments through Generated URLs”, Vercel documentation.
  3. Vercel, “Deploying to Vercel”, Vercel documentation.
  4. Vercel, “Performing an Instant Rollback on a Deployment”, Vercel documentation.
  5. Vercel, “Environment variables”, Vercel documentation.
  6. Vercel, “Runtime Logs”, Vercel documentation.
  7. Vercel, “Vercel Hobby Plan”, Vercel documentation.
  8. Vercel, “Fair Use Guidelines”, Vercel documentation. See Commercial usage.

Get the next article by email

I send one email when a new article is out, and nothing else.

Keep reading

You type a URL and press Enter

Soon

Everything that happens before the first pixel shows up.

Under the Hood

Sending a message

Soon

What your phone, the server and the other phone do between sent and read.

Under the Hood