Skip to content

Database communicatie

Database verbinding in de API

Om gegevens op te slaan en op te halen heeft je webapplicatie een database nodig. In dit hoofdstuk leren we hoe de API-laag communiceert met de database-laag.

SQLite Database Setup

SQLite is een lichtgewicht, bestandsgebaseerde database die perfect is voor het leren en ontwikkelen van webapplicaties. In tegenstelling tot andere databases zoals MySQL of PostgreSQL, heeft SQLite geen aparte server nodig - alle gegevens worden opgeslagen in één enkel bestand op je computer. Dit maakt het ideaal voor ontwikkeling en kleinere applicaties. SQLite ondersteunt de meeste standaard SQL-functionaliteit en is zeer betrouwbaar, ondanks zijn eenvoud. Veel grote bedrijven gebruiken SQLite voor mobiele apps en embedded systemen omdat het snel is en weinig resources gebruikt.

Om SQLite te gebruiken in Node.js, hebben we de sqlite3 library nodig. Deze library biedt een JavaScript interface om met SQLite databases te communiceren.

const sqlite3 = require('sqlite3');

// Verbinding maken met de database
const db = new sqlite3.Database('./flights.db', (err) => {
  if (err) {
    console.error('Fout bij verbinden met database:', err.message);
  } else {
    console.log('Verbonden met de SQLite database.');
  }
});

Database-laag functies

De database-laag bevat functies die SQL-queries uitvoeren. Elke functie heeft een specifieke taak en gebruikt callbacks om resultaten terug te geven. Dit patroon zorgt ervoor dat de database-operaties asynchroon verlopen, zodat de server niet vastloopt terwijl hij wacht op database-antwoorden.

Gegevens ophalen (SELECT)

Voor het ophalen van gegevens gebruiken we SELECT-queries. De db.all() methode wordt gebruikt wanneer we meerdere rijen verwachten, zoals bij het ophalen van alle luchthavens. De eerste functie toont een eenvoudige query die alle kolommen ophaalt, terwijl de tweede functie laat zien hoe je complexere queries met JOINs en WHERE-clausules kunt gebruiken om specifieke data te filteren.

// Alle luchthavens ophalen
function getAllAirports(callback) {
  const query = `
    SELECT *
    FROM airports
  `;

  db.all(query, [], (err, rows) => {
    if (err) {
      console.error('Fout bij ophalen luchthavens:', err);
      callback(err);
    } else {
      callback(null, rows);
    }
  });
}

// Vluchten tussen twee luchthavens zoeken
function getFlightsFromTo(from, to, callback) {
  const query = `
    SELECT f.id, f.flight_number, f.departure_time, f.arrival_time
    FROM flights f
    JOIN airports a1 ON f.departure_airport_id = a1.id
    JOIN airports a2 ON f.arrival_airport_id = a2.id
    WHERE a1.code = ? AND a2.code = ?
  `;

  db.all(query, [from, to], (err, rows) => {
    if (err) {
      console.error('Fout bij ophalen vluchten:', err);
      callback(err);
    } else {
      callback(null, rows);
    }
  });
}

Gegevens toevoegen (INSERT)

Voor het toevoegen van nieuwe gegevens gebruiken we INSERT-queries met de db.run() methode. Deze methode is geschikt voor queries die geen resultaten teruggeven, maar wel de database wijzigen. Let op het gebruik van vraagtekens (?) als placeholders - dit voorkomt SQL injection aanvallen. De this.lastID geeft ons het ID van de nieuw aangemaakte record terug.

// Nieuwe vlucht aanmaken
function createFlight(flightData, callback) {
  const query = `
    INSERT INTO flights (flight_number, departure_airport_id, arrival_airport_id, departure_time, arrival_time) 
    VALUES (?, ?, ?, ?, ?)
  `;

  db.run(query, [
    flightData.flightNr,
    flightData.departureAirportId,
    flightData.arrivalAirportId,
    flightData.departure,
    flightData.arrival
  ], function(err) {
    if (err) {
      console.error('Fout bij aanmaken vlucht:', err);
      callback(err);
    } else {
      callback(null, { id: this.lastID });
    }
  });
}

Controleren of gegevens bestaan

Soms willen we alleen weten of een bepaald record bestaat, zonder alle gegevens op te halen. Hiervoor gebruiken we db.get() die alleen de eerste gevonden rij teruggeeft. Door SELECT 1 te gebruiken in plaats van SELECT *, maken we de query efficiënter omdat we geen echte data hoeven op te halen. De !!row constructie converteert het resultaat naar een boolean waarde.

// Controleren of vluchtnummer al bestaat
function flightNrExists(flightNr, callback) {
  const query = `
    SELECT 1 FROM flights WHERE flight_number = ? LIMIT 1
  `;

  db.get(query, [flightNr], (err, row) => {
    if (err) {
      console.error('Fout bij controleren vluchtnummer:', err);
      callback(err);
    } else {
      // !!row converteert naar boolean: true als row bestaat, false als niet
      callback(null, !!row);
    }
  });
}

Database functies aanroepen vanuit API

In de API-laag roep je de database functies aan om verzoeken te verwerken. Dit is waar de verbinding wordt gemaakt tussen de HTTP-verzoeken van de frontend en de database-operaties. De API-laag fungeert als een brug die de verzoeken vertaalt naar database-acties en de resultaten teruggeeft in een formaat dat de frontend kan begrijpen.

GET-endpoint met database query

