Kennisbank › Javascript › es modules
javascript starter 08 October 2026

es modules

`require()` werkt nog steeds in Node.js 22, maar de kans dat je er een nieuwe module mee schrijft wordt elk jaar kleiner.Dat is de kern van de keuze waar je

`require()` werkt nog steeds in Node.js 22, maar de kans dat je er een nieuwe module mee schrijft wordt elk jaar kleiner.

Dat is de kern van de keuze waar je voor staat. Niet of ES Modules beter zijn — dat zijn ze, in bijna elk opzicht — maar of jouw omgeving ze aankan zonder dat je de halve dag bezig bent met het omzeilen van oude afspraken.

ES Modules zijn sinds 2015 onderdeel van de JavaScript-specificatie. Ze werken in elke browser die je vandaag serieus hoeft te nemen, in Node.js sinds versie 12 met wat gedoe en sinds versie 14 zonder, en in Deno en Bun vanaf het eerste uur.

De syntaxis is klein. Je zet `export` voor iets en `import` in het bestand dat het nodig heeft.

```js export const mijnFunctie = () => { console.log("Hallo vanuit een ES Module!"); }; ```

```js import { mijnFunctie } from './mijnModule.js'; mijnFunctie(); ```

Dat is het. Geen wrapper, geen registratie, geen `module.exports =` die je telkens vergeet.

De prijs zit niet in de syntaxis. Die zit in alles eromheen.

Neem de bestandsextensie. In een browser moet je `./mijnModule.js` schrijven, met de `.js` erbij. In Node.js met `"type": "module"` in je `package.json` ook. In een bundler als Vite of Webpack mag je de extensie weglaten, en in TypeScript met de juiste instellingen ook. Dat verschil lijkt onschuldig tot je code verhuist van een bundler naar een runtime en er ineens niets meer resolvet.

Dezelfde code, andere regels. Dat is het patroon dat je bij ES Modules steeds terugziet.

Dan de default export. Die bestaat, en hij is verleidelijk.

```js export default function() { console.log("Dit is de standaard export."); } ```

```js import mijnFunctie from './mijnModule.js'; ```

Geen accolades, geen naam, gewoon importeren. Handig voor een module die één ding exporteert, zoals een klasse of een configuratieobject.

Het probleem is dat de importerende partij zelf een naam mag verzinnen. Het bestand dat `mijnFunctie` importeert kan het net zo goed `doeIets` noemen, en dan zoek je je later scheel naar waar `doeIets` vandaan komt. Bij benoemde exports staat de naam vast aan beide kanten.

In de community is dit een terugkerend dispuut. De meeste stijlgidsen die de afgelopen jaren zijn geschreven zeggen: gebruik benoemde exports, tenzij je een hele goede reden hebt. Dat is geen wet, maar het is wel het advies waar je het minst spijt van krijgt.

Wat ES Modules echt anders maakt dan CommonJS is dat de structuur statisch is. Tijdens het parsen staat vast wat er geïmporteerd en geëxporteerd wordt. Niet tijdens runtime.

Dat klinkt als een detail. Het is de reden dat tree-shaking werkt — je bundler kan zien welke exports nooit gebruikt worden en ze weggooien. Bij CommonJS met zijn dynamische `require()` is dat veel lastiger, want die aanroep kan overal zitten, in een `if`, in een lus, achter een variabele.

Dezelfde statische structuur maakt het voor je IDE mogelijk om te springen naar de definitie van een import, en om te zien wie een functie gebruikt voordat je hem hernoemt. Bij een dynamische `require()` moet je tooling gokken.

In de browser laad je een ES Module zo:

```html ```

Twee dingen gebeuren dan automatisch. De module en al zijn dependencies worden asynchroon geladen, zonder de hoofdthread te blokkeren. En de code draait in strikte modus, ook als je nergens `"use strict"` hebt geschreven.

Die strikte modus is geen luxe. Hij verbiedt een paar constructies die je anders stil laten stuklopen — een variabele gebruiken zonder hem te declareren, een property toewijzen aan een bevroren object, `with` gebruiken. Je krijgt een fout in plaats van een verrassing drie uur later.

Nu Node.js. Daar zit de echte pijn.

Node.js is gebouwd op CommonJS. Elke `.js` in een project zonder `"type": "module"` is een CommonJS-module. Wil je ES Modules, dan zet je dat in je `package.json`:

