惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
Microsoft Azure Blog
Microsoft Azure Blog
月光博客
月光博客
S
Securelist
J
Java Code Geeks
Recorded Future
Recorded Future
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
M
MIT News - Artificial intelligence
S
Secure Thoughts
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Docker
Martin Fowler
Martin Fowler
The Last Watchdog
The Last Watchdog
WordPress大学
WordPress大学
The GitHub Blog
The GitHub Blog
Vercel News
Vercel News
O
OpenAI News
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
PCI Perspectives
PCI Perspectives
N
News and Events Feed by Topic
H
Heimdal Security Blog
SecWiki News
SecWiki News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 【当耐特】
T
Troy Hunt's Blog
L
LINUX DO - 最新话题
Hacker News: Ask HN
Hacker News: Ask HN
Hacker News - Newest:
Hacker News - Newest: "LLM"
N
Netflix TechBlog - Medium
A
Arctic Wolf
The Hacker News
The Hacker News
I
Intezer
S
Schneier on Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Apple Machine Learning Research
Apple Machine Learning Research
L
Lohrmann on Cybersecurity
宝玉的分享
宝玉的分享
P
Privacy & Cybersecurity Law Blog
Stack Overflow Blog
Stack Overflow Blog
T
Tor Project blog
小众软件
小众软件
Simon Willison's Weblog
Simon Willison's Weblog
The Cloudflare Blog
Jina AI
Jina AI

TerminalBytes

Reverse Engineering the iPod Classic Sanctuary: the browser-based Kindle jailbreak | TerminalBytes Steam Machine alternatives: mini PCs that game for less | TerminalBytes Oracle Cloud free tier 2026: 4 OCPU/24GB cut to 2 OCPU/12GB | TerminalBytes Run Gemma 4 on a mini PC, no GPU required My Arduino spins faster when Claude burns more tokens Kindle dashboard: 3 ways to build one in 2026 Best mini PC for local LLMs in 2026 (Strix Halo era) You Don't Need a Mac Mini to Run OpenClaw The Self-Hosting Revolution Powered by Mini PCs SoundLeaf: Beautiful iOS Client App for Audiobookshelf My iPhone 8 Refuses to Die: Now It's a Solar-Powered Vision OCR Server Homelab: Hosting multiple Game Servers on a Single Mini PC Reviving an Old Kindle Paperwhite 7th Gen The Ultimate Guide to Running a Minecraft Server on a Mini PC Best Mini PCs for Home Lab 2025: NUC vs Beelink vs ThinkCentre Setting Up a Robust PostgreSQL High-Availability Cluster on Azure How to Create Your Own Free VPN Server Using Oracle Cloud (2025 Guide) Set Up a Free Minecraft PE Server Using Oracle Cloud in 2025 Kubernetes at Home - From Docker Compose to K3s Prompt Engineering for ChatGPT and GPT-4 (Practical Guide) Create Your Own Minecraft PE Server for Free How to Use GeForce Now in India (VPN and Cloud Workarounds) Azure AD as an Identity Provider for AWS Cognito Free WordPress Blog with Custom Domain on Google Cloud sshuttle - A Free VPN Over SSH for Cheap, Private Browsing Raspberry Pi Temperature and Humidity Monitor with Grafana Raspberry Pi Setup Without a Monitor, Keyboard, or Mouse 6 Common Linux Commands for System Monitoring Basic Linux Commands for File Manipulation and Compression Basic Linux Commands For Text Manipulation Top 10 Linux Commands Of All Time
Scalable Node.js Backend Without Express, Koa, or Hapi
Hemant Kumar · 2022-04-20 · via TerminalBytes

So here’s the thing. I built a Node.js backend over a weekend without Express, Koa, Hapi, or any other web framework, and I’m still running it in production a couple of years later. The reason wasn’t ideology; it was an interview prep exercise that turned into “wait, why am I always pulling in 200 transitive dependencies for what amounts to URL routing and JSON parsing?”. The exercise stuck.

This post walks through the design. The code is on GitHub. The whole stack is Node.js + TypeScript + PostgreSQL + nginx, dockerised, and it survives 10,000 requests per minute on a t3.small without breaking a sweat.

Cover image: Node.js, TypeScript, nginx, and Docker logos for backend stack

TL;DR: skip Express, write the request-handling primitives yourself (with find-my-way for routing), put nginx in front for rate limiting and load balancing, ship it. The whole thing is ~600 lines of TypeScript.

Why no framework?

Two reasons, neither of them about hating Express specifically.

Dependency surface area. A fresh express install pulls in around 50 transitive packages. find-my-way pulls in zero. For a small service that has to live for years, smaller dependency trees mean fewer security advisories to scramble around, fewer breaking changes when someone upstream rewrites their middleware API, and fewer “why does my Docker image keep growing” investigations.

