Skip to content

The Version Endpoint

A heartbeat endpoint answers one useful question:

is the app alive?

Good.

But once deployments start happening regularly, another question shows up almost immediately:

what version of the app is actually running right now?

That question matters more than people expect.

If you deploy multiple times in a day and someone says:

  • “I think the fix is live”
  • “I’m still seeing the old behavior”
  • “That bug only started after the latest deploy”

then “the app is healthy” is not enough.

We also need the application to identify itself.

As soon as a team starts deploying more than once in a blue moon, ambiguity creeps in fast.

Questions start popping up like:

  • did the new deployment actually finish?
  • is the hosted app running the latest build?
  • is the browser showing stale assets?
  • are we debugging the current version or yesterday’s version?

A version endpoint gives us a direct answer instead of forcing us into vibes, guesswork, and dramatic finger-pointing.

That makes it a very small feature with a surprisingly large payoff.


Just like the heartbeat route, the version route should be small and focused.

Let’s add one to Voyagers Log right now. Open server/index.js and add this code exactly below our /api/health route:

app.get('/api/version', (req, res) => {
res.json({
app: 'voyagers-log-api',
release: process.env.RENDER_GIT_COMMIT || 'local-dev',
});
});

Now if we hit /api/version, the API returns a tiny payload telling us which release the app believes it is running.

That is exactly the kind of operational clue we want.

Operational Identity

A version endpoint gives the app a way to quietly answer a very practical support question: “which build am I looking at right now?” That saves a massive amount of confusion during fast-moving deploy cycles.


Notice that the route does not hardcode a version number (v1.4.2) into the source code.

Instead, it reads:

process.env.RENDER_GIT_COMMIT

Why?

Because version identity is deployment metadata. Hardcoding it requires making an intentional code edit just to update a version string, which developers invariably forget to do.

Instead, we lean on the platform. Render automatically injects the RENDER_GIT_COMMIT environment variable into every web service it builds. This means the deployed application always knows exactly which Git commit triggered its build.

If we run the app locally, that variable is undefined, so our code gracefully falls back to the 'local-dev' string.

That makes this endpoint completely maintenance-free while providing perfect traceability back to our GitHub history.


A version endpoint becomes useful any time deployment ambiguity appears.

For example:

  • “Did today’s hotfix actually reach production?”
  • “Are we still hitting the previous release?”
  • “Did Render redeploy the latest commit yet?”
  • “Is this screenshot from the current app or an older build?”

Without a version endpoint, those questions often lead to annoying detective work.

With one, the workflow becomes much simpler:

  1. hit /version
  2. read the release identifier
  3. compare it to the release you expected

That is wonderfully boring.

And boring is excellent in operations.

Do Not Turn This into an Information Dump

A version endpoint should return a small tracking value that is meaningful to the team. Do not use it as an excuse to dump internal dependency versions, framework details, secrets, or environment metadata that the public does not need to see.


A version endpoint is very useful, but let’s keep it honest.

If /version returns the expected release string, that tells us:

  • the app is reachable
  • the route is working
  • the deployment identity looks correct

It does not automatically prove that:

  • every feature is behaving correctly
  • the frontend assets are fresh in the browser cache
  • Atlas is healthy
  • every route in the app is functioning properly

So just like the heartbeat route, this endpoint answers a specific question.

It does not answer all of them.

That is fine. Small, focused signals are exactly what we want.


At this point, we now have two tiny operational endpoints that work really well together.

Answers:

  • is the app alive and responding?

Answers:

  • which release is actually running?

Those are two of the most practical questions you can ask a live service.

And they are both answered with tiny, very manageable bits of code.

That is a great trade.


So far, our visibility story looks like this:

  • startup logs tell us the app woke up
  • request logs tell us traffic is arriving
  • error logs tell us what failed
  • /health tells us whether the app is alive right now
  • /version tells us which release is running

That is a very respectable first layer of production awareness.

Not a giant monitoring empire. Not observability theater. Just a set of signals that make the app much easier to understand and support.

Small Signals, Big Clarity

None of these additions are complicated. That is part of the point. Production awareness often begins with small, deliberate signals that reduce confusion rather than giant systems that try to solve everything at once.


There is one catch with using a Git commit hash like a1b2c3d as your version identifier:

To a human, it is just a random string of characters.

If a user complains that “the new dark mode is broken,” hitting the /api/version endpoint to discover the app is running commit a1b2c3d only helps if you know what was shipped in that commit.

This is why operational visibility extends beyond the codebase and into genuine team communication. A Git commit hash is only useful when tied to a closely managed Change Log or GitHub Release.

A CHANGELOG.md in your repository—or an automated release draft in GitHub—is where we translate Git commits into human context:

  • Added: Dark Mode toggle
  • Fixed: Database timeout on the Voyage submission form
  • Changed: Updated Tailwind config for primary colors

When we combine a version endpoint returning a1b2c3d with a Change Log that says “Commit a1b2c3d introduced Dark Mode,” we close the operational loop. We have absolute certainty about what code is running and exactly what features that code contains.


Semantic Versioning

Knowing what version is live is useful. Next, we zoom out one level further and connect application behavior to deployment history so we can answer another critical question: what changed?