DEV Community

Cover image for I Lost 2 Days to a Deployment Error That Had a 5-Line Fix
MD. HABIBULLAH SHARIF
MD. HABIBULLAH SHARIF

Posted on • Originally published at Medium

I Lost 2 Days to a Deployment Error That Had a 5-Line Fix

A full-stack developer's honest confession about TypeScript, Node.js, and the rabbit hole nobody warns you about.


It was supposed to be a simple task.

Deploy the backend. Maybe 30 minutes. Grab a coffee, call it done.

That was Monday morning. By Tuesday night, I was debugging at midnight, staring at a 500: FUNCTION_INVOCATION_FAILED error on Vercel, wondering where my life went wrong.


The Setup (That Looked Totally Fine)

I was finishing up MediStore — a full-stack medicine management system. Express, TypeScript, Prisma, PostgreSQL. Modern stack, clean code, everything felt solid during development.

For dev mode, I had a nice little setup:

tsx watch src/server.ts
Enter fullscreen mode Exit fullscreen mode

Hot reload, TypeScript support out of the box. Bliss. I was flying.

Then came deployment day.


The First Wall: tsc and Node Don't Get Along Like You Think

My first instinct was obvious — compile with tsc, then run with node.

tsc && node dist/server.js
Enter fullscreen mode Exit fullscreen mode

Simple. Logical. Completely broken.

The error? Node couldn't find packages. Imports were failing. ESM vs CJS was exploding. I tried Node v24. Tried v25. Same thing, different error messages mocking me in different fonts.

Here's the thing nobody puts in a bold warning box anywhere: Node.js cannot natively run TypeScript. tsc compiles it, sure — but the output can still fail spectacularly depending on your module system, how your imports are written, and what phase of the moon it is.

My tsconfig.json had "module": "esnext" and "type": "module" in package.json. Sounds right. Felt right. Was a nightmare.


The Second Wall: Vercel Wanted Something Specific

While fighting the local build, I pushed what I had to Vercel anyway. Optimism. You know how it is.

500: INTERNAL_SERVER_ERROR
Code: FUNCTION_INVOCATION_FAILED
Enter fullscreen mode Exit fullscreen mode

Then I tried a different deployment strategy. Different URL, same pain:

404: NOT_FOUND
Code: NOT_FOUND
Enter fullscreen mode Exit fullscreen mode

At some point I wasn't even reading the errors anymore. I was just refreshing and hoping.


The Turning Point: My Instructor Said "Use tsup"

I'd heard of tsup before. Never used it. Didn't think I needed it.

My instructor sent over a PDF with the fix. I almost didn't read it — I was convinced the problem was something deeper, something architectural, something that required a complete rethink.

It wasn't. It was this:

"build": "prisma generate && tsup src/server.ts --format esm --platform node --target node20 --outDir api"
Enter fullscreen mode Exit fullscreen mode

That's it. That's the tweet.

tsup is a zero-config bundler built on top of esbuild. It bundles your TypeScript correctly, handles ESM output, tree-shakes, minifies — and critically, it actually works when Node tries to run the output.


What Was Actually Going Wrong

Looking back, the chain of failures made sense:

  1. tsc output preserves your import paths as-is. If you wrote import { something } from './utils' without the .js extension, the compiled output breaks in ESM mode — because Node expects explicit extensions.

  2. The ERR_MODULE_NOT_FOUND: Cannot find package 'test' error (visible in my terminal screenshot) was a ghost dependency issue — something in the build referencing a package that wasn't bundled correctly.

  3. Vercel's serverless functions need a specific entry point. My vercel.json was pointing at dist/server.js when my actual output was going to api/server.js. A typo in a JSON file cost me hours.


The Fix, All in One Place

package.json scripts:

"scripts": {
  "dev": "tsx watch src/server.ts",
  "build": "prisma generate && tsup src/server.ts --format esm --clean --minify --outDir api",
  "start": "node api/server.js"
}
Enter fullscreen mode Exit fullscreen mode

vercel.json:

{
  "version": 2,
  "builds": [
    { "src": "api/server.js", "use": "@vercel/node" }
  ],
  "routes": [
    { "src": "/(.*)", "dest": "api/server.js" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

server.ts entry point — export your app as default:

import app from './app'
export default app
Enter fullscreen mode Exit fullscreen mode

Run pnpm build locally first. Watch the /api folder appear. Check the filename. Put that exact filename in vercel.json. Then deploy.


What I Actually Learned

1. tsx is for development. It's not a build tool.
It's magical for hot-reloading TypeScript, but it's not meant to produce deployable output. Reach for tsup or esbuild when you need to ship.

2. ESM in Node.js is still a minefield.
"type": "module" in package.json changes everything. Bundlers like tsup handle the edge cases so you don't have to become an expert in Node module resolution at 1am.

3. Check your output directory before touching vercel.json.
This sounds embarrassing to type. I'm typing it anyway. The file Vercel deploys has to actually exist.

4. Your instructor's boring PDF might be the answer.
Read it first. Then go exploring. Not the other way around.


In Hindsight

Two days for a 5-line fix. Classic.

But I came out the other side actually understanding why it works now — not just copying config from Stack Overflow and hoping for the best. That's worth something.

If you're building a TypeScript backend and thinking "I'll just use tsc" — maybe reach for tsup from the start. Future-you will be grateful. Future-you has a coffee to drink instead of an error log to read.


Building MediStore as part of a full-stack development course. More posts on the way — probably also born from pain.

If this saved you a day, drop a clap. If you've been through something similar, I'd love to hear it in the comments.

Top comments (0)