You actually understand the request lifecycle. When you write your own body parser, your own auth middleware, and your own error handler, you understand exactly when each runs and why. Two years later when something weird happens at 2am, you don’t have to grep through node_modules to figure out which framework decided your error response should be HTML.

What I’m not arguing: that you should rip Express out of an existing project. Express is fine. This is for new projects where you have the latitude to keep the stack lean.

The full stack at a glance

Internet → nginx → Node.js (find-my-way) → Sequelize → PostgreSQL

Each piece earns its place.

  • nginx: TLS termination, request rate limiting, load balancing across multiple Node.js workers if you scale horizontally. Built-in DOS protection. Free.
  • Node.js + TypeScript: the application itself. TypeScript catches half the bugs Express’s runtime would have caught with middleware.
  • find-my-way: a router. Just a router. Maps GET /api/recipes/:id to a handler. No middleware system, no body parsing, just routing.
  • Sequelize: ORM. Maps PostgreSQL rows to TypeScript classes. The one place I do let dependencies do work for me, because writing a SQL builder by hand is the line where “lean stack” becomes “I’m reinventing wheels”.
  • PostgreSQL: storage. Battle-tested, transactional, the default answer to “where should my data live”.
  • Docker + docker-compose: dev environment. One command to spin up nginx, the Node.js service, and PostgreSQL together.

The full source is on GitHub: terminalbytes/nodejs-nginx. Clone it, docker compose up, and a Swagger UI is running on localhost:8082 with a working CRUD API for managing recipes (because it had to manage something, and recipes felt low-stakes).

The minimal dependency list

Here’s the actual package.json dependencies. No React, no Express, no Webpack, no thirty middleware packages.

{
  "dotenv": "^16",
  "find-my-way": "^7",
  "jsonwebtoken": "^9",
  "module-alias": "^2",
  "pg": "^8",
  "pg-hstore": "^2",
  "pino": "^8",
  "reflect-metadata": "^0.1",
  "sequelize": "^6",
  "sequelize-typescript": "^2",
  "typescript": "^5",
  "xml-js": "^1"
}

What each one buys:

  • dotenv: load .env files into process.env.
  • find-my-way: routing. The crown jewel of the stack.
  • jsonwebtoken: sign and verify JWTs for auth.
  • module-alias: lets me write import { foo } from '@utils/foo' instead of '../../../utils/foo'.
  • pg + pg-hstore: native PostgreSQL driver and the hstore extension Sequelize uses.
  • pino: structured JSON logging. Ships straight to CloudWatch or Elasticsearch with no transformation.
  • reflect-metadata: required by sequelize-typescript’s decorators.
  • sequelize + sequelize-typescript: ORM with TypeScript decorator syntax for models.
  • typescript: the compiler.
  • xml-js: JSON↔XML conversion for content-negotiation responses.

That’s it. No body-parser, no cors, no helmet, no morgan. I wrote each of those as a thin handler on top of the raw Node.js http module.

Routing with find-my-way

find-my-way is the router that powers Fastify under the hood. It’s a radix-tree-based URL matcher with built-in path parameter support, regex constraints, and HTTP method routing. It does one thing extremely well, then gets out of the way.

import findMyWay from 'find-my-way';
import { recipesController } from './controllers/recipes';

const router = findMyWay({ ignoreTrailingSlash: true });

router.get('/api/recipes', recipesController.list);
router.get('/api/recipes/:id', recipesController.get);
router.post('/api/recipes', recipesController.create);
router.put('/api/recipes/:id', recipesController.update);
router.delete('/api/recipes/:id', recipesController.remove);

const server = http.createServer((req, res) => {
  router.lookup(req, res);
});

server.listen(process.env.PORT ?? 8084);

That’s the whole entry point, minus auth and error handling. Each controller method receives the raw IncomingMessage and ServerResponse plus an object with parsed path parameters. From there, you parse the body, call into Sequelize, and write a response.

Body parsing is twenty lines:

async function readJsonBody<T>(req: IncomingMessage): Promise<T> {
  const chunks: Buffer[] = [];
  for await (const chunk of req) {
    chunks.push(chunk as Buffer);
    // 1MB cap; reject larger payloads
    if (Buffer.byteLength(Buffer.concat(chunks)) > 1_048_576) {
      throw new Error('PayloadTooLarge');
    }
  }
  return JSON.parse(Buffer.concat(chunks).toString('utf8'));
}

That’s the kind of thing Express middleware abstracts away. Doing it by hand takes ten minutes once and then you control the size limit, the error type, and how you log payload-rejection events.

A second small handler I wrote: a content-negotiation responder that looks at the Accept header and serializes the response as JSON, XML, or plain text. That’s another 30 lines, lives next to the body parser, and means the API supports application/json, application/xml, and text/plain for any endpoint without per-route work. Most of “what Express middleware does” is this kind of thing: small, composable, easy to write yourself once you decide to.

