Why I Chose 60 Lines of JavaScript Over NestJS for a Budget Project

by Hayk GhukasyanApril 18th, 2025
Read on Terminal Reader
tldt arrow

Too Long; Didn't Read

When building an API for a tiny startup on a shoestring budget, the author compares raw Node.js, Express, and NestJS. Despite NestJS’ heavier footprint, it saves time on validation, documentation, and onboarding—ultimately winning out thanks to dev-friendly features and long-term maintainability.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Why I Chose 60 Lines of JavaScript Over NestJS for a Budget Project
Hayk Ghukasyan HackerNoon profile picture
0-item


When a friend‑of‑a‑friend calls you with the words “we have a micro‑budget but we trust you,” you either run for the hills or pour yourself another espresso and crack VS Code open. I chose the second option, which is how I ended up spending two very caffeine‑charged weekends deciding whether to build a tiny API with bare‑bones JavaScript or to reach for the shinier, batteries‑included NestJS framework.


This post is a blow‑by‑blow log of that internal knife fight. No corporate buzzwords, no AI‑crafted SEO paragraphs—just me, my keyboard, and way too many TODO comments. If you’ve ever wondered whether NestJS is overkill for something that could fit in a single file, or whether hand‑rolled JavaScript is a maintenance nightmare dressed in hipster minimalism, read on.

The Mini‑Project That Started the Drama

Picture a two‑person startup that sells hand‑painted skateboard decks. They’d need a teeny‑tiny backend that could:


  1. expose a REST API for product listings (GET /decks)
  2. accept orders (POST /orders)
  3. ping Stripe’s webhook once a week

…and run on the cheapest $6 VPS the CFO (aka the founder’s dog) could find.

Total budget for dev work: ≈ $1 000.
Deadline:two weekend sprints.
Team:me, my linters, and a fig plant that judges my tab spacing.


With numbers that tight, every hour of setup matters, so the “framework vs. no‑framework” decision suddenly felt heavy.

Route 1 — The Bare‑Bones JavaScript Way

Spinning up a Node server by hand is the programming equivalent of camping with just a knife and a roll of duct tape: uncomfortable, but extremely satisfying when it works.


// server.js — 59 lines, all mine
import http from 'node:http';
import { parse } from 'node:url';
import { readFileSync } from 'node:fs';

const PORT = process.env.PORT ?? 3000;
const DB = JSON.parse(readFileSync('./db.json', 'utf8'));

const routes = {
  '/decks': (req, res) => {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(DB.decks));
  },
  '/orders': async (req, res) => {
    if (req.method !== 'POST') return send404(res);
    const body = await buffer(req);
    const order = JSON.parse(body);
    // TODO: validation, payment, kittens, etc.
    DB.orders.push(order);
    res.writeHead(201);
    res.end('cool');
  }
};

http.createServer((req, res) => {
  const path = parse(req.url).pathname;
  (routes[path] ?? send404)(req, res);
}).listen(PORT, () => console.log('listening', PORT));

function send404(res) { res.writeHead(404); res.end('nope'); }
function buffer(req) {
  return new Promise(res => {
    let d = '';
    req.on('data', c => (d += c)).on('end', () => res(d));
  });
}


Sixty lines, zero dependencies, and it runs on Node v18 that ships with Ubuntu 22.04. I could almost hear my VPS cheering.

Why I love this approach

  • Instant bootstrap. node server.js and we’re live.
  • Zero cognitive overhead. The entire app fits on one screen.
  • Disk‑space friendly. The node_modules folder is literally non‑existent.
  • Old‑school debugging. Add a console.log, refresh, done.

Why it keeps me up at night

  • Manual everything. CORS, validation, DI—if I want it, I write it.
  • Testing pain. Jest + supertest works, but all the wiring is on me.
  • Callback relapse. One async slip and we’re in callback‑hell nostalgia.
  • Team onboarding. Future devs might cry or quit (or both).


Here’s what a “quick” validation patch looked like by Sunday midnight:


if (!order.email || !/^[\w+.]+@\w+\.\w+$/.test(order.email)) {
  res.writeHead(422);
  return res.end('invalid email bro');
}


Multiply that by every field in the payload and you start missing those NestJS DTOs.

Route 2 — The NestJS Rollercoaster

