Modules en project structuur in Node.js uitgelegd

Leer hoe Node.js modules werken, wat het verschil is tussen CommonJS en ES modules, en hoe je een schaalbare projectstructuur opzet.

3 juli 20267 min leestijdDoor We Develop Communication

Zodra je eerste Node.js script meer dan een paar regels code bevat, loop je tegen de vraag aan: hoe splits ik dit op in logische stukken? Het antwoord ligt in het slim gebruiken van modules en project structuur in Node.js. In dit artikel leer je hoe het modulesysteem werkt, wat de verschillen zijn tussen CommonJS en ES modules, en hoe je een projectstructuur opzet die meegroeit met je applicatie.

Heb je de basis nog niet onder de knie? Lees dan eerst Wat is Node.js en hoe werkt de event loop? en bekijk onze handleiding Eerste Node.js server bouwen: stap voor stap.

Wat is een module in Node.js?

Een module is een herbruikbaar blok code dat je in een apart bestand zet en vervolgens importeert waar je het nodig hebt. In Node.js is elk bestand automatisch een module. Dat betekent dat variabelen, functies en klassen die je in een bestand definieert, niet zomaar beschikbaar zijn in andere bestanden.

Dit principe heet module scope en is een van de belangrijkste redenen waarom Node.js-applicaties schaalbaar blijven, ook als ze duizenden bestanden tellen. Zonder deze scheiding zou elke globale variabele conflicten kunnen veroorzaken.

Er zijn drie soorten modules die je tegenkomt:

  • Core modules, ingebouwd in Node.js zelf, zoals fs, http, path en crypto.
  • Lokale modules, bestanden die jijzelf schrijft binnen je project.
  • Third-party modules, pakketten die je installeert via npm, zoals express of axios.

CommonJS versus ES modules

Node.js ondersteunt twee modulesystemen. Het is belangrijk dat je het verschil kent, want je komt ze allebei tegen.

CommonJS (CJS)

CommonJS is het oorspronkelijke systeem van Node.js. Je gebruikt require() om een module te importeren en module.exports om iets beschikbaar te maken.

// math.js
function optellen(a, b) {
  return a + b;
}
module.exports = { optellen };

// app.js
const { optellen } = require('./math');
console.log(optellen(2, 3));

CommonJS laadt modules synchroon en wordt pas op runtime geanalyseerd.

ES Modules (ESM)

ES modules zijn de officiële standaard van moderne JavaScript. Je gebruikt import en export, dezelfde syntax die je in de browser en in frameworks zoals React en Vue tegenkomt.

// math.js
export function optellen(a, b) {
  return a + b;
}

// app.js
import { optellen } from './math.js';
console.log(optellen(2, 3));

Om ES modules te activeren in Node.js zet je "type": "module" in je package.json, of gebruik je de .mjs bestandsextensie.

Welke kies je?

Voor nieuwe projecten adviseren we ES modules. Het is de toekomst van JavaScript, werkt consistent met frontend-code en biedt voordelen zoals tree shaking. Houd er wel rekening mee dat sommige oudere npm-pakketten alleen CommonJS aanbieden. Meer achtergrond vind je in de officiële Node.js documentatie over modules.

package.json: het hart van je project

Elke Node.js applicatie begint met een package.json. Dit bestand is het manifest van je project en bevat alles wat Node en npm moeten weten.

Maak er een aan met:

npm init -y

Een typisch package.json bevat:

{
  "name": "mijn-app",
  "version": "1.0.0",
  "type": "module",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js",
    "dev": "node --watch src/server.js",
    "test": "node --test"
  },
  "dependencies": {
    "express": "^4.19.0"
  },
  "devDependencies": {
    "eslint": "^9.0.0"
  }
}

Dependencies versus devDependencies

  • dependencies: pakketten die je applicatie nodig heeft om te draaien in productie.
  • devDependencies: tools die alleen tijdens ontwikkeling nodig zijn, zoals linters, formatters en testframeworks.

Installeer je een pakket met npm install express, dan komt het automatisch in dependencies. Met npm install --save-dev eslint belandt het in devDependencies.

package-lock.json

Dit bestand legt exact vast welke versies van alle (sub)dependencies zijn geïnstalleerd. Commit dit bestand altijd naar Git, het zorgt ervoor dat iedereen in je team en je productiemachine dezelfde versies gebruikt.

Een schaalbare projectstructuur

Er is geen enkele "juiste" manier om een Node.js project te organiseren, maar er zijn wel patronen die zich bewezen hebben. Een goede vuistregel: scheid verantwoordelijkheden duidelijk.

Hier is een structuur die werkt voor de meeste web-APIs en serverapplicaties:

mijn-app/
├── src/
│   ├── config/          # Configuratie en environment variables
│   ├── routes/          # Route-definities
│   ├── controllers/     # HTTP-handlers
│   ├── services/        # Businesslogica
│   ├── models/          # Datamodellen en database-toegang
│   ├── middleware/      # Express middleware
│   ├── utils/           # Generieke helpers
│   └── server.js        # Entry point
├── tests/               # Unit- en integratietests
├── public/              # Statische bestanden
├── .env                 # Environment variables (niet in Git!)
├── .gitignore
├── package.json
└── package-lock.json