Dit voorbeeld toont hoe een eenvoudige GET-endpoint alle luchthavens ophaalt uit de database. De endpoint roept de database-functie aan en handelt zowel succesvolle resultaten als fouten af. Het is belangrijk om altijd foutafhandeling toe te voegen, zodat de frontend een duidelijke melding krijgt als er iets misgaat.

app.get('/airports', (req, res) => {
  console.log('Verzoek ontvangen voor alle luchthavens');

  // Database functie aanroepen
  dblayer.getAllAirports((err, airports) => {
    if (err) {
      console.error('Database fout:', err);
      return res.status(500).json({ error: 'Serverfout' });
    }

    // Resultaat terugsturen naar client
    res.json(airports);
  });
});

GET-endpoint met parameters

Deze endpoint laat zien hoe je parameters uit de URL kunt lezen en doorgeven aan de database-functie. Query parameters zoals ?from=AMS&to=CDG worden automatisch geparsed door Express en zijn beschikbaar via req.query. Deze parameters worden dan gebruikt om specifieke vluchten te zoeken in de database.

app.get('/flights', (req, res) => {
  const from = req.query.from;
  const to = req.query.to;

  console.log(`Zoeken vluchten van ${from} naar ${to}`);

  // Database functie aanroepen met parameters
  dblayer.getFlightsFromTo(from, to, (err, flights) => {
    if (err) {
      console.error('Database fout:', err);
      return res.status(500).json({ error: 'Serverfout' });
    }

    res.json(flights);
  });
});

POST-endpoint met validatie

POST-endpoints zijn complexer omdat ze data ontvangen en validatie nodig hebben voordat ze iets in de database opslaan. Dit voorbeeld toont een typisch patroon: eerst controleren of de data geldig is (in dit geval of het vluchtnummer al bestaat), en dan pas de nieuwe record aanmaken. Dit voorkomt duplicaten en zorgt voor data-integriteit.

app.post('/flights', (req, res) => {
  const flightData = req.body;

  // Eerst controleren of vlucht al bestaat
  dblayer.flightNrExists(flightData.flightNr, (err, exists) => {
    if (err) {
      return res.status(500).json({ error: 'Serverfout bij controle' });
    }

    if (exists) {
      return res.json({ userFeedback: 'Vlucht bestaat al' });
    }

    // Nieuwe vlucht aanmaken
    dblayer.createFlight(flightData, (err, result) => {
      if (err) {
        return res.status(500).json({ error: 'Serverfout bij aanmaken' });
      }

      res.json({ 
        userFeedback: 'Vlucht succesvol aangemaakt',
        flightId: result.id 
      });
    });
  });
});

SQL in SQLite3

SQLite3 ondersteunt vrijwel alle standaard SQL-functionaliteit die je nodig hebt voor webapplicaties. Je kunt alle geldige SQL-queries gebruiken in SQLite3, inclusief complexe JOINs, subqueries, aggregatiefuncties, en geavanceerde WHERE-clausules. Het enige verschil met andere SQL-databases is dat SQLite3 een beperktere set datatypes heeft (TEXT, INTEGER, REAL, BLOB), maar dit converteert automatisch naar de juiste JavaScript types wanneer je de data ophaalt. Voor de meeste webapplicaties biedt SQLite3 alle functionaliteit die je nodig hebt, van eenvoudige SELECT-statements tot complexe queries met meerdere tabellen.

Error Handling in Database Communicatie

Goede foutafhandeling is essentieel voor een betrouwbare webapplicatie. In de database-laag moet je altijd controleren of er fouten optreden bij het uitvoeren van queries, en deze fouten doorgeven aan de callback-functie. Dit zorgt ervoor dat de API-laag weet dat er iets misgegaan is en een passende response kan sturen naar de frontend.

function getAllFlights(callback) {
  const query = 'SELECT * FROM flights';

  db.all(query, [], (err, rows) => {
    if (err) {
      // Log de fout voor debugging
      console.error('Database query fout:', err.message);

      // Geef fout door aan callback
      callback(err);
    } else {
      // Geef resultaat door aan callback
      callback(null, rows);
    }
  });
}

In de API-laag moet je deze fouten opvangen en omzetten naar gebruiksvriendelijke foutmeldingen. Log altijd de technische details voor ontwikkelaars, maar stuur nooit gevoelige informatie naar de frontend. Gebruik de juiste HTTP status codes zodat de frontend weet hoe het moet reageren op verschillende soorten fouten.

app.get('/flights', (req, res) => {
  dblayer.getAllFlights((err, flights) => {
    if (err) {
      // Log de fout
      console.error('Fout bij ophalen vluchten:', err);

      // Stuur gebruiksvriendelijke foutmelding
      return res.status(500).json({ 
        error: 'Er ging iets mis bij het ophalen van de vluchten' 
      });
    }

    res.json(flights);
  });
});

Module Exports

Om je database functies te kunnen gebruiken in andere bestanden, moet je ze exporteren uit je database-laag bestand. Node.js gebruikt het CommonJS module systeem, waarbij je module.exports gebruikt om functies beschikbaar te maken voor andere bestanden. Dit zorgt voor een nette scheiding tussen verschillende onderdelen van je applicatie.

module.exports = {
  getAllAirports,
  getAllFlights,
  getFlightsFromTo,
  flightNrExists,
  createFlight
};

In je API-bestand importeer je vervolgens de database-laag met require(). Het is een goede gewoonte om een duidelijke naam te gebruiken zoals dblayer zodat het meteen duidelijk is dat je database-functies aanroept.

const dblayer = require('./db-layer');

Met deze kennis kun je een complete verbinding maken tussen je API-endpoints en je database, zodat je webapplicatie gegevens kan opslaan en ophalen.