```json { "type": "module" } ```

Of je noemt je bestanden `.mjs`. Beide werken, en beide hebben gevolgen voor hoe je imports resolvet.

Het lastige is niet het inschakelen. Het lastige is dat de twee systemen niet vrij met elkaar praten.

Een ES Module die een CommonJS-module importeert: dat gaat. De `module.exports` van de CommonJS-kant komt binnen als de default export, en benoemde exports moet je via `import * as` ophalen. Dat werkt, maar het is niet altijd voorspelbaar welke namen er dan precies in dat namespace-object zitten.

De andere kant op is erger. Een CommonJS-module kan geen ES Module importeren met `require()`. Dat kan simpelweg niet, want `require()` is synchroon en ES Modules zijn dat niet per se. Je enige uitweg is dynamische `import()`, die een promise teruggeeft, of transpilatie naar CommonJS met een bundler.

Dus zodra je één ES Module in een oud project stopt, krijg je een grens. Aan de ene kant werkt alles nog, aan de andere kant moet je nadenken bij elke import die de grens oversteekt.

De meeste teams kiezen dan voor één van twee dingen: alles omzetten, of alles laten zoals het is. Half omzetten is de duurste optie.

TypeScript maakt het niet eenvoudiger, maar wel overzichtelijker. Het herkent de `import`- en `export`-syntaxis en compileert naar wat je in `tsconfig.json` hebt ingesteld.

Je kunt dus in ES Modules schrijven en naar CommonJS compileren voor een oude runtime. Of direct ES Modules uitsturen als je doelomgeving dat ondersteunt.

TypeScript exporteert niet alleen waarden. Ook types, interfaces, enums en namespaces gaan met hetzelfde sleutelwoord de deur uit.

```ts export interface Gebruiker { naam: string; leeftijd: number; }

export type Rol = 'admin' | 'gebruiker'; ```

```ts import { Gebruiker, Rol } from './types'; ```

Dat is prettig. Dezelfde syntaxis voor dingen die in de runtime bestaan en dingen die alleen tijdens het compileren tellen. Je hoeft niet twee mentale modellen naast elkaar te houden.

Eén `tsconfig.json` dekt één omgeving. Dat is een valkuil waar veel projecten in lopen.

Een applicatie met servercode, DOM-code, webworker-code en tests heeft vaak vier verschillende sets globals en soms ook vier verschillende module-instellingen. TypeScript lost dat op met project references: elk onderdeel krijgt zijn eigen `tsconfig.json`, en de root verwijst ernaar.

Voor elke configuratie bepaal je apart welk moduleformaat eruit komt. Servercode kan naar CommonJS als de runtime dat eist. Frontendcode gaat naar ES Modules, want de bundler en de browser willen niets anders.

Dat is meer configuratie dan je in eerste instantie wilt. Het alternatief is één configuratie die overal een beetje in past, en dat is precies de configuratie waar je later een middag aan kwijt bent.

Wat je tooling betreft: Vite en Webpack ondersteunen ES Modules zonder morren. Rollup is erop gebouwd. Moderne ontwikkelomgevingen zijn erop ingericht, en de hele ecosysteem-beweging van de afgelopen jaren wijst één kant op.

Dat betekent niet dat CommonJS dood is. Het draait in productie op meer plekken dan je denkt, en het zal daar nog jaren blijven draaien. Node.js ondersteunt het nog steeds volledig, en de meeste npm-packages die je vandaag installeert zijn nog CommonJS onder de motorkap.

De vraag is niet welk systeem beter is. De vraag is welke grens je wilt onderhouden.

Kies je ES Modules in een groen project, dan krijg je de statische analyse, de tree-shaking, de strikte modus en de tooling die ervoor gebouwd is. Je betaalt met extensies die je niet mag weglaten, met een `package.json` die je moet instellen, en met het gedoe zodra je een oude dependency binnenhaalt.

Blijf je bij CommonJS in een bestaand project, dan werkt alles wat er al werkt. Je betaalt met een ecosysteem dat langzaam de andere kant op beweegt, en met tooling die je steeds vaker moet overtuigen.

Er is geen versie van deze keuze die je niets kost. Er is alleen de vraag welke kosten je het beste kunt dragen — en of je team het erover eens is voordat de eerste `.mjs` in de repository belandt.

← Terug naar kennisbank
← Terug naar kennisbank