# Back-end API Development
Een back-end API bestaat uit verschillende onderdelen. De webserver ontvangt inkomende HTTP-verzoeken van clients (zoals webbrowsers, mobiele apps, of andere servers). De routes bepalen hoe inkomende verzoeken worden doorgestuurd naar de bijbehorende controllers of verwerkingslogica. De controllers ontvangen de inkomende verzoeken, verwerken de gegevens indien nodig, en sturen de juiste respons terug naar de client. De gegevens komen vaak uit databases om gegevens op te slaan, op te halen, bij te werken en te verwijderen.
# Webserver
Het opzetten van een webserver wordt uitvoerig besproken in de setup van Express.
project-root
│
├── src
│ └── app.js
│
├── node_modules
├── package.json
└── .gitignore
2
3
4
5
6
7
8
app.js
const express = require("express");
const app = express();
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => {
console.log(`De server luistert op poort ${PORT}`);
});
2
3
4
5
6
7
# Body Parser
Importeert Express, bodyParser (opens new window) (voor het verwerken van JSON-verzoeken), en userRoutes. Initialiseert de Express-app en voegt de bodyParser en userRoutes toe. Start de server op een opgegeven poort.
// app.js
const express = require("express");
const bodyParser = require("body-parser");
const userRoutes = require("./routes/userRoutes");
const app = express();
app.use(bodyParser.json());
app.use("/api", userRoutes); // Middleware om /api prefix te hebben.
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server gestart op poort ${PORT}`);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Routes
Gebruik app.METHOD(PATH, HANDLER) om een route te definiëren, waarbij METHOD een HTTP-methode is (bijv. get, post, put, delete), PATH het pad van de route is, en HANDLER de functie is die wordt uitgevoerd wanneer de route wordt aangeroepen.
app.get("/", (req, res) => {
res.send("Welkom op de hoofdpagina!");
});
app.post("/users", (req, res) => {
// Verwerk POST-verzoek voor gebruikerscreatie
});
2
3
4
5
6
7
# Dynamische path-parameters
Definieer parameters in de route met een dubbele punt :. Deze parameters worden beschikbaar gesteld aan de routehandler via req.params.
app.get("/users/:userId", (req, res) => {
const userId = req.params.userId;
// Gebruik userId in de logica van de route
});
2
3
4
# Query-parameters
GET verzoek naar '/search?q='Jan''
app.get("/search", (req, res) => {
const query = req.query.q;
// Voer zoekactie uit op basis van de queryparameter
});
2
3
4
# Router middleware
Voeg middlewarefuncties toe aan een specifieke route of een groep routes met app.use of router.use.
const middlewareFunction = (req, res, next) => {
// Voer middleware-logica uit
next(); // Belangrijk om door te gaan naar de volgende middleware of routehandler
};
app.use("/admin", middlewareFunction, (req, res) => {});
2
3
4
5
6
# Express Router
Gebruik express.Router om modulaire routers te maken en routes te organiseren in afzonderlijke bestanden.
userRoutes.js
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
// Logica voor het ophalen van alle gebruikers
});
module.exports = router;
2
3
4
5
6
7
8
app.js
const userRoutes = require("./userRoutes");
app.use("/users", userRoutes);
2
project-root
│
├── src
│ ├── routes
│ │ └── userRoutes.js
│ └── app.js
│
├── node_modules
├── package.json
└── .gitignore
2
3
4
5
6
7
8
9
10
# Database initialiseren
Een database is een georganiseerde verzameling van gestructureerde gegevens. Dankzij een database kunnen we data op een systematische en efficiënte manier opslaan, beheren en opvragen. Het gebruik van databases verbetert bovendien de efficiëntie, schaalbaarheid, en betrouwbaarheid van backend-systemen.
Node.js ondersteunt verschillende soorten databases, en de keuze hangt af van de aard van de applicatie, de vereisten en de voorkeur van de ontwikkelaar. Meer uitleg over enkele populaire voorbeelden zoals MongoDB, Redis, SQLLite, … vind je in de cursus van Computersystemen (opens new window).
In deze introductie tot back-end development gebruiken we een JSON-bestand als database. Het gebruik van een JSON-bestand als database in een Node.js/Express.js-applicatie heeft zowel voordelen als nadelen, en de keuze hangt vaak af van de aard van het project en de specifieke vereisten. Hier zijn enkele overwegingen:
# Voordelen
- Eenvoudige implementatie: Het gebruik van een JSON-bestand als database is eenvoudig te implementeren, vooral voor kleinere projecten of tijdens de ontwikkelingsfase.
- Weinig configuratie: Er is geen noodzaak voor het opzetten van een apart databasesysteem, wat handig kan zijn bij kleinere projecten of voor beginners.
- Snelle prototyping of opbouwen van gegevensarchitectuur: Het is handig voor het snel prototypen van een applicatie wanneer je nog niet klaar bent om een volwaardige database te integreren.
Een JSON-bestand als database is voornamelijk geschikt voor simpele toepassingen waar geen complexe query’s of transacties nodig zijn.
# Nadelen
- Beperkte schaalbaarheid: Niet geschikt voor grootschalige toepassingen waarbij de belasting op de database toeneemt, aangezien bestandsbewerkingen minder efficiënt zijn dan databasequery’s.
- Gebrek aan geavanceerde functionaliteiten: JSON-bestanden bieden niet de geavanceerde functionaliteiten die relationele databasesystemen bieden, zoals gegevensintegriteit, complexe query’s en transacties.
- Gedeelde toegang en concurrency (gelijktijdigheid) problemen: Bij gelijktijdige schrijfoperaties kunnen er problemen met gedeelde toegang en concurrency optreden, aangezien JSON-bestanden niet zijn ontworpen voor gelijktijdige schrijfbewerkingen.
- Beveiligingsrisico’s: JSON-bestanden zijn toegankelijk via het bestandssysteem, wat beveiligingsrisico’s met zich meebrengt als er geen juiste toegangscontroles zijn ingesteld.
Vermijd het gebruik van JSON-bestanden als database voor grootschalige, complexe applicaties waar geavanceerde databasefunctionaliteiten, schaalbaarheid en gelijktijdige toegang van belang zijn.
# Bestand aanmaken
Creëer een nieuw bestand genaamd data.json. Dit bestand zal fungeren als onze eenvoudige database voor het opslaan en uitlezen van gegevens.
project-root
│
├── data
│ └── users.json
│
├── src
│ ├── routes
│ │ └── userRoutes.js
│ └── app.js
│
├── node_modules
├── package.json
└── .gitignore
2
3
4
5
6
7
8
9
10
11
12
13
Je kan starten met een leeg bestand of met enkele startgegevens.
data.json
[]
users.json
[
{ "id": 1, "name": "John Doe" },
{ "id": 2, "name": "Jane Doe" }
]
2
3
4
# Verwerkingslogica of CRUD
CRUD staat voor de vier basisbewerkingen die kunnen worden uitgevoerd op gegevens in een database of andere opslagplaats.
- Create (C): Toevoegen van nieuwe gegevens aan de database. Dit houdt in dat nieuwe records of entiteiten worden gemaakt en in de database worden opgeslagen.
- Read (R): Ophalen van gegevens uit de database, het opvragen van bestaande records of entiteiten om ze te bekijken of te gebruiken.
- Update (U): Aanpassen van bestaande gegevens in de database. Hiermee worden wijzigingen aangebracht in de bestaande records of entiteiten.
- Delete (D): Verwijderen van gegevens uit de database. Dit betekent het permanent verwijderen van records of entiteiten uit de database.
Indien we op een bepaalde entiteit éen of meer CRUD-operaties willen toepassen, maken we een nieuw bestand genaamd een controller. Elke controller is verantwoordelijk voor het ontvangen van invoer, verwerken van logica, deze verwerkte invoer terug te sturen en aanpassen in de database, …
project-root
│
├── data
│ └── users.json
│
├── src
│ ├── controllers
│ │ └── userController.js
│ ├── routes
│ │ └── userRoutes.js
│ └── app.js
│
├── node_modules
├── package.json
└── .gitignore
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Om het overzichtelijk te houden, maken we voor iedere entiteit een afzonderlijke controller. In een controller vinden we de acties om data te lezen, aan te maken, te bewerken en te verwijderen.
# Read
Wanneer een GET-verzoek wordt gedaan naar "/users", wordt de “Read” actie uitgevoerd en worden alle gebruikersgegevens teruggestuurd als JSON-reactie.
app.js
const userRoutes = require("./routes/userRoutes");
app.use("/api", userRoutes);
2
Je kunt de “Read” actie toewijzen aan een specifieke route in je router.
userRoutes.js
const express = require("express");
const router = express.Router();
const UserController = require("../controllers/userController");
// Route voor het ophalen van alle gebruikers (user prefix via Middleware)
router.get("/users", UserController.getAllUsers);
// Andere routes kunnen hier worden toegevoegd
module.exports = router;
2
3
4
5
6
7
8
9
10
Om data uit te lezen uit een bestand gebruiken we de ingebouwde fs-module.
userController.js
const fsp = require("fs/promises");
const path = require("path");
const usersFilePath = path.join(__dirname, "..", "data", "users.json");
// Functie om alle gebruikers op te halen
const getAllUsers = async (req, res) => {
try {
// Lees de gegevens uit het JSON-bestand
const data = await fsp.readFile(usersFilePath, "utf8");
const users = JSON.parse(data);
// Stuur de gebruikersgegevens als JSON-reactie naar de client
res.json({ users });
} catch (error) {
console.error(error);
// Stuur een foutreactie als er een probleem is bij het ophalen van de gegevens
res.status(500).json({ error: "Internal Server Error" });
}
};
// Andere controllerfuncties kunnen hier worden toegevoegd
module.exports = {
getAllUsers,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
In dit voorbeeld gebruiken we fs.readFile om de inhoud van het JSON-bestand te lezen, parsen het naar een JavaScript-object, en sturen het terug als een JSON-reactie naar de client. Als er een fout optreedt, sturen we een foutreactie met de statuscode 500 (Internal Server Error).
# Create
In je router (bijvoorbeeld userRoutes.js), definieer een nieuwe route voor het aanmaken van een gebruiker. Hier gebruiken we de HTTP-methode POST omdat we gegevens willen toevoegen aan de database.
Bestand
const express = require("express");
const router = express.Router();
const UserController = require("../controllers/userController");
// Route voor het aanmaken van een nieuwe gebruiker
router.post("/users", UserController.createUser);
// Andere routes kunnen hier worden toegevoegd
module.exports = router;
2
3
4
5
6
7
8
9
10
In de controller (bijvoorbeeld userController.js), implementeer de functie voor het aanmaken van een nieuwe gebruiker. In dit voorbeeld gebruiken we de fs-module om het JSON-bestand te lezen en schrijven.
userController.js
const fsp = require("fs/promises");
const path = require("path");
const usersFilePath = path.join(__dirname, "..", "data", "data.json");
// Functie voor het aanmaken van een nieuwe gebruiker
const createUser = async (req, res) => {
// Haal de naam van de gebruiker op uit het verzoek
const { name } = req.body;
try {
// Lees de bestaande gebruikersgegevens uit het JSON-bestand
const data = await fsp.readFile(usersFilePath, "utf8");
const users = JSON.parse(data);
// Creëer een nieuwe gebruiker met een uniek ID
// Of gebruik hiervoor een module om een nieuwe ID te genereren.
const newUser = { id: users.length + 1, name };
// Voeg de nieuwe gebruiker toe aan de bestaande array
users.push(newUser);
// Schrijf de bijgewerkte gebruikersgegevens terug naar het JSON-bestand
await fsp.writeFile(usersFilePath, JSON.stringify(users, null, 2));
// Stuur de nieuw aangemaakte gebruiker als JSON-reactie naar de client
res.status(201).json({ user: newUser });
} catch (error) {
console.error(error);
// Stuur een foutreactie als er een probleem is bij het aanmaken van de gebruiker
res.status(500).json({ error: "Internal Server Error" });
}
};
// Andere controllerfuncties kunnen hier worden toegevoegd
module.exports = {
createUser,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Wanneer een POST-verzoek wordt gedaan naar de "/users" route, wordt de createUser-functie uitgevoerd. Deze functie:
- Haalt de naam van de gebruiker op uit het verzoek
- Leest de bestaande gebruikersgegevens uit het JSON-bestand
- Voegt de nieuwe gebruiker toe
- Schrijft de bijgewerkte gegevens terug naar het bestand.
- Ten slotte stuurt het een JSON-reactie met de nieuw aangemaakte gebruiker naar de client.
# Update
In je router (bijvoorbeeld userRoutes.js), definieer een nieuwe route voor het bijwerken van een gebruiker. We gebruiken de HTTP-methode PUT omdat we gegevens willen bijwerken.
userRoutes.js
const express = require("express");
const router = express.Router();
const UserController = require("../controllers/userController");
// Route voor het bijwerken van een bestaande gebruiker
router.put("/users/:id", UserController.updateUser);
// Andere routes kunnen hier worden toegevoegd
module.exports = router;
2
3
4
5
6
7
8
9
10
In de controller (bijvoorbeeld “userController.js”), implementeer de functie voor het bijwerken van een bestaande gebruiker. Net als bij het aanmaken, gebruiken we de fs-module om het JSON-bestand te lezen en schrijven.
userController.js
const fsp = require("fs/promises");
const path = require("path");
const usersFilePath = path.join(__dirname, "../data/data.json");
// Functie voor het bijwerken van een bestaande gebruiker
const updateUser = async (req, res) => {
// Haal het gebruikers-ID en de bij te werken naam op uit het verzoek
const userId = parseInt(req.params.id);
const { name } = req.body;
try {
// Lees de bestaande gebruikersgegevens uit het JSON-bestand
const data = await fsp.readFile(usersFilePath, "utf8");
let users = JSON.parse(data);
// Zoek de index van de gebruiker die moet worden bijgewerkt
const userIndex = users.findIndex((u) => u.id === userId);
// Als de gebruiker niet wordt gevonden, stuur een foutreactie
if (userIndex === -1) {
return res.status(404).json({ error: "User not found" });
}
// Werk de naam van de gebruiker bij
users[userIndex].name = name;
// Schrijf de bijgewerkte gebruikersgegevens terug naar het JSON-bestand
await fsp.writeFile(usersFilePath, JSON.stringify(users, null, 2));
// Stuur de bijgewerkte gebruiker als JSON-reactie naar de client
res.json({ user: users[userIndex] });
} catch (error) {
console.error(error);
// Stuur een foutreactie als er een probleem is bij het bijwerken van de gebruiker
res.status(500).json({ error: "Internal Server Error" });
}
};
// Andere controllerfuncties kunnen hier worden toegevoegd
module.exports = {
updateUser,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Wanneer een PUT-verzoek wordt gedaan naar de “/users/:id” route, wordt de updateUser-functie uitgevoerd. Deze functie haalt het gebruikers-ID en de bij te werken naam op uit het verzoek, zoekt de gebruiker in de gegevens, werkt de naam bij, schrijft de bijgewerkte gegevens terug naar het bestand, en stuurt een JSON-reactie met de bijgewerkte gebruiker naar de client.
# Delete
In je router (bijvoorbeeld “userRoutes.js”), definieer een nieuwe route voor het verwijderen van een gebruiker. We gebruiken de HTTP-methode DELETE omdat we gegevens willen verwijderen.
const express = require("express");
const router = express.Router();
const UserController = require("../controllers/userController");
// Route voor het verwijderen van een bestaande gebruiker
router.delete("/users/:id", UserController.deleteUser);
// Andere routes kunnen hier worden toegevoegd
module.exports = router;
2
3
4
5
6
7
8
9
10
In de controller (bijvoorbeeld “userController.js”), implementeer de functie voor het verwijderen van een bestaande gebruiker. Net als eerder gebruiken we de fs-module om het JSON-bestand te lezen en schrijven.
userController.js
const fsp = require("fs/promises");
const path = require("path");
const usersFilePath = path.join(__dirname, "../data/data.json");
// Functie voor het verwijderen van een bestaande gebruiker
const deleteUser = async (req, res) => {
// Haal het gebruikers-ID op uit het verzoek
const userId = parseInt(req.params.id);
try {
// Lees de bestaande gebruikersgegevens uit het JSON-bestand
const data = await fsp.readFile(usersFilePath, "utf8");
let users = JSON.parse(data);
// Zoek de index van de gebruiker die moet worden verwijderd
const userIndex = users.findIndex((u) => u.id === userId);
// Als de gebruiker niet wordt gevonden, stuur een foutreactie
if (userIndex === -1) {
return res.status(404).json({ error: "User not found" });
}
// Verwijder de gebruiker uit de array
const deletedUser = users.splice(userIndex, 1)[0];
// Schrijf de bijgewerkte gebruikersgegevens terug naar het JSON-bestand
await fsp.writeFile(usersFilePath, JSON.stringify(users, null, 2));
// Stuur de verwijderde gebruiker als JSON-reactie naar de client
res.json({ user: deletedUser });
} catch (error) {
console.error(error);
// Stuur een foutreactie als er een probleem is bij het verwijderen van de gebruiker
res.status(500).json({ error: "Internal Server Error" });
}
};
// Andere controllerfuncties kunnen hier worden toegevoegd
module.exports = {
deleteUser,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Wanneer een DELETE-verzoek wordt gedaan naar de “/users/:id” route, wordt de deleteUser-functie uitgevoerd. Deze functie haalt het gebruikers-ID op uit het verzoek, zoekt de gebruiker in de gegevens, verwijdert de gebruiker uit de array, schrijft de bijgewerkte gegevens terug naar het bestand, en stuurt een JSON-reactie met de verwijderde gebruiker naar de client.
# Validatie en foutafhandeling
# Try-catch
Gebruik try en catch in je controllerfuncties om specifieke fouten af te vangen en een passende reactie te sturen.
// Controllerfunctie met try-catch
const exampleController = async (req, res) => {
try {
// Voer hier je logica uit
throw new Error("Dit is een voorbeeldfout");
} catch (error) {
console.error("Fout opgetreden in controller:", error);
// Stuur een foutreactie naar de client
res.status(500).json({ error: "Internal Server Error" });
}
};
// Gebruik de controller in je Express-routes
app.get("/example", exampleController);
2
3
4
5
6
7
8
9
10
11
12
13
14
Bij asynchrone code met Promises, zoals bij het lezen en schrijven van bestanden, kun je try en catch gebruiken om asynchrone fouten op te vangen.
// Asynchrone code met try-catch
const exampleAsyncFunction = async () => {
try {
// Asynchrone logica, bijvoorbeeld lezen van een bestand
const data = await fs.readFile("bestand.txt", "utf8");
console.log(data);
} catch (error) {
console.error("Fout opgetreden bij het lezen van het bestand:", error);
}
};
// Roep de asynchrone functie aan
exampleAsyncFunction();
2
3
4
5
6
7
8
9
10
11
12
13
Het gebruik van try en catch is ontzettend belangrijk en essentieel om de foutafhandeling te verbeteren en de robuustheid van je code te vergroten. Zorg ervoor dat je de fouten die je specifiek wilt afhandelen identificeert en passende reacties stuurt naar de client of logs genereert voor verdere analyse.
# Middleware
Overweeg het gebruik van middleware voor algemene validatie en foutafhandeling. Bijvoorbeeld, een middleware die controleert op de aanwezigheid van een Content-Type: application/json header in de inkomende verzoeken.
middleware.js
// Middleware voor het controleren van Content-Type header
const checkContentType = (req, res, next) => {
if (!req.is("application/json")) {
return res.status(400).json({ error: "Invalid Content-Type" });
}
next();
};
2
3
4
5
6
7
app.js
// Gebruik de middleware in je app.
app.use(checkContentType);
2
# Testen met API Client
We streven naar onafhankelijkheid van de frontend-ontwikkeling om eventuele onnauwkeurigheden of problemen in de backend te identificeren.
Door gebruik te maken van een API-client (zoals Hoppscotch), kunnen ontwikkelaars de API consumeren en verschillende scenarios simuleren. Dit stelt ons in staat om:
- de betrouwbaarheid van de API te controleren
- de gegevensrespons te verifiëren
- eventuele fouten te identificeren
Dit allemaal zonder afhankelijk te zijn van een frontend die mogelijk nog in ontwikkeling is.
Deze aanpak verbetert niet alleen de efficiëntie bij het testen, maar waarborgt ook de consistentie en nauwkeurigheid van de backend-functionaliteit, ongeacht de status van de frontend-implementatie.
# Eindstructuur
Hier is een voorbeeld van een eenvoudige en schaalbare structuur:
project-root
│
├── data
│ └── users.json
├── src
│ ├── controllers
│ │ ├── UserController.js
│ │ ├── OtherController.js
│ │ └── ...
│ ├── routes
│ │ ├── userRoutes.js
│ │ ├── otherRoutes.js
│ │ └── ...
│ ├── middleware
│ │ ├── authMiddleware.js
│ │ ├── loggingMiddleware.js
│ │ └── ...
│ └── app.js
│
├── node_modules
├── package.json
└── .gitignore
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
src: Hier bevindt zich de broncode van de applicatie
- controllers: Elke controller behandelt de logica voor een specifieke resource.
Bijvoorbeeld,
UserController.jszou verantwoordelijk zijn voor gebruikersgerelateerde logica zoals het ophalen van users uit. - routes: Definieer hier de routering van je applicatie. Maak aparte routebestanden voor elke resource, zoals
userRoutes.js. - models: Plaats de datamodellen hier. Bijvoorbeeld,
UserModel.jsdefinieert het gebruikersmodel. - middleware: Bevat herbruikbare middleware-functies. Bijvoorbeeld, authMiddleware.js kan authenticatielogica bevatten.
- controllers: Elke controller behandelt de logica voor een specifieke resource.
Bijvoorbeeld,
app.js: Dit bestand initialiseert en configureert de Express-applicatie. Hier worden de controllers, routes en middleware geïntegreerd.
node_modules: Hier worden de externe modules geïnstalleerd via npm.
package.json: Het configuratiebestand voor npm, waarin projectafhankelijkheden en scripts worden gespecificeerd.
.gitignore: Specificeert bestanden die moeten worden genegeerd door Git, zoals de node_modules-map.