nginx as the front door

Putting nginx in front of Node.js does three concrete things.

Rate limiting. A single bad client can DOS a Node.js service trivially because the event loop is single-threaded; one slow handler blocks every other request. nginx’s limit_req directive caps requests per IP per second before they reach Node, so the worst case is nginx returning 429s, not Node falling over.

http {
  limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

  server {
    listen 80;

    location /api/ {
      limit_req zone=api burst=20 nodelay;
      proxy_pass http://node:8084/;
      proxy_set_header X-Forwarded-For $remote_addr;
    }
  }
}

Load balancing. When you scale to two or more Node.js containers, nginx round-robins between them. No additional infrastructure, no Kubernetes service mesh. Add a second node container in docker-compose.yml, list both in the upstream block, done.

Static file serving. If you ever serve static assets, nginx can do it from disk an order of magnitude faster than Node.js piping the file through the event loop.

That’s three jobs nginx does better than Node.js, with config that fits on a screen.

Database access with Sequelize

Sequelize is the place I let a real dependency in, because writing a SQL builder is the wrong level of abstraction. With sequelize-typescript, models are TypeScript classes with decorators:

import { Table, Column, Model, DataType, PrimaryKey } from 'sequelize-typescript';

@Table({ tableName: 'recipes' })
export class Recipe extends Model {
  @PrimaryKey
  @Column(DataType.UUID)
  id!: string;

  @Column(DataType.STRING)
  name!: string;

  @Column(DataType.JSONB)
  ingredients!: string[];

  @Column(DataType.INTEGER)
  prepTimeMinutes!: number;
}

// Querying:
const recipes = await Recipe.findAll({
  where: { prepTimeMinutes: { [Op.lte]: 30 } },
  limit: 100,
});

Three lines for a paginated query against a JSONB column. Pre-sequelize-typescript me would have written 30 lines of pg.query with $1, $2, $3 placeholders, which works but rots fast as the schema evolves.

The connection pool, transactions, and migration scaffolding all come from Sequelize. The cost is a few MB of dependency weight; the payoff is that I can refactor the schema without rewriting the data layer.

Load test results on a t3.small

I ran wrk against the deployed service for 10 minutes at 10,000 requests per minute (so ~167 RPS) on a t3.small (2 vCPU, 2GB RAM) with Postgres on the same instance. Most requests fetched 100 recipes from a table of 50,000.

MetricValue
Average response time39ms
99th-percentile response time112ms
Errors0
Total bandwidth974 MB
nginx CPU4%
Node.js CPU22%
Memory (RSS, total)230 MB

That’s plenty of headroom. The bottleneck would arrive somewhere around 1,000 RPS, at which point I’d add a second Node.js container behind nginx (which is also why nginx is there in the first place).

Load testing the Node.js service at 10,000 clients per minute, 39ms average response time

CPU and memory during the test stayed well under 25% on average. The single-threaded event loop is fine when nginx is doing the gatekeeping. The dirty secret of “Node.js can’t scale” memes is that 90% of the time, the bottleneck is somewhere outside Node: the database, network egress, or the absence of any caching at the front. Solve those before you blame the runtime.

Server performance dashboard during load test showing 4% CPU and 1% memory usage

Trade-offs and when to actually use Express

I’m not religious about no-framework. There are situations where Express (or Fastify, which is closer to my stack) is clearly the right answer.

Use Express when:

  • The team has 10+ engineers and “everyone knows Express” is a productivity multiplier.
  • You depend on middleware that doesn’t exist outside the Express ecosystem (some auth providers, some observability tools).
  • The project life expectancy is two years and you don’t want to maintain custom plumbing.

Skip Express when:

  • It’s a personal project or a small team you control.
  • You want to learn what’s actually happening at the HTTP layer.
  • Build size, cold-start time, or attack surface matter (think: AWS Lambda, edge functions).
  • You enjoy writing the bones yourself and have done it before, so you know which 5% of edge cases will bite you.

This project hits all four of the second list. Yours might or might not.

The other thing I picked up running this stack: dependency-free Node.js makes upgrading runtime versions almost a non-event. Bumping from Node 18 to 20 to 22 was a single Dockerfile change and a yarn install rebuild each time. No middleware deprecations, no peer-dependency warnings, no waiting on a half-maintained type definitions package to catch up. That’s the hidden return on investment of a lean stack: the boring upgrade story is also the safe one.

If this got you thinking about lean backend stacks, my PostgreSQL cluster setup on Azure post covers the database side, the Linux text-manipulation commands post is the cheat sheet I lean on for the daily grep/awk on production logs, and the common system-monitoring commands post covers the in-shell tools you’ll want when the service starts misbehaving in production.

The full repo is at terminalbytes/nodejs-nginx if you want a working starting point. Fork it, gut what you don’t need, and build on top.

Last updated: October 2024