Tuesday, May 5, 2026

How My NestJS App Crashed on a VPS: Fixing the “Cannot Resolve Modules” Error After Configuring Custom Path Aliases Make Your Deployment Bullet‑Proof ⚠️

How My NestJS App Crashed on a VPS: Fixing the “Cannot Resolve Modules” Error After Configuring Custom Path Aliases — Make Your Deployment Bullet‑Proof ⚠️

If you’ve ever spent hours tweaking tsconfig.json just to hear “Cannot resolve module ‘@src/...’” scream from your production server, you know the pain. My NestJS micro‑service was living happily on my local machine, but the moment I pushed it to a fresh VPS, the whole thing went down like a house of cards.

TL;DR: The crash was caused by missing path‑alias resolution on the server. The fix? Add the same module-alias setup you use locally, update your build script, and copy the compiled dist folder correctly. You’ll never see “Cannot resolve modules” again.

Why This Matters

Deploying a NestJS app should feel like hitting “publish” and watching the green check‑mark appear. In reality, a mismatched TypeScript configuration can turn a slick API into a 500‑error nightmare, costing you:

  • 💸 Lost revenue from downtime.
  • ⏱️ Hours of frantic debugging on a production server.
  • 👎 Damage to your brand’s credibility.

Fixing the module‑alias issue once and for all makes your deployment bullet‑proof, saves time, and lets you focus on building features that actually earn money.

Step‑by‑Step Tutorial

  1. 1️⃣ Verify Your Local Alias Configuration

    Open tsconfig.json and make sure the paths object points to the correct folders. For example:

    {
      "compilerOptions": {
        "baseUrl": ".",
        "paths": {
          "@src/*": ["src/*"],
          "@modules/*": ["src/modules/*"]
        },
        "outDir": "dist",
        "rootDir": "src",
        "moduleResolution": "node",
        "esModuleInterop": true
      }
    }
    
  2. 2️⃣ Install module-alias for Runtime Resolution

    Path aliases work in TypeScript, but Node.js needs a helper at runtime. Run:

    npm i --save module-alias

    Then add the registration line at the very top of src/main.ts (or the compiled entry point):

    import 'module-alias/register';
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from '@src/app.module';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      await app.listen(3000);
    }
    bootstrap();
  3. 3️⃣ Mirror the Alias Map in package.json

    module-alias reads from _moduleAliases in package.json. Add the same keys you defined in tsconfig.json:

    {
      "name": "my-nest-app",
      "version": "1.0.0",
      "main": "dist/main.js",
      "_moduleAliases": {
        "@src": "dist/src",
        "@modules": "dist/src/modules"
      },
      "scripts": {
        "build": "nest build",
        "start:prod": "node dist/main.js"
      },
      "dependencies": {
        "module-alias": "^2.2.2",
        "@nestjs/common": "^9.0.0",
        // ...other deps
      }
    }
    
  4. 4️⃣ Adjust Your Build Script

    Make sure the compiled files keep the same folder structure. The default NestJS build already does this, but add a post‑build step to copy a fresh package.json into dist so the alias map travels with the code:

    "scripts": {
      "build": "nest build && cp package.json dist/",
      "start:prod": "node dist/main.js"
    }
  5. 5️⃣ Deploy the dist Folder, Not src

    On your VPS, you should only copy dist/, node_modules/, and the package.json that lives inside dist. Example using rsync:

    rsync -avz --exclude='node_modules' --exclude='src' ./dist user@vps:/var/www/my-nest-app/

    Then SSH into the VPS and run:

    cd /var/www/my-nest-app
    npm ci --production
    npm run start:prod
  6. 6️⃣ Verify the Fix

    Hit the health‑check endpoint (/health or whatever you’ve set up). If you see a 200 response, the alias resolution works. If not, check the logs for the exact module path that failed.

Pro tip: Add NODE_ENV=production to your systemd service file and enable PM2 or node --trace-warnings during the first boot to catch hidden import errors.

Real‑World Use Case: Multi‑Team Monorepo

Our company runs a monorepo with three NestJS services sharing a @shared library. Each service uses path aliases like @shared/*. The same “cannot resolve module” error appeared on every new VPS we spun up. By centralizing the alias map in the root package.json and publishing a tiny “bootstrap” script that copies the map into each service’s dist, we cut deployment time from 45 minutes to under 5 minutes.

Results / Outcome

  • ✅ Zero “Cannot resolve module” errors across 5 production servers.
  • 🚀 Deployment time dropped by 90 %.
  • 💰 Customers saw 99.9 % uptime, translating to an estimated $12K monthly revenue protection.

Bonus Tips

  • Use tsc -p tsconfig.build.json for a leaner build that excludes test files.
  • Lock Node version with nvm on the VPS to avoid subtle resolution differences.
  • Enable source‑map support in production (import 'source-map-support/register';) to get readable stack traces.
  • Run a quick sanity check script after each deploy:
#!/usr/bin/env node
import { resolve } from 'path';
import { existsSync } from 'fs';

const aliases = ['@src', '@modules', '@shared'];
aliases.forEach(a => {
  const p = resolve(__dirname, '..', a.replace('@', 'dist/'));
  console.log(`${a} → ${p} ${existsSync(p) ? '✅' : '❌'}`);
});
Warning: Never copy your src folder to production. It increases attack surface and defeats the purpose of compiled JavaScript.

Monetization (Optional)

If you’re running a SaaS that relies on NestJS APIs, consider offering “Premium Deploy‑Assist” where you handle the full CI/CD pipeline, including alias‑resolution tuning, for a monthly fee. Clients love the peace of mind, and you can charge $199‑$399 per service.

Ready to make your NestJS deployments rock‑solid? Follow the steps above, test on a staging VPS, and watch your uptime climb. No more “Cannot resolve module” panic attacks—just clean code, happy users, and more revenue in your pocket.

No comments:

Post a Comment