NodeJS Summary

Node.js has a rich ecosystem but its own patterns that differ significantly from synchronous languages. The event loop, streams, modules, async patterns, and the npm ecosystem all have their own rules. This post covers the ones that show up repeatedly in real backend development.

TL;DR: A Node.js reference covering the event loop, async patterns, streams, modules, error handling, and ecosystem patterns.
Stack: Node.js, JavaScript
Level: Intermediate
Reading time: ~15 min

Init the project

npm init -y
npm install express typescript @types/express ts-node-dev
npx tsc --init

tsconfig.json minimal

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}

Create src/index.ts

import express, { Application, Request, Response } from 'express';

const app: Application = express();
const port: number = 3000;

app.get('/', (req: Request, res: Response) => {
  res.send('Hello, world!');
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

Add dev script to package.json

{
  "scripts": {
    "dev": "ts-node-dev src/index.ts"
  }
}
npm run dev

V1 – Routes (personRoutes.ts)

import { Router } from 'express';
import { ListPerson } from '../../application/useCases/ListPerson/ListPerson';

const personRouter = Router();
const listPersonController = new ListPerson();

personRouter.get('/list-person', async (req, res) => {
  try {
    const people = await listPersonController.handle(req.body);
    res.json(people);
  } catch (error) {
    res.status(500).send('Error listing people');
  }
});

export { personRouter };

V2 – Controllers (ListPersonController.ts)

import { Request, Response } from 'express';

export class ListPersonController {
  async handle(req: Request, res: Response): Promise {
    return res.json({ message: 'Hello, World!' });
  }
}

V3 – Use Cases (ListPerson.ts)

import { IListPersonRequest } from "./protocols/IListPersonRequest";
import { IListPersonResponse } from "./protocols/IListPersonResponse";

class ListPerson {
  async handle(inbound: IListPersonRequest): Promise> {
    const returnList: Array = [];
    returnList.push({ name: 'Allan' } as IListPersonResponse);
    returnList.push({ name: 'Maradona' } as IListPersonResponse);
    return returnList;
  }
}

V4 – Repository with TypeORM

npm install typeorm pg reflect-metadata

Create the ormconfig.json, add the TypeORM entity model, implement the concrete repository, and inject it into the use case through the controller. The pattern is: controller injects repository into use case, use case doesn’t know what database it’s talking to.

What you’ve built

A layered Node.js backend with controllers, use cases, and repository injection, covering the patterns that show up in real production code.

Next steps

  • Understand the event loop: synchronous code blocks it, so CPU-intensive operations should go to worker threads or child processes.
  • Use Promise.all() for concurrent independent async operations, and Promise.allSettled() when you need results even if some fail.
  • Never call callback(err, result) synchronously inside an async function, it creates subtle timing bugs.

Questions or feedback? Find me on LinkedIn or GitHub.

Leave a Comment