Werken met databases in Node.js: praktische gids

Leer hoe je werkt met databases in Node.js. Van MySQL en PostgreSQL tot MongoDB, met praktische voorbeelden, connection pooling en best practices.

6 juli 20267 min leestijdDoor We Develop Communication

Een moderne webapplicatie zonder database is als een boek zonder pagina's: er is simpelweg geen plek om je data te bewaren. Werken met databases in Node.js is dan ook een essentiële vaardigheid voor elke backend-developer. Gelukkig biedt het Node.js-ecosysteem fantastische tools om met vrijwel elke database te communiceren, of het nu gaat om klassieke relationele databases of moderne NoSQL-oplossingen.

In deze gids duiken we in de praktijk van database-integratie. Je leert welke databases goed passen bij Node.js, hoe je verbindingen opzet, queries uitvoert en hoe je dit op een veilige en schaalbare manier doet.

Welke database kies je voor je Node.js project?

De eerste vraag die je jezelf moet stellen is: wat voor data ga ik opslaan? De structuur en het gebruik van je data bepalen grotendeels welke database het beste past.

Relationele databases zoals PostgreSQL en MySQL zijn ideaal als je data duidelijke relaties heeft. Denk aan een webshop met klanten, bestellingen en producten die allemaal aan elkaar gekoppeld zijn. Ze bieden sterke consistentie en krachtige query-mogelijkheden met SQL.

Document databases zoals MongoDB werken met JSON-achtige documenten. Ze zijn flexibel, schalen goed horizontaal en voelen natuurlijk aan voor JavaScript-developers omdat je werkt met objecten in plaats van tabellen.

Key-value stores zoals Redis zijn razendsnel en perfect voor caching, sessies of real-time functionaliteit. Vaak gebruik je deze naast je hoofddatabase, niet als vervanging.

Voor de meeste MKB-projecten is PostgreSQL een solide standaardkeuze. Het is gratis, uitstekend gedocumenteerd en kan zowel relationele als JSON-data prima aan.

Verbinden met een SQL-database

Laten we beginnen met een concreet voorbeeld: verbinden met PostgreSQL. De populairste library hiervoor is pg, ook wel bekend als node-postgres.

Installeer het pakket eerst:

npm install pg

Vervolgens zet je een basisverbinding op:

const { Pool } = require('pg');

const pool = new Pool({
  host: 'localhost',
  port: 5432,
  database: 'mijn_app',
  user: 'postgres',
  password: process.env.DB_PASSWORD,
});

async function getUsers() {
  const result = await pool.query('SELECT * FROM users');
  return result.rows;
}

Merk op dat we direct een Pool gebruiken in plaats van een enkele Client. Dit brengt ons bij een van de belangrijkste concepten bij databasewerk: connection pooling.

Connection pooling uitgelegd

Elke keer dat je een nieuwe verbinding met je database opent, kost dat tijd en resources. Als je voor elk HTTP-verzoek een nieuwe connectie opent, loop je snel tegen performance-problemen aan. Zeker wanneer je begrijpt hoe de event loop in Node.js werkt, wil je blocking I/O tot een minimum beperken.

Connection pooling lost dit op door een pool van verbindingen in stand te houden. Queries lenen een verbinding, gebruiken hem en geven hem terug. Dit is drastisch efficiënter en voorkomt dat je database bezwijkt onder duizenden gelijktijdige verbindingen.

Een typische poolconfiguratie ziet er zo uit:

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

Met max: 20 zeg je dat de pool maximaal 20 gelijktijdige verbindingen mag openen. Dit is een balans: te laag en queries moeten wachten, te hoog en je database raakt overbelast.

Queries veilig uitvoeren

Een van de grootste beveiligingsrisico's bij database-integratie is SQL injection. Nooit, maar dan ook nooit, mag je gebruikersinvoer direct samenvoegen met een SQL-string:

// FOUT - kwetsbaar voor SQL injection
const result = await pool.query(
  `SELECT * FROM users WHERE email = '${email}'`
);

Een aanvaller kan hier simpel code injecteren die je hele database leegtrekt of wist. Gebruik altijd parameterized queries:

// GOED - veilig met parameters
const result = await pool.query(
  'SELECT * FROM users WHERE email = $1',
  [email]
);

De $1 is een placeholder. De driver zorgt ervoor dat de waarde veilig wordt ingevoegd, zonder dat deze als SQL-code wordt geïnterpreteerd. Dit is niet alleen veiliger, het is vaak ook sneller door query caching op databaseniveau.

Werken met een ORM: Prisma als voorbeeld

Ruwe SQL schrijven is krachtig, maar voor veel projecten is een ORM (Object-Relational Mapper) productiever. Een ORM mapt je databasetabellen naar JavaScript-objecten en genereert queries voor je.

Prisma is momenteel een van de populairste ORMs in het Node.js-ecosysteem. Het biedt type-safety, automatische migraties en een prettige query-API.

Een eenvoudig Prisma-schema ziet er zo uit:

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  content  String
  author   User   @relation(fields: [authorId], references: [id])
  authorId Int
}

In je code gebruik je dit vervolgens zo:

const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

async function createUserWithPost() {
  const user = await prisma.user.create({
    data: {
      email: '[email protected]',
      name: 'Sander',
      posts: {
        create: {
          title: 'Mijn eerste post',
          content: 'Hallo wereld!',
        },
      },
    },
    include: { posts: true },
  });
  return user;
}

Je ziet meteen hoe natuurlijk dit voelt voor een JavaScript-developer. Prisma zorgt op de achtergrond voor de juiste SQL, inclusief de transactie die beide inserts atomair uitvoert.