De rol van elke map

  • config/, laadt environment variables en bundelt ze in één object dat je overal importeert.
  • routes/, definieert welke URL's naar welke controllers gaan.
  • controllers/, ontvangt requests, valideert input en stuurt antwoorden terug. Bevat geen businesslogica.
  • services/, hier leeft de échte logica: berekeningen, workflows, regels.
  • models/, praat met je database of externe API's.
  • middleware/, herbruikbare functies voor authenticatie, logging, errorhandling.
  • utils/, pure helperfuncties zonder side effects.

Waarom deze scheiding werkt

Door businesslogica in services/ te isoleren van HTTP-details in controllers/, maak je je code veel makkelijker testbaar. Je kunt services testen zonder een echte HTTP-server op te starten. Bovendien kun je dezelfde service vanuit een REST API, een queue-worker én een CLI gebruiken.

Modules importeren en exporteren: praktische tips

Named exports versus default export

Gebruik bij voorkeur named exports. Ze zijn expliciet, maken refactoring makkelijker en geven betere autocomplete in je editor.

// services/gebruikerService.js
export function haalGebruikerOp(id) { /* ... */ }
export function maakGebruikerAan(data) { /* ... */ }

// controllers/gebruikerController.js
import { haalGebruikerOp, maakGebruikerAan } from '../services/gebruikerService.js';

Gebruik een index-bestand voor barrel exports

Als een map veel gerelateerde modules bevat, kun je een index.js maken die alles bundelt:

// services/index.js
export * from './gebruikerService.js';
export * from './bestellingService.js';

Let op: gebruik dit met mate. Bij grote projecten kunnen barrel files de opstarttijd en tree shaking negatief beïnvloeden.

Vermijd circulaire imports

Wanneer module A module B importeert, en B importeert weer A, krijg je vreemde bugs zoals undefined exports. Los dit op door gedeelde code naar een derde module te verplaatsen.

Environment variables en configuratie

Gevoelige waarden zoals database-wachtwoorden of API-keys horen niet in je code. Zet ze in een .env bestand en laad ze met de ingebouwde --env-file flag (vanaf Node.js 20):

node --env-file=.env src/server.js

Of gebruik het veelgebruikte pakket dotenv voor oudere versies. Voeg .env altijd toe aan je .gitignore.

Veelgemaakte fouten

  • Alles in één bestand proppen. Splits zodra een bestand meer dan ~200 regels telt of meerdere verantwoordelijkheden heeft.
  • Paden hardcoden. Gebruik path.join() en import.meta.url om platformonafhankelijke paden te bouwen.
  • Businesslogica in routes. Je routes moeten dun zijn, ze delegeren naar controllers en services.
  • node_modules committen. Voeg het toe aan .gitignore. De map kan honderden megabytes groot worden.
  • Globale state gebruiken. Exporteer functies en objecten via modules in plaats van globals te gebruiken.

Van theorie naar praktijk

Een goede projectstructuur hoeft niet vanaf dag één perfect te zijn. Begin klein, refactor naarmate je project groeit, en wees consistent in je conventies. Als je vastzit, kijk dan naar hoe populaire open source projecten (zoals Express zelf) hun code organiseren, dat is vaak een goede bron van inspiratie.

In een volgend artikel in deze serie duiken we dieper in Eerste Node.js server bouwen: stap voor stap en koppelen we deze projectstructuur aan een echte webserver met routes, middleware en controllers.

Veelgestelde vragen

Wat is een module in Node.js?

Een module is een herbruikbaar stukje code dat je exporteert vanuit het ene bestand en importeert in een ander bestand. In Node.js is elk bestand automatisch een module met een eigen scope, waardoor variabelen niet per ongeluk botsen met andere delen van je applicatie.

Wat is het verschil tussen CommonJS en ES modules?

CommonJS gebruikt require() en module.exports en is de klassieke standaard in Node.js. ES modules gebruiken import en export en zijn de moderne JavaScript-standaard. ES modules zijn statisch analyseerbaar en werken zowel in de browser als in Node.js.

Hoe structureer ik een Node.js project het beste?

Gebruik een mappenstructuur die functionaliteit scheidt: routes, controllers, services, models en middleware. Houd je entry point (bijvoorbeeld server.js) dun en zet configuratie, businesslogica en data-toegang in aparte modules. Dit maakt je code testbaar en onderhoudbaar.

Wat is het verschil tussen dependencies en devDependencies?

Dependencies zijn pakketten die je applicatie nodig heeft om te draaien in productie, zoals Express of een database-driver. DevDependencies zijn alleen nodig tijdens ontwikkeling, denk aan test-frameworks, linters of buildtools. Ze worden apart opgeslagen in package.json.

Moet ik node_modules in Git opnemen?

Nee. Voeg node_modules toe aan je .gitignore. De map kan enorm groot worden en wordt opnieuw aangemaakt met npm install op basis van je package.json en package-lock.json.

Veelgestelde vragen

Klaar om digitaal te groeien?

Wij helpen Nederlandse bedrijven met webtechnologie en SEO-strategieën die écht werken. Neem vrijblijvend contact op.