Ga naar hoofdinhoud

API-endpoints met MySQL

We breiden de planeten-applicatie uit zodat we planeten kunnen toevoegen, bekijken en verwijderen.

We bouwen verder op Gebruik in Express.js. We voegen databasefuncties toe aan database.ts en routes aan routers/planetsRouter.ts. De andere bestanden blijven hetzelfde.

Overzicht

MethodeEndpointBewerkingGeslaagd antwoord
GET/planetsAlle planeten lezen200 met een JSON-array
GET/planets/:idEén planeet lezen200 met een JSON-object
POST/planetsEen planeet toevoegen201 met het aangemaakte object
DELETE/planets/:idEen planeet verwijderen204 zonder body

De router verwerkt de request en stuurt de response. De databasefuncties voeren de SQL-queries uit. Alle waarden uit requests geven we via placeholders (?) door aan execute().

Databasefuncties toevoegen

Voeg ResultSetHeader toe aan de bestaande import bovenaan database.ts:

database.ts (import)
import mysql, { Connection, ConnectionOptions, FieldPacket, RowDataPacket, ResultSetHeader } from "mysql2/promise";

De bestaande functies blijven staan. Voeg de volgende functies onderaan database.ts toe.

Eén planeet ophalen

database.ts
export async function getPlanetById(id: number): Promise<Planet | undefined> {
const [rows]: [RowDataPacket[], FieldPacket[]] = await databaseConnection.execute(
"SELECT id, name FROM planet WHERE id = ?",
[id]
);
return rows[0] as Planet | undefined;
}

Ook wanneer we één rij zoeken, geeft MySQL een array terug. We nemen de eerste rij. Als het id niet bestaat, krijgen we undefined.

Een planeet toevoegen

database.ts
export async function createPlanet(name: string): Promise<Planet | undefined> {
const [result]: [ResultSetHeader, FieldPacket[]] = await databaseConnection.execute(
"INSERT INTO planet (name) VALUES (?)",
[name]
);
return await getPlanetById(result.insertId);
}

MySQL maakt het id aan via AUTO_INCREMENT. De INSERT-query geeft een ResultSetHeader terug met het nieuwe id in insertId. We gebruiken getPlanetById() om de aangemaakte planeet terug te geven.

Een planeet verwijderen

database.ts
export async function deletePlanet(id: number): Promise<void> {
await databaseConnection.execute(
"DELETE FROM planet WHERE id = ?",
[id]
);
}

Deze functie verwijdert de planeet met het opgegeven id. Vergeet bij DELETE de WHERE-clause niet: zonder die voorwaarde zou je alle rijen verwijderen.

Routes toevoegen

Pas de imports bovenaan routers/planetsRouter.ts aan:

routers/planetsRouter.ts (imports)
import { Router } from "express";
import { Planet } from "../types";
import { getPlanets, getPlanetById, createPlanet, deletePlanet } from "../database";

Plaats de volgende routes in de functie planetsRouter(), na de bestaande GET-route en vóór return router;. Omdat de router in index.ts al aan /planets gekoppeld is, gebruiken we hier alleen / en /:id.

Eén planeet ophalen

routers/planetsRouter.ts (GET)
router.get("/:id", async (req, res): Promise<void> => {
const id: number = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
res.status(400).json({ error: "Ongeldig id." });
return;
}

const planet: Planet | undefined = await getPlanetById(id);
if (planet) {
res.json(planet);
} else {
res.status(404).json({ error: "Planeet niet gevonden." });
}
});

We zetten het id uit de URL om naar een getal. Een ongeldig id krijgt 400. Als de databasefunctie geen planeet vindt, antwoorden we met 404.

Een planeet toevoegen

routers/planetsRouter.ts (POST)
router.post("/", async (req, res): Promise<void> => {
const name: unknown = req.body?.name;
if (typeof name !== "string" || name.trim().length === 0 || name.length > 100) {
res.status(400).json({ error: "Geef een name van 1 tot 100 tekens." });
return;
}

const planet: Planet | undefined = await createPlanet(name.trim());
if (!planet) {
res.status(500).json({ error: "Planeet kon niet worden aangemaakt." });
return;
}
res.status(201).json(planet);
});

Dankzij express.json() vinden we de JSON-data in req.body. We nemen de naam eerst over als unknown: de client kan immers ook een getal of niets opsturen. Na de typeof-controle weet TypeScript dat name een string is.

We geven alleen de naam door aan de databasefunctie. De client bepaalt dus niet zelf het id. Bij succes sturen we 201 met de nieuwe planeet terug.

Een planeet verwijderen

routers/planetsRouter.ts (DELETE)
router.delete("/:id", async (req, res): Promise<void> => {
const id: number = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
res.status(400).json({ error: "Ongeldig id." });
return;
}

const planet: Planet | undefined = await getPlanetById(id);
if (!planet) {
res.status(404).json({ error: "Planeet niet gevonden." });
return;
}
await deletePlanet(id);
res.status(204).send();
});

We controleren eerst of de planeet bestaat. Na het verwijderen antwoorden we met 204 No Content. Die response heeft geen body.

De API testen

Compileer en herstart de server na je wijzigingen:

npx tsc
node dist/index.js

Voer de volgende requests uit in een tweede terminal. Met -i zie je ook de HTTP-status en headers. Op Windows kan je hiervoor Git Bash gebruiken.

Alle planeten opvragen

curl -i http://localhost:3000/planets

Een planeet toevoegen

curl -i -X POST http://localhost:3000/planets \
-H 'Content-Type: application/json' \
-d '{"name":"Mars"}'

Je krijgt status 201, een Location-header en bijvoorbeeld:

{ "id": 3, "name": "Mars" }

Het id kan bij jou anders zijn. Gebruik in de volgende requests het id uit jouw response in plaats van 3.

Eén planeet opvragen

curl -i http://localhost:3000/planets/3

De planeet verwijderen

curl -i -X DELETE http://localhost:3000/planets/3

Je krijgt status 204 zonder body. Een volgende GET of DELETE met hetzelfde id geeft 404.

Ongeldige invoer

curl -i -X POST http://localhost:3000/planets \
-H 'Content-Type: application/json' \
-d '{"name":" "}'

curl -i http://localhost:3000/planets/abc

Beide requests geven 400 met een JSON-foutmelding. Herstart tot slot de server en vraag de lijst opnieuw op: de wijzigingen zijn in MySQL opgeslagen en blijven bewaard.

In de volgende hoofdstukken stellen we CORS in en maken we een Vite-pagina met een planetenlijst en een formulier.