According to the NestJS 11 documentation released last month, the platform delivers “structure without the pain” for each version since version 6. The framework adopts a stringent structure by default while utilizing TypeScript by default while demanding SOLID pattern adoption despite your real feelings of pattern neutrality. The catch? Node v20 has become the required minimum version according to their documentation, so I had to replace my comfortable Debian 10 droplet.


The famous five‑minute start looked like this:

npm i -g @nestjs/cli
nest new skatedecks-api
cd skatedecks-api
npm run start:dev


Then I scaffolded the decks module:

nest g resource decks


Boom—controller, service, DTOs, tests, e2e spec, an OpenAPI stub, and a partridge in a pear tree.

A slice of the generated controller

// decks.controller.ts
@Controller('decks')
export class DecksController {
  constructor(private readonly svc: DecksService) {}

  @Get()
  findAll() { return this.svc.all(); }

  @Post()
  create(@Body() dto: CreateDeckDto) {
    return this.svc.add(dto);
  }
}

The good stuff

  • Type safety. Muscle‑memory order. autocomplete instead of guessing keys.
  • Dependency injection. Swap DecksService for a mock in one line—tests love you.
  • Batteries included. Swagger doc, class‑validator, caching, WebSockets—all one import.
  • Ecosystem. I found a Stripe module in 13 seconds.

The wallet ache

  • Cold start. npm install pulled 164 MB of packages—not great for the $6 VPS.
  • Learning curve. My junior dev would need a full day to grok modules vs. providers.
  • Boilerplate. Every file opens with four decorators and a dozen imports.
  • Performance tax. In Hello‑World benchmarks, NestJS adds ~10 ms to P99. On big iron you don’t care; on a t2.micro you do.

Detour — The Case of “Just Use Express”

On Day 1 a buddy dropped into Slack and went, “Dude, why are you tormenting yourself with raw Node? Just npm i express, done.” Express sits between vanilla JS and full‑blown frameworks, so I gave it a fair trial. Spoiler: it solved 20 % of my gripes but introduced others.

import express from 'express';
const app = express().use(express.json());

app.get('/decks', (req, res) => res.json(DB.decks));
app.post('/orders', (req, res) => {
  const order = req.body;
  // still writing manual validation...
});

app.listen(3000);


Express saved me maybe 30 lines, but I was still stitching middlewares, hand‑rolling Swagger, and dreaming about decorators. It felt like buying furniture from IKEA—cheaper than custom carpentry, but you’re still the one with an Allen key at 11 PM.

Benchmark Rabbit Hole

Because I’m a numbers nerd, I spun up three DigitalOcean droplets (each 1 vCPU, 1 GB RAM) and hammered them with wrk:

wrk -t4 -c200 -d30s http://myip:3000/decks

Stack

Req/sec

P99 Latency

Vanilla HTTP

12 560

14 ms

Express 5 beta

11 830

16 ms

NestJS 11 (Fastify)

11 310

25 ms

TL;DR: the difference is peanuts compared to Stripe’s 300 ms average. If performance is your only yardstick, choose whichever stack lets you ship faster.

“Can’t I Just Add TypeScript to Vanilla?”

Absolutely—and I tried. But the moment you add ts-node, tsconfig.json, a build script, and types for Node, you’re already halfway up the framework mountain. NestJS just finishes the climb and throws in rope and a granola bar.

Hidden Costs I Almost Missed

  1. DevOps friction. My hand‑rolled server needed a dozen custom health checks for Kubernetes. NestJS offers a ready‑made health module.
  2. Monitoring. NestJS + @nestjs/terminus + Prometheus exporter took 10 min. Rolling my own metrics exporter would be “fun” but billable.
  3. Security headers. One line with helmet() vs. a night with OWASP docs.
  4. Community answers. StackOverflow has 53 k questions tagged NestJS. Hand‑rolled patterns usually = “you’re on your own, buddy.”

These soft costs rarely show up in spreadsheets, but they bite you at 2 AM.

Costing the Two Paths (Round 1)

I grabbed a napkin, a pen, and my highly scientific “developer‑hour × coffee” formula.

Item

Vanilla JS

NestJS

Initial setup

2 h

5 h

Basic CRUD & validation

6 h

4 h

Tests & docs

5 h

3 h