Werken met MongoDB

Voor document-georiënteerde data is MongoDB een veelgekozen optie. De officiële driver is mongodb, maar veel developers gebruiken Mongoose voor schema-validatie en gemak.

Een basis-setup met Mongoose:

const mongoose = require('mongoose');

await mongoose.connect(process.env.MONGO_URL);

const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  name: String,
  createdAt: { type: Date, default: Date.now },
});

const User = mongoose.model('User', userSchema);

const newUser = await User.create({
  email: '[email protected]',
  name: 'Test User',
});

MongoDB schaalt uitstekend horizontaal en is flexibel wanneer je datastructuur nog evolueert. Het nadeel is dat je minder strikte consistentiegaranties hebt dan bij een klassieke SQL-database.

Async patterns en error handling

Databaseoperaties zijn vrijwel altijd asynchroon. Hoe je hiermee omgaat is cruciaal voor de stabiliteit van je applicatie. Als je nog niet helemaal thuis bent in async programming met promises en async/await, raad ik aan die basis eerst te leggen.

Een robuust patroon voor databasequeries in een HTTP server met routing ziet er zo uit:

app.get('/users/:id', async (req, res) => {
  try {
    const result = await pool.query(
      'SELECT * FROM users WHERE id = $1',
      [req.params.id]
    );
    
    if (result.rows.length === 0) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    res.json(result.rows[0]);
  } catch (error) {
    console.error('Database error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

Let op dat je specifieke database-errors niet direct doorstuurt naar de client. Die kunnen gevoelige informatie over je databasestructuur prijsgeven.

Transacties: alles of niets

Soms moet je meerdere queries atomair uitvoeren. Denk aan het overboeken van geld: je wilt niet dat het ene account wordt gedebiteerd zonder dat het andere wordt gecrediteerd. Hiervoor gebruik je transacties.

async function transferMoney(fromId, toId, amount) {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query(
      'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
      [amount, fromId]
    );
    await client.query(
      'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
      [amount, toId]
    );
    await client.query('COMMIT');
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}

Het try/catch/finally-patroon zorgt ervoor dat je altijd de verbinding teruggeeft aan de pool, zelfs bij een fout.

Organiseer je database-code slim

Strooi geen database-queries door je hele codebase. Bouw een dedicated data-access layer zoals je zou doen in een goede Node.js projectstructuur. Een simpele aanpak:

src/
  db/
    index.js          # Pool setup
    users.js          # User queries
    posts.js          # Post queries
  routes/
  services/

Dit maakt je code testbaar, herbruikbaar en makkelijker te onderhouden. Als je later migreert naar een andere database, hoef je alleen de db/-laag aan te passen.

Performance tips voor productie

Een paar gouden regels die je applicatie soepel laten draaien onder belasting:

  1. Indexeer je kolommen waar je vaak op filtert of joint. Zonder indexen worden queries bij grote tabellen extreem traag.
  2. Gebruik EXPLAIN om te zien hoe je database een query uitvoert en waar de bottleneck zit.
  3. Cache veelgebruikte queries met Redis of in-memory caching, zeker voor data die niet vaak verandert.
  4. Paginateer grote resultsets in plaats van alles in één keer op te halen. Een query die 100.000 rijen teruggeeft is vrijwel nooit wat je wilt.
  5. Monitor je queries in productie. Tools zoals pg_stat_statements voor PostgreSQL geven inzicht in welke queries traag zijn.

Veelgestelde vragen

Welke database is het beste voor Node.js?

Dat hangt af van je use case. PostgreSQL en MySQL zijn uitstekend voor gestructureerde data met relaties. MongoDB past beter bij flexibele, document-georiënteerde data. Voor caching en real-time toepassingen is Redis een slimme keuze.

Wat is connection pooling en waarom is het belangrijk?

Connection pooling hergebruikt databaseverbindingen in plaats van voor elke query een nieuwe te openen. Dit bespaart resources, verbetert de performance aanzienlijk en voorkomt dat je database overbelast raakt door te veel gelijktijdige verbindingen.

Moet ik een ORM gebruiken of zelf SQL schrijven?

Een ORM zoals Prisma of Sequelize versnelt ontwikkeling en beschermt tegen SQL injection. Zelf SQL schrijven geeft meer controle over complexe queries en performance. In de praktijk combineer je vaak beide, afhankelijk van de situatie.

Hoe voorkom ik SQL injection in Node.js?

Gebruik altijd parameterized queries of prepared statements. Voeg gebruikersinvoer nooit direct samen met SQL-strings. Moderne database libraries en ORMs doen dit standaard goed, mits je de API correct gebruikt.

Wat is het verschil tussen SQL en NoSQL databases?

SQL-databases zoals PostgreSQL en MySQL gebruiken tabellen met een vast schema en relaties. NoSQL-databases zoals MongoDB slaan documenten of key-value paren op met een flexibel schema. SQL is sterker in consistentie, NoSQL in schaalbaarheid en flexibiliteit.

Tot slot

Werken met databases in Node.js is goed te doen zodra je de basisprincipes onder de knie hebt: kies de juiste database, gebruik connection pooling, schrijf veilige parameterized queries en organiseer je data-access-code netjes. Of je nu voor ruwe SQL kiest of een ORM zoals Prisma, de tooling in het Node.js-ecosysteem is volwassen genoeg om elke schaal aan te kunnen.

Begin klein met een simpele setup, meet waar je bottlenecks zitten en optimaliseer waar nodig. Premature optimalisatie is zelden de moeite waard, maar een solide fundament met pooling en veilige queries is dat zeker wel.

Veelgestelde vragen

Klaar om digitaal te groeien?

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