Containerization

1 h

1 h

Total

14 h

13 h

Surprise: NestJS actually wins by an hour once you factor validation and docs. The time I saved not hand‑rolling OpenAPI wiped out the heavier bootstrap.

Hourly rate being $60 (hey, it’s a friend project), that’s $840 vs. $780. Not a deal‑breaker, but when the budget cap is $1 000, fifty bucks buys better coffee beans.

What If the Budget Was $10 000?

Interestingly, the decision matrix flips. With more money I’d consider going even simpler (e.g., Cloudflare Workers or Firebase) to avoid running servers at all. The real enemy isn’t frameworks—it’s YAGNI: building things you’ll never need.

But in a $1 000 universe, self‑hosted VPS plus a sensible framework hits the sweet spot between cost and control.

A Quick Checklist for Your Own Decision

  • < 50 LOC and never grows? ➜ Raw Node.
  • Needs Swagger, validation, & a team? ➜ NestJS.
  • Edge function serving static JSON? ➜ Skip Node, use serverless.
  • You hate decorators with a fiery passion? ➜ Express + Zod.
  • Management wants “enterprisey” buzzwords? ➜ NestJS wins PR points.

Stick this list on your monitor; thank me later.

The Hybrid Compromise Nobody Talks About

Here’s the secret third option: start in NestJS but treat it like a micro‑kernel.

  • Use only the core: controllers, services, pipes.

  • Skip fancy modules until needed.

  • Replace Fastify adapter if performance hurts later.

  • Keep feature flags to eject back to vanilla JS for specific hot paths.


I actually wrote one tiny performance‑critical route in raw Node and mounted it under /legacy/*:

import { createServer } from 'node:http';

// legacy.ts
export function mount(app: INestApplication) {
  const server = createServer((req, res) => {
    // crazy optimized CSV streaming
  });
  app.use('/legacy', (req, res) => server.emit('request', req, res));
}


It felt dirty—but it worked, and nobody on Reddit yelled at me.

A Little Easter Egg

Because Hackernoon loves code 😀, here’s the cheapest possible NestJS guard I wrote to rate‑limit the orders endpoint without introducing Redis:

@Injectable()
export class SimpleThrottleGuard implements CanActivate {
  private readonly hits = new Map<string, number>();

  canActivate(ctx: ExecutionContext): boolean {
    const ip = ctx.switchToHttp().getRequest().ip;
    const now = Date.now();
    const count = (this.hits.get(ip) ?? 0) + 1;

    this.hits.set(ip, count);
    setTimeout(() => this.hits.set(ip, count - 1), 30_000); // decay in 30 s
    return count < 10; // 10 hits per 30 s per IP
  }
}


Yes, it’s in‑memory and will reset on pod restart, but for tiny budgets elegance often loses to pragmatism.

Spoiler: What I Finally Picked

After two weekends, four espressos, and a delicate negotiation with the VPS gods, I went with NestJS. The deciding factors were:

  • Free Swagger docs impressed the client without extra code.

  • class‑validator saved ~300 lines of manual checks.

  • Onboarding the cousin dev is easier with consistent patterns.

  • Docker image size barely changed after multi‑stage build tricks.


Here’s the final Dockerfile slice that kept the image at 140 MB:

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist

CMD ["node", "dist/main.js"]


The server now idles at 29 MB RAM, handles ~400 req/s on wrk, and logs pretty JSON. Most importantly, I sleep better knowing future‑me won’t invent a routing convention at 2 AM because “why not.”

Tiny Lessons You Can Steal

  • Measure first. Benchmarks killed my fear of NestJS latency.
  • Hide boilerplate. Generate templates once; point juniors to src/examples.
  • Don’t marry a framework. It’s OK to break out raw Node for edge cases.
  • Budget ≠ tech stack. The cheapest code is the code you only write once.

Outro — Ship First, Refactor Less

Each stack that leads genuine users to engage physically with actual skateboards represents the flawless stack choice. The selection of my next side-project can take a different direction, which is perfectly acceptable. Digital stacks function like tools that do not become permanent tattoos.


Purchasing me a coffee in Yerevan will show appreciation for my decision paralysis guidance. You will spot me in the corner zone working on pipe transformation from the guard structure as the fig plant agrees with silent gestures.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks