Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Als je nog nooit met versiebeheer gewerkt hebt, kan het moeilijk zijn om je een goed beeld te vormen van hoe alle puzzelstukjes van Git in elkaar passen. Laat ons het dus eerst even hebben over wat Git precies is, voor we het leren gebruiken.
Git is een systeem voor versiebeheer, dat wil zeggen een systeem om oudere versies van je project op een ordelijke manier bij te houden. Als je geen oude versies bijhoudt, riskeer je belangrijke code te verliezen (bv. als je in een nieuwe versie een kritieke bug vindt die er in een oude versie niet was). Je kan klassieke backups maken door regelmatig al je code te zippen, maar voor je het weet, heb je enorm veel schijfruimte verbruikt of weet je niet meer in welk bestand de interessantste aanpassingen gebeurd zijn. Met een goed systeem voor versiebeheer gaat dat soort werk veel efficiënter.
Meestal bestaat dat project dat je met een versiebeheersysteem beheert uit code, maar dat hoeft niet. Je kan er vanalles mee bijhouden, van tekeningen in Illustrator tot recepten. Zelfs data in die je niet kan voorstellen als leesbare tekst is mogelijk, bijvoorbeeld audiobestanden, maar versiebeheersystemen komen het best tot hun recht als de data in een tekstformaat staat.
Je kan git versie beheer volledig lokaal doen, maar in de meeste gevallen zal je een remote repository gebruiken. Dat is een server waar je je code op plaatst, zodat je er vanop verschillende computers aan kan werken. Je kan je code dan ook delen met anderen, zodat zij er aan kunnen werken. Dat is handig als je met meerdere mensen aan een project werkt, maar ook als je je code wil delen met de wereld. Github is een van de bekendste websites waar je zo'n remote repository kan aanmaken.
Ga naar de officiële Git-website git-scm.com en download de laatste versie van Git voor Windows. Dit bestand is een uitvoerbaar (.exe) bestand.
Volg de instructies van de installatie wizard. De standaardinstellingen zijn prima. Zorg er zeker voor dat je ook Git Bash
installeert. Dit is een command line interface (CLI) die je toelaat om git commando's uit te voeren.
Nadat je de installatie hebt voltooid, kan je git bash
openen door in het start menu te zoeken naar git bash
. Je kan ook rechtsklikken in een map en kiezen voor Git Bash Here
. Dit opent een command line interface in de map waarin je rechtsklikte. Dit is handig om git commando's uit te voeren in de juiste map.
Git heeft een aantal instellingen die je kan aanpassen. Je kan dit doen via de command line interface. Open git bash
en voer de volgende commando's uit:
Vervang Jouw Naam
en Jouw Email
door je eigen naam en email adres. Deze gegevens worden gebruikt om je commits te identificeren. Je kan ook nog andere instellingen aanpassen, maar dat is niet nodig voor deze cursus.
Git is een command line tool. Dat wil zeggen dat je het gebruikt door commando's in te typen in een command line interface. Het is op dit moment nog niet nodig om alle commando's te kennen. We zien hier enkel de basis commando's die je nodig hebt om te starten met deze cursus.
Let op dat de uitleg hier onder een sterk vereenvoudigde versie is van wat git kan. Git is een heel krachtige tool, maar dat maakt het ook complex. We gaan hier enkel de basis commando's zien die je nodig hebt om te starten met deze cursus. Als je meer wil weten over git, kan je altijd de officiële documentatie raadplegen of volg je de git leerlijn gitbook.
Stel dat we een bestand README.md
hebben aangepast, een bestand dat al in de git repository zit. Aanpassingen worden niet zomaar opgeslagen in git. Je moet ze eerst altijd toevoegen aan de staging area. De staging area in Git is een tussenstap tussen je lokale bestanden en de geschiedenis van je project (de repository). Om een bestand toe te voegen aan de staging area, gebruik je het commando git add
. De naam van dit commando is een beetje verwarrend, want je voegt niet echt bestanden toe, maar je voegt ze toe aan de staging area. Dus je doet dit ook voor bestanden die al bestonden en in de repository zitten.
Zorg dan zeker en vast dat je in de juiste map zit. Je kan dit controleren met het commando pwd
. Dit commando toont de huidige map waarin je zit.
Maak je een nieuw bestand aan dat je wil toevoegen aan de repository, dan moet je het ook eerst toevoegen aan de staging area. Dit doe je met hetzelfde commando.
Heb je meerdere bestanden die je wil toevoegen, of meerdere wijzigingen in verschillende bestanden, dan kan je met 1 commando meerdere alle gewijzigde bestanden toevoegen aan de staging area.
of
als je alleen de bestanden in de huidige map wil toevoegen.
Om te weten welke bestanden je hebt toegevoegd aan de staging area, kan je het commando git status
gebruiken. Dit commando toont je de status van je repository. Het toont je welke bestanden je hebt toegevoegd aan de staging area, welke bestanden je hebt aangepast en welke bestanden je nog niet hebt toegevoegd.
Dan krijg je iets zoals dit:
Dit wil zeggen dat je de bestanden README.md
en nieuw_bestand.md
hebt toegevoegd aan de staging area. Als je nu een commit maakt, dan zullen deze bestanden in de geschiedenis van je project terecht komen.
Als je klaar bent met het toevoegen van bestanden aan de staging area, dan kan je de wijzigingen committen. Dit wil zeggen dat je de wijzigingen definitief maakt en toevoegt aan de geschiedenis van je project. Je kan dit doen met het commando git commit
. Dit commando heeft een aantal opties, maar de belangrijkste is -m
. Hiermee kan je een boodschap meegeven aan je commit. Deze boodschap is verplicht. Het is een korte beschrijving van de wijzigingen die je hebt aangebracht. Het is belangrijk dat deze boodschap duidelijk is, zodat je later nog weet wat je hebt gedaan.
Het is perfect mogelijk om git volledig lokaal te gebruiken, maar het is pas echt krachtig als je het combineert met een remote repository. Een remote repository is een server waar je je code op plaatst, zodat je er vanop verschillende computers aan kan werken. Je kan je code dan ook delen met anderen, zodat zij er aan kunnen werken. Dat is handig als je met meerdere mensen aan een project werkt, maar ook als je je code wil delen met de wereld. Github is een van de bekendste websites waar je zo'n remote repository kan aanmaken. Wij gaan in deze cursus gebruik maken van Github. Normaal gezien omdat we gebruik maken van devcontainers heeft jouw git repository al een remote repository en hoeven we hier niets meer voor te doen.
Om alle commits die je lokaal hebt gemaakt naar de remote repository te sturen, gebruik je het commando git push
.
Dit commando stuurt alle commits die je lokaal hebt gemaakt naar de remote repository. Het is belangrijk dat je dit commando uitvoert, anders zullen je commits niet zichtbaar zijn voor anderen en ben je ze kwijt als er iets met je computer gebeurt.
Tijdens het onderdeel van datatypes hebben we heel kort het concept Array
laten vallen. In TypeScript is een Array een lijst van waarden. Elke waarde kan aangesproken worden aan de hand van een index.
Net zoals bij andere variabelen moeten we in TypeScript bij het maken van een variabele voor een array een type geven. Dit type kan je op twee verschillende manieren uitdrukken. Het is in TypeScript niet de bedoeling om verschillende types in een array te steken. Als je kiest voor 1 type dan moeten de rest van de elementen van hetzelfde type zijn.
We kiezen bijvoorbeeld een array van getallen (numbers). De declaratie van de variabele zal er als volgt uit zien
We willen meestal ook als begin waarde een lege array meegeven. Er zitten dus op dat moment nog geen waarden in. We kunnen een lege array toekennen aan de variabele op de volgende manier:
In andere talen zoals Java en C# moet je de lengte van de array meegeven. In JavaScript en TypeScript is dat niet zo. De array zal groeien met het aantal elementen er in geplaatst worden.
Je kan ook op voorhand al een aantal elementen meegeven:
Om een element op te vragen van een array kan je dat doen aan de hand van vierkante haakjes met daarin een getal. Dit getal komt overeen met de positie van het element dat je wil opvragen. Let op: ook in TypeScript begint het eerste element bij 0.
Vraag je een element voor een index op die niet bestaat dan krijg je undefined
Als je een element wil vervangen kan je dit op de volgende manier doen:
Je kan ook elementen toevoegen nadat je de array hebt gedeclareerd:
Soms is het nodig om te weten hoeveel elementen er in de array zitten. Dit kan je met length
doen.
Je kan ook de array uitprinten in je console venster. Dit is vooral handig tijdens het debuggen
Ook een loop over een array is zeer gelijkaardig aan JavaScript.
Door type inference is het ook mogelijk het type van i
weg te laten. TypeScript zal dit automatisch als een number
type beschouwen.
In de for...of
loop wordt in TypeScript de types van het element automatisch ingevuld. Je moet dus ook het type van fruit
weglaten.
Een array kan ook een array bevatten. Dit noemen we een multi-dimensionale array. Je kan dit op de volgende manier doen:
Je kan deze array op dezelfde manier gebruiken als een gewone array. Je kan bijvoorbeeld het eerste element van de eerste array opvragen op de volgende manier:
Nog een voorbeeld:
Als je hier over wil itereren met een for loop kan je dit op de volgende manier doen:
Tuples zijn een speciaal soort array waar je een vast aantal elementen kan aan toevoegen waarvan het type van bekend is.
Bijvoorbeeld als je een coordinaat op een kaart zou willen in een array steken weet je dat de x coordinaat op index 0 staat en de y coordinaat op index 1. Dit kan je aan de hand van een tuple op de volgende manier schrijven
je kan deze voor de rest op dezelfde manier als een andere array gebruiken
Je kan ook meer dan twee types meegeven in een tuple. Als we bijvoorbeeld ook de stad willen bijhouden die op die coordinaten liggen kan je dit op de volgende manier doen:
Je kan ook arrays maken van tuples. Bijvoorbeeld als we een lijst van landen willen maken
We kunnen hier ook over loopen met een for...of lus:
Modules zijn een manier om je code te organiseren in verschillende bestanden. Vaak wil je bepaalde functies beschikbaar maken voor andere bestanden. Dit kan je doen door deze functies in een module te zetten. Je kan dan in andere bestanden deze module importeren en de functies gebruiken.
Eigenlijk heb je al modules gebruikt in vorige delen in de vorm van npm packages. Deze bevatten ook modules die je kan importeren in je eigen code.
Stel dat je een functie hebt om de oppervlakte te berekenen van een cirkel, vierkant en rechthoek.
Tot nu toe heb je altijd deze functies in hetzelfde bestand gezet. Maar stel dat je deze functies ook in een ander bestand wil gebruiken. Dan kan je deze functies in een module zetten door gebruik te maken van een export
statement.
Zorg er wel voor dat je deze functies in een apart bestand zet met de extensie .ts
. In dit geval bijvoorbeeld area.ts
.
Wil je deze functies gebruiken in een ander bestand? Dan moet je deze eerst importeren aan de hand van het volgende commando.
De functies die je wil importeren zet je tussen de accolades. Het gedeelte achter from
is het pad naar het bestand waar de module in staat. In dit geval is dat ./area
omdat het bestand area.ts
in dezelfde map staat als het bestand waar je de functies wil gebruiken. Plaats je de module in een andere map, dan moet je het pad aanpassen. Staat je area.ts
bestand in de directory functions
dan moet je het volgende commando gebruiken.
nu kan je deze functies gebruiken in je code net zoals je dat zou doen alsof ze in hetzelfde bestand staan.
Heel vaak wordt er door een module maar één functie geëxporteerd. In dat geval kan je gebruik maken van een default export. Dit is een export zonder naam.
Je kan deze functie dan importeren zonder tussen de accolades te zetten.
In principe maakt het niet uit welke naam je achter de import zet want er is maar één functie geëxporteerd.
Tot nu toe hebben we altijd interfaces in hetzelfde bestand gezet als de code die deze interface gebruikt. Maar je kan ook interfaces exporteren uit een module.
We plaatsen deze interfaces vaak in een apart bestand met de naam types.ts
. We kunnen deze dan importeren in een ander bestand.
npm.js is de package manager voor JavaScript. Het is de grootste software registry ter wereld. Hier vind je heel veel packages die je kan gebruiken in je projecten. Wil je een bepaalde package zoeken dan kan je dat doen op de npmjs website. Je vind er ook uitgebreide documentatie over de packages en hoe je deze kan gebruiken.
Npm packages kunnen typisch op drie verschillende manieren geïnstalleerd worden: als dependency, dev dependency, en globaal. Een dependency wordt geïnstalleerd wanneer een pakket nodig is voor de werking van je applicatie in productie; dit zijn bijvoorbeeld bibliotheken die essentieel zijn om de applicatie te laten draaien (bv. leaflet om je interactieve map te tonen). Een dev dependency daarentegen is een pakket dat alleen nodig is tijdens de ontwikkeling, zoals tools voor testen of linting, en wordt niet meegeleverd in productie. Tot slot kunnen pakketten ook globaal geïnstalleerd worden, wat betekent dat ze overal op je systeem beschikbaar zijn, ongeacht welk project je gebruikt. Dit wordt meestal gedaan voor CLI-tools die je buiten een specifiek project wilt gebruiken, zoals TypeScript of ESLint.
Installatiemethode
Beschrijving
Installatievoorbeeld
Dependencies
Packages die nodig zijn om de applicatie in productie te laten draaien.
npm install package-name
DevDependencies
Packages die alleen nodig zijn tijdens de ontwikkeling (testing, linting, building, enz.).
npm install package-name --save-dev
Globale installatie (-g
)
Packages die globaal op je systeem worden geïnstalleerd, vaak gebruikt voor CLI-tools.
npm install package-name -g
Elk project heeft een package.json
bestand. Dit bestand bevat alle informatie over je project. Het bevat ook een lijst van alle packages die je nodig hebt voor je project. Wanneer je een package installeert met npm dan wordt deze package toegevoegd aan dit bestand in de juiste dependency
sectie.
Packages installeren in de CLI
package.json
Je kan alle dependencies installeren aan de hand van het volgende commando. Dus je moet niet elke package apart installeren.
Wanneer je een package installeert met npm dan wordt deze package geïnstalleerd in een map genaamd node_modules
. Deze map bevat alle packages die je nodig hebt voor je project. Je moet deze map niet zelf aanmaken. npm doet dit automatisch voor je.
Omdat alle dependencies opgegeven staan in het package.json
bestand en je deze ten allen tijde kan installeren aan de hand van het npm install
commando, moet je deze map ook niet toevoegen aan je git repository. Het is een goed idee om deze map toe te voegen aan je .gitignore
bestand. Voeg deze map ook nooit toe aan een zip bestand dat je doorstuurt naar iemand anders. Deze persoon kan dan zelf de dependencies installeren aan de hand van het npm install
commando.
Wanneer je een package terug zou willen verwijderen uit je node_modules folder kan je dit doen met het volgende commando:
Al de bestanden die je voordien had gedownload bij het installeren van deze package in de node_modules folder zijn nu terug verwijderd. De package is ook verwijderd uit de package.json
file.
Dit is ook de manier hoe je meestal npm packages importeert. Daar maakte het ook nooit uit welke naam je achter de import zette.
Deze functies kon je dan gebruiken door middel van de naam die je achter de import zette gevolgd door een punt.
We gaan in dit voorbeeld de chalk
package gebruiken. Deze package zorgt ervoor dat je tekst in de terminal kan kleuren. We gaan een programma maken dat de naam van de gebruiker in het rood toont.
Het eerste wat we moeten doen is de package installeren.
Opgelet we moeten hier de versie 4 installeren omdat de nieuwste versie niet werkt met ts-node
(en oudere versies van node)
Vervolgens bekijken we de documentatie van de package op de npmjs website. Hier vinden we hoe we de package kunnen gebruiken.
Dit zal de naam Jelle
in het rood tonen in de terminal.
http://definitelytyped.github.io/
Af en toe kom je in contact met een npm package die geen meegeleverde types hebben. Dit is bijvoorbeeld het geval bij de readline-sync
package. In dat geval kan je gebruik maken van de @types
(ook gekend als DefinitelyTyped) packages. Deze bevatten de types die bij de npm package horen. Je moet deze dan wel altijd apart installeren.
Een overzicht van alle @types
packages die je nodig hebt in deze cursus:
Je kan op de npmjs website heel eenvoudig zien of een bepaalde package TypeScript support heeft:
Bevat deze geen van beide? Dan heb je helemaal geen types en heb je geen voordelen van TypeScript. Je moet dan ook nog een extra aanpassing doen aan je project om deze library toch nog te gebruiken.
Bijvoorbeeld de rainbow-colors-array
package bevat geen TypeScript support en geen @types
package. Je kan deze dan toch nog gebruiken door in je project een types.d.ts
bestand aan te maken met de volgende inhoud.
Dit is ook wat je vscode je aanraad als je over de error hovered als hij de types niet vindt:
We gaan in dit voorbeeld de lodash
package gebruiken. Deze package bevat heel veel handige functies die je kan gebruiken in je projecten. Het is een soort zwitsers zakmes voor JavaScript.
We installeren deze library aan de hand van het volgende commando.
Deze library heeft geen ingebouwde types. We moeten deze dus apart installeren.
Vaak is de documentatie bedoeld voor een ouder module systeem. We moeten dan de documentatie aanpassen naar het nieuwe module systeem.
Dit moeten we aanpassen naar het nieuwe module systeem.
Vervolgens kunnen we de functies gebruiken zoals beschreven in de documentatie.
Bv de reverse
functie.
of de round
functie.
In die oefeningen zullen we nog een aantal handige functies van lodash
bekijken.
Bevat deze een tag? Dan kan je deze installeren aan de hand van de bovenstaande commando's
Bevat deze een tag, dan zitten de types al in de npm package en dan hoef je niets te doen.
Tot nu toe hebben we nog niet concreet gezien wat het grote verschil is tussen TypeScript en JavaScript. Zoals de naam zegt is de belangrijkste toevoeging van TypeScript is het toevoegen van types. Dit is een concept dat we kennen uit andere talen zoals C# en Java. Types zorgen ervoor dat je variabelen kan declareren met een bepaald type. Dit zorgt ervoor dat je code minder foutgevoelig is en dat je code leesbaarder is voor een programmeurs. Zeker als je functies gaat gebruiken van anderen is het belangrijk dat je weet welke types je moet meegeven en welke types je terugkrijgt. Dit is niet altijd even duidelijk in JavaScript. In TypeScript is dit wel duidelijk.
We gaan verder in dit onderdeel kijken welke types er allemaal zijn in TypeScript maar we gaan beginnen met een klein voorbeeldje van waarom types handig kunnen zijn. Stel dat je de volgende variabele tegenkomt in een stuk code:
Het is hier overduidelijk dat de id een getal is. Maar stel je voor dat ergens anders in je code deze variabele wordt aangepast:
en dat we ook een functie hebben die deze id gebruikt en eventueel verhoogt met 1:
Dit is een soort fout die je makkelijk kan maken en die je niet snel zal opmerken. JavaScript zal dit gewoon uitvoeren en geen foutmelding geven maar dit kan wel voor problemen zorgen. In TypeScript zou dit niet mogelijk zijn. We zouden een foutmelding krijgen bij het toekennen van de string aan de variabele id en ook bij het aanroepen van de functie increaseId. Dit is een van de vele voordelen van TypeScript. Ze noemen dit in de programmeerwereld: strongly typed. Eenmaal je een variabele een type heeft gekregen dan kan dit type nooit meer veranderd worden. De waarde mag wel veranderd worden als het type hetzelfde blijft.
of
We gaan nu in detail een aantal basis types bekijken in TypeScript en de werking ervan bespreken.
In TypeScript is er geen verschil tussen gehele getallen (integers) en floating point getallen zoals in andere programmeertalen (bv C#). Dus alle getallen of er nu getallen na de komma staan of niet worden uitgedrukt met het number
data type.
Je kan in TypeScript een number variabele declareren als volgt:
Je kan allerhande operaties uitvoeren op getallen, zoals vermenigvuldiging (*), delen (/), optellen (+), aftrekken (-), en nog veel meer
Naast de gewone getallen die jullie allemaal al kennen heb je ook "speciale numerieke waarden" die tot dit data type behoren: Infinity
, -Infinity
en NaN
Infinity stelt het wiskundige ∞ symbool voor. Het is een speciale waarde die groter is dan alle mogelijke waarden. Als je iets van wiskunde kent dan weet je dat als je deelt door 0 dat je dan eigenlijk het getal oneindig krijgt. Dit is ook zo in TypeScript
Deze value zal je waarschijnlijk niet vaak tegenkomen, maar het is een indicatie dat je per ongeluk waarschijnlijk door 0 hebt gedeeld
NaN stelt voor dat de waarde het gevolg is van een rekenfout. Bijvoorbeeld als je een stuk tekst probeert te delen door getal
Dus NaN
zal je vaak tegen komen als je iets verkeerd gedaan hebt in je berekening. Je komt NaN ook vaak tegen tijdens het converteren van een datatype naar een ander. Hoe we dit doen zien we later in detail, maar hier heb je al een voorproefje:
De reden waarvoor dat ze gekozen hebben om deze getallen uit te drukken is vrij eenvoudig. De ontwerpers van JavaScript wilden niet dat de programma's crashen als je een wiskundige fout maakt zoals bij andere programmeertalen.
Een string in TypeScript moet omringd zijn met quotes.
In TypeScript zijn er drie types van quotes
Double quotes: "Hello"
.
Single quotes: 'Hello'
.
Backticks: `Hello`
.
Double en single quotes zijn "eenvoudige" quotes. Er is geen verschil tussen de twee in TypeScript. Je mag dus kiezen om single of double quotes te gebruiken. Zorg er wel voor dat je deze keuze wel over heel je project hetzelfde houdt.
De derde soort quotes is een speciaal soort. Ze laten ons toe om variabelen en expressies in strings te plaatsen door ze te omringen door ${...}
Alles wat tussen de ${...}
staat wordt geëvalueerd en het resultaat wordt deel van de string.
Je kan eender welke variabele omzetten van een ander datatype omzetten naar een string aan de hand van de toString()
methode.
Het boolean type heeft maar twee verschillende waarden: true
en false
Het type wordt vaak gebruikt om ja/nee waarden in op te slagen. true
betekent ja of waar. En false
betekent nee of niet waar.
De bovenstaande code zou een variabele kunnen voorstellen die aangeeft of het licht aan is of niet.
Boolean waarden kunnen ook afkomstig zijn uit het resultaat van vergelijkingen:
Vergelijkingen of comparisons zien we later in detail in een verder hoofdstuk.
TypeScript is een taal die gebruik maakt van type inference. Dit wil zeggen dat TypeScript zelf zal proberen te achterhalen welk type een variabele heeft. Dit is een van de grote voordelen van TypeScript. Je moet dus niet altijd expliciet het type van een variabele aangeven. Dit is een van de grote verschillen met andere talen zoals C# en Java.
Het is wel een goed idee om altijd het type van een variabele te declareren. Dit zorgt ervoor dat je code leesbaarder is en dat je minder fouten zal maken.
De speciale null
waarde hoort niet thuis in een van de types die hierboven zijn beschreven.
Het vormt een apart type dat alleen de null
value bevat.
Het is een speciale waarde die "niets" of "leeg" voorstelt. De code hierboven zegt gewoon dat de gebruiker bijvoorbeeld geen collegeDegree
heeft. Dus bijvoorbeeld zit deze gebruiker nog in het middelbaar en heeft dus geen diploma.
Net zoals null
is undefined
een waarde die op zichzelf staat met zijn eigen type. De betekenis is zeer gelijkaardig maar toch iets anders. Het zegt dat de waarde nog niet is toegekend.
We hebben hier boven dus nog geen waarde toegekend aan de variabele message
Het any
type is een speciaal type in TypeScript. Het is een type dat je kan gebruiken als je niet weet welk type een variabele zal hebben. Het is een type dat je best zo weinig mogelijk gebruikt. Het is een type dat je kan gebruiken als je nog niet weet welk type een variabele zal hebben. Het wordt vaak gebruikt als je een externe library gebruikt die je niet zelf hebt geschreven. Je weet dan niet welke types er allemaal gebruikt worden in die library. Je kan dan het any
type gebruiken om aan te geven dat je niet weet welk type de variabele zal hebben.
Je ziet dat we de variabele notSure
eerst een getal geven, dan een string en dan een boolean. Dit is allemaal mogelijk omdat we het any
type gebruiken. Je verliest hier wel de voordelen van TypeScript. Je kan dus best zo weinig mogelijk het any
type gebruiken.
In TypeScript is unknown een type dat aangeeft dat we een variabele hebben waarvan het type niet bekend is op het moment van declaratie. Het is vergelijkbaar met het any type, maar met meer typeveiligheid. Waar any je toestaat om willekeurige bewerkingen op de variabele uit te voeren zonder controle, dwingt unknown je om het type expliciet te controleren of te specificeren voordat je er bewerkingen op uitvoert.
Een union type is een type dat bestaat uit meerdere types. Je kan een union type gebruiken als je een variabele wil declareren die meerdere types kan bevatten. Je kan een union type declareren door de types te scheiden met een |
teken. Als je wil toelaten dat een variabele een string of een getal kan bevatten dan kan je dit doen door het volgende te schrijven:
Dit wordt vaak gebruikt in combinatie met het undefined
type. Je kan dan een variabele declareren die een bepaald type of undefined
kan bevatten. Dit kan handig zijn als je een variabele wil declareren en deze nog niet meteen een waarde wil geven. Je kan dan het undefined
type gebruiken om aan te geven dat de variabele nog niet is toegewezen.
Een string union is een speciaal type in TypeScript. Het is een type dat een aantal vaste waarden kan bevatten. Je kan een string union gebruiken als je een variabele wil declareren die een van een aantal vaste waarden kan bevatten. Je kan een string union declareren door de waarden te scheiden met een |
teken.
Als je wil toelaten dat een variabele enkel de waarden "ON", "DIMMED" en "OFF" kan bevatten dan kan je dit doen door het volgende te schrijven:
Je kan ook dit soort types apart declareren als een type
. Dit is handig als je deze types op meerdere plaatsen in je code wil gebruiken.
Je moet er wel rekening mee houden als je user input gaat gebruiken dat je deze waarden eerst controleert. Je kan dit doen door een if
statement te gebruiken.
Als je bijvoorbeeld
doet zal je een error krijgen:
Je moet dus eerst controleren of de waarde die je hebt ingelezen wel een van de waarden is die je hebt gedefinieerd.
Soms kan het gebeuren dat er een fout optreedt tijdens het uitvoeren van je programma. Dit kan bijvoorbeeld gebeuren als je een bestand wil openen dat niet bestaat. In dit geval zal je programma crashen of vreemd gedrag beginnen vertonen. Bijvoorbeeld als je een functie wil schrijven voor het delen van twee getallen. Als je de functie aanroept met een deler die gelijk is aan 0, dan zal de functie een foutmelding moeten geven.
In dit geval krijg je geen foutmelding maar een vreemd resultaat, je krijgt Infinity
als resultaat. Dit is niet wat we willen. We willen dat de functie een foutmelding geeft als de deler gelijk is aan 0. Dit kunnen we doen aan de hand van een if
statement en de throw
statement.
Let nu wel op dat je de functie niet meer kan gebruiken zoals hieronder. Als je de functie aanroept met een deler die gelijk is aan 0 dan zal de functie een foutmelding geven en zal je programma stoppen met uitvoeren (en dus "crashen").
Willen we nu deze foutmelding opvangen en zelf een foutmelding geven aan de gebruiker dan kunnen we dit doen aan de hand van een try catch
statement. We kunnen de functie aanroepen in een try
block en de foutmelding opvangen in een catch
block. In het catch
block kunnen we dan zelf een foutmelding geven aan de gebruiker.
Natuurlijk is dit een beetje een nutteloos voorbeeld, want je weet op voorhand dat deze functie een fout zal geven omdat je zelf als programmeur de deler op 0 hebt gezet. Maar als je de twee getallen afhankelijk maakt van de input van een gebruiker dan kan je niet op voorhand weten of de deler gelijk zal zijn aan 0. In dat geval kan je de foutmelding opvangen en zelf een foutmelding geven aan de gebruiker en opnieuw vragen naar de input.
Je kan ook een finally
block toevoegen aan je try catch
statement. Dit block zal altijd uitgevoerd worden, ook als er geen foutmelding is opgetreden. Dit kan handig zijn als je bijvoorbeeld een bestand hebt geopend en je wil dit bestand altijd sluiten, ook als er een foutmelding is opgetreden. Dus bijvoorbeeld:
Met het bovenstaande voorbeeld krijg je de volgende output:
Als je een foutmelding opvangt in een catch
block dan kan je deze foutmelding gebruiken in je code. Tot nu toe hebben we altijd een nieuw Error object aangemaakt en zelf een foutmelding gegeven. Maar je kan ook de foutmelding die je opvangt gebruiken. Dit is een object met een aantal properties die je kan gebruiken. Zo heeft dit object een message
property die de foutmelding bevat. Je kan deze property gebruiken om de foutmelding te tonen aan de gebruiker. Let er op dat je hier het type any moet gebruiken omdat je niet weet welk type de foutmelding zal hebben.
Dit zal de volgende output geven:
In principe is het ook mogelijk om een foutmelding op te gooien zonder een Error object te gebruiken. Je kan bijvoorbeeld een string opgooien als foutmelding.
Dan kan je uiteraard wel niet meer gebruik maken van de message
property van het Error object.
Functies in TypeScript worden gedeclareerd met de function
keyword. De parameters van een functie worden gedeclareerd met hun naam en type. De return type van een functie wordt gedeclareerd na de parameters.
Dit is een functie die twee getallen optelt. De functie verwacht twee parameters van het type number
en geeft een number
terug.
Als je een functie hebt die geen return type heeft, dan kan je void
gebruiken. void
betekent dat de functie niets teruggeeft.
Je kan ook de void
keyword weglaten. Dit is hetzelfde als void
.
Let er op dat je de types van de parameters altijd moet declareren. Als je dit niet doet, dan zal TypeScript een foutmelding geven.
Stel dat we een multiply functie willen aanpassen dat ze ook toestaat om maar 1 argument mee te geven. Als we gewoon de 2de argument zouden weglaten krijgen we een foutmelding. Logisch ook want hij kan helemaal geen vermenigvuldiging doen met 1 getal.
Willen we dit toch toestaan dan kunnen we de 2de parameter optioneel maken. Dit doen we door een ?
te plaatsen na de naam van de parameter.
Uiteraard krijgen we nu een foutmelding. Dit komt omdat we de parameter b
niet altijd meegeven. Als we de functie aanroepen met 1 argument dan zal b
undefined
zijn. We kunnen dit oplossen door een check te doen of b
undefined
is.
Je kan ook een default waarde meegeven aan een parameter. Dit doe je door de waarde na de declaratie van de parameter te plaatsen.
Nu geeft de functie 5 terug als je maar 1 argument meegeeft. Dit komt omdat b
nu een default waarde heeft van 1.
Soms wil je een functie schrijven die een onbepaald aantal parameters kan aannemen. Dit kan je doen door een rest parameter te gebruiken. Dit is een parameter die je voorafgaat met ...
. Je kan de rest parameter een naam geven. Dit is de naam van de array waarin alle parameters worden opgeslagen.
Je hebt naast de function
keyword ook nog arrow functions. Dit zijn functies die je kan declareren met de pijl operator =>
. Je kan een arrow function gebruiken als je een functie wil declareren die je als parameter wil meegeven aan een andere functie (een callback functie). Dit is een veel voorkomend patroon in JavaScript en TypeScript.
Stel dat je de volgende functie hebt:
kan je die ook schrijven als een arrow function:
Merk op dat als we de functie willen kunnen aanroepen zoals hierboven, we de functie moeten declareren als een variabele. Op die manier kunnen we de functie aanroepen door de variabele aan te roepen met de nodige parameters.
De concepten van optionele parameters, default parameters en rest parameters zijn ook van toepassing op arrow functions.
Zoals we hierboven al vermeld hebben worden arrow functions vaak gebruikt als callback functies. Dit zijn functies die je als parameter meegeeft aan een andere functie. De functie die je meegeeft wordt dan uitgevoerd op een bepaald moment in de functie waar je de callback functie meegeeft.
Een ideaal voorbeeld hiervan is de forEach
functie op een array. Deze functie zal een callback functie uitvoeren voor elk element in de array.
Merk op dat we hier een arrow function meegeven aan de forEach
functie. Deze arrow function zal uitgevoerd worden voor elk element in de array. De waarde van het element wordt meegegeven als parameter aan de arrow function. Als we een arrow function meegeven zonder deze een naam te geven, dan noemen we die ook vaak een anonieme functie.
Stel je voor dat we zelf een functie willen schrijven die een array doorloopt en een callback functie uitvoert voor elk element in de array. We kunnen dit doen door een functie te schrijven die een array en een callback functie verwacht.
De functie hierboven heeft twee parameters: array`` en een
callbackfunctie. Je kan die op een heel gelijkaardige manier gebruiken zoals de ingebouwde
forEach` functie.
Maar let op! De callback parameter bevat nog altijd het any
type. Dit is niet wat we willen. We willen dat de callback functie een number
verwacht als parameter. Dit kunnen we doen door de callback parameter te declareren met het juiste type. Hiervoor moeten we een interface maken die de callback functie beschrijft.
We kunnen nu de callback parameter declareren met het type Callback
.
Geef je nu een callback functie mee die een parameter verwacht van een ander type dan number
, dan zal TypeScript een foutmelding geven.
Hier ook nog een voorbeeld van een callback functie die een return type heeft en meer dan 1 parameter verwacht.
We kunnen nu de calculate
functie gebruiken om een berekening uit te voeren met een callback functie. De berekening zelf wordt bepaald door de callback functie.
Als we nu de calculate functie willen uitvoeren maar we willen een vermenigvuldiging doen in plaats van een optelling, dan kunnen we een andere callback functie meegeven.
Wanneer je maar 1 lijn code hebt staan in jouw functie, kan je jouw schrijfwijze verkorten:
Wanneer jouw lijn code maar 1 statement uitvoert, mag je de accolades weglaten:
Wanneer jouw lijn code een return doet, hoef je zelfs return niet meer te vermelden:
Wanneer je maar 1 parameter hebt, kan je zelfs de haakjes rond de parameter weglaten:
De verkorte syntax is vooral handig bij het gebruik van callback functies. Je kan dan de callback functie meegeven zonder de haakjes en accolades. We grijpen terug naar het voorbeeld van de forEach
functie.
Je hebt al een aantal array methodes gezien zoals forEach
en hoe je deze gebruikt met een callback functie. Er zijn nog een aantal andere handige array methodes die je kan gebruiken.
De map
methode zal een nieuwe array teruggeven waarbij elk element van de originele array is vervangen door het resultaat van de callback functie.
Het type van element
is hetzelfde als het type van een element in de array. In dit geval is element
van het type number
. Je kan dit ook expliciet aangeven.
De filter
methode zal een nieuwe array teruggeven waarbij alleen de elementen van de originele array worden behouden waarvoor de callback functie true
teruggeeft.
Ook hier kan je het type van element
expliciet aangeven.
De reduce
methode zal een enkele waarde teruggeven die het resultaat is van de callback functie. De callback functie verwacht 2 parameters: de accumulator en het huidige element. De accumulator is de waarde die wordt teruggegeven door de vorige uitvoering van de callback functie. Het huidige element is het huidige element van de array.
Het type dat de callback functie teruggeeft is hetzelfde als het type van de accumulator. In dit geval is dat number
. Je kan dit ook expliciet aangeven.
Het type van de accumulator is hetzelfde als het type van de initiele waarde die je meegeeft aan de reduce
methode. Als je geen initiele waarde meegeeft, dan zal de accumulator hetzelfde type hebben als het eerste element van de array.
Soms is het zelfs nodig om het type van de accumulator expliciet aan te geven omdat het type niet kan worden afgeleid.
Daarom is het altijd een goed idee om de types van de parameters van de callback functie expliciet aan te geven in een reduce
methode.
De find
methode zal het eerste element van de array teruggeven waarvoor de callback functie true
teruggeeft.
Het type van firstEven
is number | undefined
. Dit komt omdat de find
methode undefined
zal teruggeven als er geen element is gevonden waarvoor de callback functie true
teruggeeft. Zelfs als je zeker weet dat er altijd een element zal zijn dat voldoet aan de voorwaarde, moet je nog steeds undefined
in overweging nemen.
Er bestaan een aantal datatypes in TypeScript die we "primitieve" of "eenvoudige" datatypes noemen. Dit is omdat de waarden altijd maar uit 1 enkel ding bestaat. In het hoofdstuk over arrays heb je gezien dat er ook nog een ander soort datatypes bestaat: de complexe datatypes. Deze worden opgebouwd uit meerdere primitieve datatypes.
Een object is een ander voorbeeld van een complex datatype. In JavaScript en TypeScript kom je objecten haast overal tegen. Daarom is het belangrijk om deze te begrijpen en deze te kunnen gebruiken.
We kunnen een object beschrijven aan de hand van een interface . Deze interface beschrijft welke properties een object bevat en kan bevatten. We maken een eigen soort type dat we later kunnen gebruiken bij het declareren van onze variabelen.
Stel dat je het volgende object in JavaScript hebt:
Dit object heeft twee properties: name
en age
. De waarde van name
is een string en de waarde van age
is een number. Als we nu een tweede object willen aanmaken en perongeluk een typfout maken, dan zal TypeScript ons niet waarschuwen van deze fout:
TypeScript zal dit als twee verschillende objecten zien. Dit is niet wat we willen. We willen dat TypeScript ons waarschuwt als we een typfout maken. We willen dus een type declareren dat zegt dat een object een name
en een age
property moet hebben. Dit kunnen we doen aan de hand van een interface:
We hebben nu een interface gemaakt die we Person
noemen. Deze interface beschrijft een object dat een name
en een age
property moet hebben. We kunnen nu een variabele declareren van het type Person
:
Als we nu een typfout maken, dan zal TypeScript ons waarschuwen:
Ook is het niet mogelijk om een property toe te voegen die niet in de interface staat:
Ook het weglaten van bepaalde properties zal een foutmelding geven:
Uiteraard moet je ook de data types van de properties respecteren:
Het is ook mogelijk om objecten in andere objecten te gaan steken. Bijvoorbeeld voor ons User object zouden we kunnen kiezen om ook een adres toe te voegen. We zouden deze als aparte eigenschappen kunnen opgeven van het user object maar het is beter om dit in een apart object te steken.
We passen dus de User interface hiervoor aan:
Het type Address
moeten we dan ook nog aanmaken aan de hand van een nieuwe interface.
Nu kunnen we een User object aanmaken gebruik makende van deze interface.
Wil je dan bijvoorbeeld de straat van deze gebruiker op het scherm tonen dan kan je dit doen aan de hand van de dot notatie:
Als we address
zouden niet verplicht maken (optioneel):
dan moet je wel eerst nakijken of address
wel is opgegeven:
anders krijg je deze error:
Een record type is een object waarvan we de properties niet kennen. We weten niet welke properties het object zal hebben. We kunnen dit aangeven aan de hand van de Record
type. Dit type heeft twee type parameters: het eerste type parameter is het type van de keys en het tweede type parameter is het type van de values.
Stel dat we een object willen gebruiken om bij te houden hoeveel keer een bepaalde waarde voorkomt in een array. We kunnen onmogelijk op voorhand weten welke waarden er in de array zullen zitten dus we kunnen niet op voorhand zeggen welke properties het object zal hebben. We kunnen dit oplossen aan de hand van de Record
type:
Je kan aan de hand van het import
statement in TypeScript een JSON bestand inlezen. Dit is handig als je bijvoorbeeld een configuratie bestand hebt dat je wil inlezen of dat je een lijst van objecten wil inlezen vanuit een bestand.
Stel dat je een JSON bestand users.json
hebt met de volgende inhoud:
Dan kan je dit bestand inlezen aan de hand van het import
statement:
Je moet hier wel op letten dat je in je tsconfig.json
bestand de volgende optie hebt aangezet:
Je moet dan nog wel de inhoud van usersJson
in een variabele of constante steken:
Het is niet altijd mogelijk om een bestand in te lezen aan de hand van het import
statement. Als je bijvoorbeeld een bestand wil inlezen dat niet in je project zit en ergens anders op je computer staat dan kan je dit niet inlezen aan de hand van het import
statement. In dat geval kan je gebruik maken van de fs
module.
De optionele chaining operator is een nieuwe operator die sinds TypeScript 3.7 beschikbaar is. Deze operator is zeer handig als je objecten in objecten hebt. Stel dat je een object hebt met een aantal properties en je wil een property van een property opvragen. Als je niet zeker bent of de property wel bestaat dan kan je de optionele chaining operator gebruiken.
We hebben hierboven gezien dat we dit kunnen voorkomen door eerst na te kijken of de property wel bestaat aan de hand van een if statement. Dit is echter niet zo handig. Je kan dit nu oplossen aan de hand van de optionele chaining operator:
Als address nu undefined is dan zal de optionele chaining operator undefined teruggeven. Als address wel bestaat dan zal de optionele chaining operator de waarde van street teruggeven.
Zo kunnen we heel diepe objecten gaan opvragen zonder dat we eerst moeten nakijken of de properties wel bestaan:
In TypeScript wordt de ! (non-null assertion) operator gebruikt om aan te geven dat een waarde zeker niet null of undefined zal zijn op het punt waar de operator wordt gebruikt, zelfs als de typechecker dat niet kan garanderen. Dit kan handig zijn in situaties waar je meer weet over de waarde dan TypeScript kan afleiden.
Stel dat je een functie hebt die een default user aanmaakt en returned:
Als we deze functie nu gebruiken en we willen de straat van de default user opvragen dan krijgen we een foutmelding:
Nochtans weten we met zekerheid dat de default user een adres heeft. We kunnen dit aangeven aan de hand van de ! operator:
Let op dat je deze operator niet begint te gebruiken om fouten te verbergen. Je moet er zeker van zijn dat de waarde niet undefined of null kan zijn. Anders zal je een foutmelding krijgen op runtime. Gebruik hem dus niet om de compiler de mond te snoeren.
Net zoals in de browser is het mogelijk om in Node.js een HTTP request te doen naar een andere server. Dit doe je met de fetch
functie. Deze functie heeft als argument een URL. De functie geeft een Promise terug die een Response object bevat. Dit object bevat de data die je terugkrijgt van de server. In TypeScript is het wel belangrijk dat je het type van de data opgeeft die je verwacht terug te krijgen. Je moet dus een interface voorzien die de data beschrijft.
De syntax is grotendeels hetzelfde als in JavaScript. Het enige verschil is dat je het type van de data moet opgeven.
We gaan in dit voorbeeld gebruik maken van de JSONPlaceholder API. Deze API bevat een aantal endpoints die je kan gebruiken om data op te halen. We gaan in dit voorbeeld gebruik maken van de /posts
endpoint. Deze endpoint geeft een lijst van gebruikers terug.
Het eerste wat je moet doen is een interface maken die de data beschrijft die je verwacht terug te krijgen. In dit geval is dit een array van objecten. Elk object heeft een userId, id, title en body property. De userId en id property zijn van het type number. De title en body property zijn van het type string.
Je kan deze interface zelf maken, maar je kan ook gebruik maken van een tool zoals QuickType om deze automatisch te genereren. Zorg vooral dat de interface correct is en overeenkomt met de data die je verwacht terug te krijgen.
Nu kunnen we de fetch functie gebruiken om de data op te halen. We geven als argument de URL van de endpoint mee. Omdat de fetch functie een Promise teruggeeft, kunnen we de then functie gebruiken om de data te gebruiken. Omdat over het algemeen de data die je terugkrijgt van een server een JSON object is, moeten we de data eerst omzetten naar een JavaScript object. Dit doen we met de json
functie. Deze functie geeft ook een Promise terug. We kunnen dus de then functie gebruiken om de data te gebruiken.
Stel dat we de titel van de eerste post willen loggen naar de console. We kunnen dit doen met de volgende code:
of met async en await:
Let op dat een API niet altijd een array teruggeeft. Het kan ook een object zijn dat op zijn beurt weer een array bevat. Je moet dus altijd controleren wat de data is die je terugkrijgt.
De catch functie is nodig om een error af te handelen. Onder een errors vallen alleen errors die veroorzaakt worden op netwerk niveau. Dus bijvoorbeeld als de server niet bereikbaar is of als de URL niet bestaat.
Als je toch een error wil afhandelen die veroorzaakt wordt door een fout in de code van de server, dan moet je de status code van de response controleren. Als de status code 2xx is, dan is er geen error. Als de status code iets anders is, dan is er een error.
We kijken hier na of de status code niet 2xx is aan de hand van de ok
property. Deze property is true
als de status code 2xx is. Als de status code niet 2xx is, dan gooien we een error.
Deze code is ook weer sterk te vereenvoudigen met async en await:
Je kan ook de status
property gebruiken om de status code op te vragen. Deze property bevat een nummer.
We gaan nu een iets complexer voorbeeld bekijken. We gaan deze keer eens de https://reqres.in/api/users
API gebruiken. Als je naar de response kijkt, dan zie je dat deze geen array teruggeeft, maar een object. Dit object bevat een array met de key data
en ook een andere properties zoals page
, per_page
, total
en total_pages
. Het data object bevat een array van gebruikers.
Hier kan je de volgende interface voor gebruiken:
We gaan nu de gebruikers ophalen en de gebruikers te loggen naar de console.
Merk op dat we hier wel geen rekening hebben gehouden met de pagina's. Als je naar de response kijkt zie je dat hier een page
property in zit. Deze property geeft aan op welke pagina je zit. Als je naar de URL kijkt, dan zie je dat er een page
query parameter in zit. Deze parameter geeft aan op welke pagina je zit. Als je deze parameter aanpast, dan krijg je een andere pagina terug. Je zou hier dus een loop kunnen maken die alle pagina's afgaat.
Let wel op dat elk paging systeem anders is. Je moet dus altijd controleren hoe het paging systeem werkt.
Express laat ons toe statische bestanden te serveren. Dit zijn bestanden die niet veranderen. Denk aan CSS, JavaScript, afbeeldingen, etc.
Wil je dit doen, dan moet je een folder aanmaken waarin je deze bestanden plaatst. Deze folder noemen we meestal public
. In deze folder plaatsen we dan onze statische bestanden.
Het enige wat we dan nog moeten doen is Express vertellen dat deze folder statische bestanden bevat. Dit doen we met de use
methode.
Alle bestanden in de public folder kunnen nu worden opgevraagd. Als je een bestand style.css
in de public folder plaatst, kan je dit bestand opvragen via http://localhost:3000/style.css
.
Vaak plaats je de bestanden niet in de public folder zelf, maar in subfolders. Dit kan er als volgt uit zien:
Wil je nu een afbeelding opvragen, dan kan je dit doen via http://localhost:3000/assets/images/image-01.jpg
. Wil je een script opvragen, dan kan je dit doen via http://localhost:3000/js/script.js
.
Dankzij Express kunnen we nu dynamisch HTML terugsturen naar de client. Bekijk eventjes dit voorbeeld:
Bezoek http://localhost:3000
en merk op wat er gebeurt.
Bij elke refresh verandert de waarde van het random getal. Kijk naar de source code van deze pagina. Je ziet enkel een getal, geen scripts! Express stuurt een nieuwe inhoud van de pagina bij elke refresh! Laten we het voorbeeld even analyseren:
Deze lijn geeft een willekeurig getal terug. We gebruiken Math.random dat een random getal geeft tussen 0 en 1 en vermenigvuldigen dat met 100.
In plaats van een vaste string, geven we nu het randomgetal mee. Elke refresh voert de callback in app.get uit, dus elke refresh zorgt voor een ander getal.
Volledige web paginas in variabelen steken is niet ideaal. Wanneer je weet dat ook nog CSS en scripts erbij moeten, dan is het duidelijk dat we een andere oplossing nodig hebben. Express laat toe templates te gebruiken.
We kunnen bv een Hello World pagina maken:
Maar wat als we nu een willekeurige boodschap willen tonen?
Templates laten ons toe HTML paginas te schrijven zoals we dat gewoon zijn maar met variabele inhoud. Express ondersteunt verschillende template "engines". Hier gaan we gebruik maken van EJS.
Om EJS (Embedded JavaScript templating) te gebruiken installeren we de ejs module:
en we installeren ook de TypeScript types:
We stellen onze express app in om EJS als default view engine te gebruiken:
Net zoals we "port" de waarde 3000 geven, zetten we de property "view engine" op ejs.
EJS bestanden lijken op HTML files maar bevatten wat extras. Laten we starten met een simpel EJS bestand.
Dit bestand bewaren we als index.ejs in de folder /views. Merk op, dit is HTML in een EJS bestand, niets speciaals.
Alle EJS files moeten de extensie .ejs hebben.
Alle EJS files moeten in de views folder staan (die zich in de folder van jouw applicatie bevindt)
Nu passen we onze applicatie aan om de index.ejs te tonen (renderen: omzetten van EJS naar HTML):
Merk op hoe eenvoudig onze route naar / is geworden:
Ipv res.send
gebruiken we res.render
. Render verwacht als parameter de naam van een template die zich in de views folder bevindt. Hier tonen we de index file. Ga naar localhost:3000/ om jouw EJS file als HTML te zien.
Je kan nu verschillende EJS files toevoegen. Render ze via verschillende routes en kijk hoe je nu volledige control hebt over routes en de html die getoond wordt.
Templates helpen ons de HTML dynamisch te maken. Laten we ons voorbeeld aanpassen zodat we het willekeurig getal weer zien verschijnen. Eerst passen we onze TypeScript aan.
res.render()
heeft ook een tweede optionele parameter: een object waar elke property een variabel is die beschikbaar zal zijn in de EJS file.
In dit voorbeeld heeft de tweede parameter maar 1 property: aRandomNumber
. We geven dit de waarde van de variabele randomGetal
. aRandomNumber
zal dus bij elke refresh een willekeurig getal tussen 0 en 100 bevatten.
We kunnen ook meerdere properties meegeven:
Express zal nu index tonen, maar geeft eerst deze lijst van properties mee. Deze properties zijn nu beschikbaar als variabelen in de EJS file!
Laten we de index.ejs file aanpassen:
Wanneer je nu naar localhost:3000 gaat, zal je het random getal in de tekst zien staan.
Om een variabele te tonen die werd meegegeven in de render functie, gebruiken we volgende notatie:
Het is perfect mogelijk om ook properties van een object te tonen:
Als je een array hebt, kan je ook een element tonen:
EJS laat ons toe JavaScript te gebruiken om meer controle te hebben over de dynamische inhoud van het template. Tussen <% %> kunnen we JavaScript plaatsen:
In EJS bestanden kan je geen TypeScript types gebruiken. Let hier zeker op dat je enkel aan de controller kant TypeScript kan gebruiken.
Een if statement kan ook toegevoegd worden:
Let hier op dat de if statement en het afsluiten van het }
teken tussen <% %>
moet staan.
Er bestaat ook een mogelijkheid om een loop toe te voegen. We hebben al gezien dat we <% %> gebruiken om JavaScript uit te voeren. Stel je voor dat 10 keer "Hallo" getoond moet worden. We kunnen dit doen met een for loop:
Het is belangrijk om te weten dat alle javascript code tussen <% %>
moet staan en dus ook de for statement en het afsluiten van het }
teken.
Wil je de waarde van i
tonen, dan moet je <%= i %>
gebruiken.
Laten we een voorbeeld bekijken waar we een lijst van mensen tonen. We hebben een array van mensen en we willen de naam van elke persoon tonen.
In de index.ejs file tonen we nu de lijst van mensen:
Laten we een voorbeeld bekijken waar we een tabel tonen. We hebben een array van mensen. Deze keer worden deze mensen voorgesteld als objecten. We willen de naam, stad en leeftijd van elke persoon tonen.
In de index.ejs file tonen we nu de tabel van mensen:
Het is ook mogelijk om een if statement toe te voegen. Stel dat we enkel personen willen tonen die ouder zijn dan 30:
Je kan ook andere EJS files includen in een EJS file. Dit is handig wanneer je bv een header en footer hebt die je in elke pagina wil tonen. Stel je voor dat we de volgende pagina hebben:
In principe wil je op elke pagina dezelfde header en footer tonen. We kunnen de header en footer in aparte EJS files steken en deze includen in de index.ejs file. Deze EJS files bewaren we meestal in een directory genaamd partials in de view directory.
We maken dus een header.ejs file:
En een footer.ejs file:
nu kunnen we in elke EJS file de header en footer includen:
Alle variabelen die doorgegeven worden met de render functie zijn ook beschikbaar in de included files.
Het Request object is een object dat door Express wordt aangemaakt en wordt meegegeven aan de callback functie van een route. Het bevat informatie over de request die de client verstuurd heeft. Je kan het gebruiken om bv. de inhoud van een POST
request te lezen, de headers te lezen, de parameters van een route te lezen, etc.
Tot nu toe heb je dit object al gebruikt voor het uitlezen van query en route parameters. Enkele properties van het Request object zijn:
req.body
: bevat de inhoud van een POST
request
req.headers
: bevat de headers van de request
req.params
: bevat de parameters van een route
req.query
: bevat de query parameters van een request
req.path
: bevat het pad van de request
req.method
: bevat de HTTP method van de request (GET, POST, PUT, DELETE, etc.)
req.ip
: bevat het IP adres van de client
Request headers zijn een belangrijk onderdeel van een HTTP request. Ze bevatten informatie over de request zelf. Ze worden meegestuurd door de client en kunnen door de server gelezen worden. Headers bevatten bv. informatie over de browser die de request verstuurd, de taal van de client, de versie van de HTTP protocol, etc. Een header bestaat uit een naam en een waarde.
Zo kan je bijvoorbeeld aan de hand van de header User-Agent
de browser van de client bepalen. De waarde van deze header is bv. Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
.
Headers worden meegestuurd in de request. Je kan ze lezen via het Request object:
Wil je een specifieke header lezen, gebruik dan de property req.headers
:
Express is een populair Web Application Framework gebaseerd op Node.js waarmee we web applicaties en APIs kunnen bouwen.
Aan de hand van Express kunnen we onze applicaties als een full TypeScript (of JavaScript) stack schrijven, m.a.w. onze code op de backend en de code in de client is allemaal TypeScript.
Dit type frameworks laat ons ook toe de client pagina"s dynamisch te genereren. Dit wil zeggen dat de finale HTML/CSS/TS pagina niet bestaat tot een gebruiker de server raadpleegt. Express zal deze pagina aanmaken met alle nodige data (bv. data uit de database).
Fictief voorbeeld: De gebruiker gaat naar http://webontwikkeling.ap/. Express herkent dat de gebruiker de homepagina wil (omdat er na de laatste slash niets volgt). De ontwikkelaar heeft gezorgd dat Express hier een mooie hoofdpagina toont met de laatste 3 nieuwtjes (die uit een database komen) over Webontwikkeling. Onderdelen van deze pagina bestonden (bv. de CSS, wat HTML en een stukje client script) maar Express vult de ontbrekende data in en stuurt mooie volledige HTML/CSS/TS naar de client.
We starten met het installeren van Express.
In deze module gebruiken we TypeScript dus we moeten ook de types installeren
De Hello World applicatie. We lichten hieronder toe hoe deze werkt:
Sla dit bestand op als server.ts
en start het op met ts-node server.ts
. Je kan nu naar http://localhost:3000
surfen en je ziet de tekst Hello World
verschijnen.
Laten we elke lijn bekijken
We importeren de express module en de nodige types en voeren express()
uit. Het resultaat hiervan is een Express
object dat de Express applicatie voorstelt.
We kunnen dit object gebruiken om een aantal settings aan te passen, te beslissen welke url welke data (HTML, JSON, etc.) laadt en de applicatie te starten.
app.set
laat ons toe bepaalde properties van onze Express applicatie aan te passen. Het is belangrijk om te definieren op welke poort onze webapplicatie gaat luisteren. Een typische keuze tijdens ontwikkeling is de poort 3000. Dit wil zeggen dat we lokaal naar http://localhost:3000
zullen moeten gaan.
Laten we nu eerst de laatste lijnen bekijken:
app.listen
zorgt dat onze web applicatie start met luisteren op de meegegeven poort. Let op, we gebruiken in de eerste parameter app.get om de property die we daarnet een waarde hadden gegeven op te roepen. De tweede parameter is een callback die wordt uitgevoerd wanneer het luisteren succesvol is opgestart. Hier printen we gewoon de url en port uit op de console ter info.
Web applicaties en websites bevatten meestal meer dan 1 pagina. Tot nu toe zie je het path in een URL als een locatie op een webserver. Bv. localhost:3000/index.html
zou de file index.html
zijn die zich bevindt in de root folder. localhost:3000/profile/addPicture.html
zou een file addPicture.html
zijn die zich in /profile/
bevindt.
Express laat ons toe zelf te bepalen welke data wordt teruggestuurd aan de hand van de URL. In ons voorbeeld zal localhost:3000/
de tekst "Hello World" tonen:
Express werkt met routes. Een route bepaalt welk path welke data terugstuurt. Je doet dit aan de hand van de methode app.get
. app.get
heeft 2 parameters:
het pad
een functie die de data terugstuurt naar de client
Het pad hier is "/". Dit komt overeen met localhost:3000/
, omdat het relatief is ten opzichte van het adres van de server. Vervang je dit door bv "/helloworld", dan zal je naar localhost:3000/helloworld
moeten gaan. localhost:3000 zal niet meer werken.
De functie als tweede parameter heeft zelf 2 parameters:
het request object. Dit object heeft informatie over de request die de client heeft gedaan. Denk bv. aan de headers, de body, de query parameters, etc. Het is een object van het type Request.
het response object. Het request object bevat informatie over de vraag van de client (bv. data die de client stuurt, meer hierover later).
Het response object laat ons toe een response te sturen naar de client. We gebruiken hier 2 functies:
type()
laat ons toe het Content-type van de response te bepalen. Een volledige lijst van types vind je hier. De belangrijkste die we gaan gebruiken zijn "text/html"
en "application/json"
.
Om data terug te sturen gebruiken we de send()
functie. Omdat we HTML willen terugsturen, vullen we de parameter met een string in HTML formaat.
Verschillende routes toevoegen is makkelijk:
Voor elke route roepen we app.get
op met het path dat we willen instellen en de data die we willen terugsturen.
We kunnen ook verkeerde paths opvangen. Als de gebruiker naar een path gaat dat niet bestaat, kunnen we ze bv. een foutmelding geven.
Merk op dat we een nieuwe lijn hebben toegevoegd:
Dit geeft een custom status code terug. 404 laat jouw browser weten dat de pagina niet gevonden werd. Standaard zal 200 worden teruggegeven, als alles ok is (en als je dus zelf geen status meegeeft).
Opgelet: De volgorde is hier belangrijk. Zet je deze app.use bovenaan in de applicatie, dan zal geen enkele route meer werken. Plaats die onderaan (maar boven app.listen).
Tot nu toe stuurden we html terug. Maar we kunnen ook data terugsturen. Dit verandert onze web applicatie in een echte API.
We hebben een variabele data aangemaakt die een array van objecten bevat. Om deze data te sturen gebruiken we een ander type: application/json.
Dit zorgt ervoor dat de client weet dat het om json gaat. De Content-type in de header zal dan ook "application/json" zijn.
Om de JSON data terug te sturen, gebruiken we res.json.
Eigenlijk moeten we het type hier niet definieren. res.json zal zelf de content-type van de header aanpassen naar "application/json".
We kunnen ook async routes maken. Dit is handig als we bv. data moeten ophalen uit een API.
Deze route zal dus eerst de data ophalen van de API en pas daarna terugsturen. De response zal dus niet onmiddellijk terugkomen. De snelheid van de API zal dus ook de snelheid van onze applicatie bepalen.
Wil je niet afhankelijk zijn van de snelheid van een externe API, dan kan je ook bij het opstarten van de server de data ophalen en opslaan in een variabele. Deze variabele kan je dan gebruiken in je routes. Vaak word dit gedaan in de app.listen
functie. Deze moet dan ook async zijn.
Nodemon is een npm package die het ontwikkelen van een express applicatie makkelijker maakt. Het zorgt ervoor dat de server automatisch herstart wordt wanneer er een bestand veranderd wordt. Dit is handig omdat je dan niet telkens de server handmatig moet herstarten.
Als je nodemon wil gebruiken in je project kan je dit installeren met het volgende commando:
Vervolgens kan je in je package.json
file een script toevoegen dat nodemon gebruikt. Dit kan gedaan worden door het volgende toe te voegen aan je package.json
file:
In dit voorbeeld wordt ervan uitgegaan dat de server file index.js
heet. Vervang dit door de naam van jouw server file. Vervolgens kan je nodemon starten door het volgende commando uit te voeren in de terminal:
Het voordeel hierbij is dat de andere developers in je team niet nodemon moeten installeren. Wanneer zij het project clonen en npm install
uitvoeren, zal nodemon automatisch geïnstalleerd worden en kan het gebruikt worden door npm start
uit te voeren.
Nodemon kan geïnstalleerd worden via npm. Dit kan gedaan worden door het volgende commando uit te voeren. Dit installeert nodemon globaal op je systeem en moet dus maar één keer uitgevoerd worden.
Nadat nodemon geïnstalleerd is kan het gebruikt worden door het volgende commando uit te voeren in de terminal:
In dit voorbeeld wordt ervan uitgegaan dat de server file index.js
heet. Vervang dit door de naam van jouw server file. Hij zal zelf de server starten en herstarten wanneer nodig. Je hoeft ook niet meer het ts-node
commando te gebruiken, nodemon zal dit zelf doen.
Nodemon kan ook gebruikt worden zonder het te installeren. Dit kan gedaan worden door het volgende commando uit te voeren in de terminal:
npx is een package runner die standaard bij npm geleverd wordt. Het zal nodemon downloaden en uitvoeren. Het voordeel hiervan is dat nodemon niet geïnstalleerd moet worden op je systeem. Het nadeel is dat het telkens opnieuw gedownload moet worden wanneer je het commando uitvoert.
Wanneer een gebruiker naar het domein van onze website surft, stuurt zijn browser een GET
-request naar de route /
van onze applicatie.
Die kunnen we bijvoorbeeld zo afhandelen:
De gebruiker vraagt bijvoorbeeld naar localhost:3030
en krijgt zo de tekst "hallo" te zien.
Wat als we de gebruiker wat meer controle willen geven over de request?
GET
-requests zijn de "default". Express-applicaties bevatten vaak calls van de vorm app.get
en deze dienen dus om aan te geven hoe een GET
-request moet worden afgehandeld. Met andere woorden: wat moet gebeuren wanneer de gebruiker naar een bepaalde pagina surft. Om meer data mee te geven kunnen we gebruik maken van query strings, bijvoorbeeld:
Dit is een GET
request naar Google. De domeinnaam is google.be
. Het pad is /search
. Alles achter ?
is de query string: q=ap&client=safari
De query string bepaalt de velden en waarden die we naar de server willen sturen. In dit geval sturen we q met de waarde "ap" (de zoekterm) en client met de waarde "safari" (de gebruike web browser).
Om de waarden in een query string te raadplegen, maken we gebruik de property query
van het request object.
Het request object is de eerste parameter in de callback functie van app.get
. Dit object bevat informatie van de request die de gebruiker/browser stuurt.
Veronderstel dat we een array met namen hebben. We willen een naam opzoeken door een index mee te geven.
req
is het Request object. De property query
bevat alle query velden die meegestuurd worden.
Probeer zelf eens een lijst van query velden toe te voegen aan de URL en print de inhoud van req.query
naar de console.
Zoals al vermeld hebben gebruikt Google de query string om zoektermen mee te geven. We kunnen dit ook doen in onze eigen applicatie. Stel dat we een zoekfunctie willen maken die de gebruiker toelaat om een naam op te zoeken in een array van namen. We kunnen dit doen met een formulier in ons ejs bestand:
We gebruiken hier de query string q
om de zoekterm mee te geven. De gebruiker kan een zoekterm invullen in het input veld en op de knop drukken. De browser zal een GET
request sturen naar /search?q=zoekterm
. We kunnen dit request afhandelen in onze Express applicatie:
We gebruiken hier de filter
methode van een array om enkel de namen te tonen die beginnen met de zoekterm. We zetten de zoekterm en de gefilterde namen in een object en sturen dit naar de view.
Het is belangrijk om de zoekterm te normaliseren. We willen niet dat de zoekterm "Sven" niet gevonden wordt omdat de gebruiker "sven" heeft ingegeven. Daarom zetten we de zoekterm en de namen om naar kleine letters met de toLowerCase
methode. Let hier op dat we zoeken op de beginletters van de namen. Als je wil zoeken op een deel van de naam, kan je de includes
methode gebruiken.
Merk op dat we de q
variabele ook meegeven aan de view. Zo kunnen we de zoekterm tonen in het input veld en blijft deze behouden wanneer de pagina herladen wordt.
Sorteren is een andere use case waarbij we de query string kunnen gebruiken. De opzet is iets complexer dan de zoekfunctie, maar het principe blijft hetzelfde. We willen de gebruiker toelaten om de namen te sorteren op basis van een bepaald veld.
Het eerste wat we gaan doen is twee query parameters kiezen voor de sorteerfunctie: sortField
en sortDirection
. De gebruiker kan een veld kiezen om op te sorteren en een richting. We halen deze als volgt op:
We kijken hier of de query parameters bestaan. Als ze bestaan, gebruiken we de waarde. Als ze niet bestaan, gebruiken we een default waarde. We gebruiken de sort
methode van een array om de namen te sorteren. De richting van de sortering bepalen we door de return waarde van de sorteerfunctie om te keren. Als de richting "asc" is, sorteren we de namen in oplopende volgorde. Als de richting "desc" is, sorteren we de namen in aflopende volgorde.
We geven nu deze gesorteerde namen mee aan de view:
Dan kunnen we nu de gesorteerde namen tonen in de view. We voorzien ook al een formulier om de gebruiker toe te laten om de namen te sorteren:
Als we de sorteerrichting willen bijhouden in de view, kunnen we dit doen door de selected
property van de optie te gebruiken.
We moeten dan wel de sortField
en sortDirection
variabelen meegeven aan de view:
Bij veel velden kan het handig zijn om de opties voor de select elementen te genereren in de route. Dit kan je doen door een array van objecten te maken en deze door te geven aan de view:
en kan je deze doorgeven aan de view:
In onze ejs kunnen we dan de select elementen genereren:
In plaats van query strings te gebruiken, kunnen we ook gestructureerde routes maken die parameters integreren in het pad zelf. Route parameters laten ons toe parameters te definiëren in onze route. Bijvoorbeeld:
Parameters van een route starten met :
. De gebruiker moet een waarde achter /person/
plaatsen om de route aan te spreken.
Door :index
bepaal je de naam van de property die de waarde van de parameter zal bevatten. Deze naam kan je dan gebruiken om de property terug te vinden in het object params
van het request object.
Je kan ook meerdere parameters meegeven:
Het Response object is een object dat door Express wordt aangemaakt en wordt meegegeven aan de callback functie van een route. Het bevat methodes om de response van de server te configureren. Je kan bv. de inhoud van de response, de headers, de status code, etc. instellen.
We hebben dit object al gebruikt om bijvoorbeeld de content type van de response te configureren, de status code te wijzigen, response te sturen, etc. We zien hier nog een aantal nuttige methodes.
Een redirect is een HTTP response die instructies bevat voor de client om een nieuwe request te sturen naar een andere URL. Een redirect wordt gebruikt om bv. een gebruiker door te sturen naar een andere pagina. De client wordt automatisch doorverwezen naar de nieuwe URL.
Om een redirect te sturen, gebruik je de method res.redirect
:
Soms is het interessant om een redirect te sturen naar de vorige pagina. Dit kan je doen met de method res.redirect
en de waarde back
:
Dit kan je bijvoorbeeld gebruiken om een gebruiker door te sturen na het verzenden van een POST request.
De status code van een HTTP response geeft aan of de request geslaagd is of niet. De status code wordt automatisch ingesteld op 200 (OK) wanneer je een response verstuurd. Je kan de status code wijzigen met de method res.status
:
Wil je direct een response sturen met een bepaalde status code, gebruik dan de method res.sendStatus
:
Hier een tabel met de meest gebruikte status codes:
Het is belangrijk om de juiste status code te gebruiken zodat de client weet of er iets mis is gegaan of niet. En als er iets mis is gegaan, kan de client bv. een foutmelding tonen.
Net zoals bij een request, kan je ook bij een response headers instellen. Dit kan je doen met de method res.set
:
Als je een response verstuurd, kan je geen headers meer wijzigen. Als je dit toch probeert, krijg je de volgende foutmelding:
Error: Can"t set headers after they are sent.
Bijvoorbeeld:
Dit komt omdat de headers al verstuurd worden door de send functie. Je kan dit oplossen door de headers te configureren voor je de response verstuurd:
De response type wordt automatisch ingesteld op text/html
wanneer je een response verstuurd. Je kan de response type wijzigen met de method res.type
:
Je kan ook de response type instellen op een van de volgende waarden: html
, text
, json
, xml
. Als je een van deze waarden gebruikt, wordt de content type automatisch ingesteld op de juiste waarde:
Middleware is een functie die toegang heeft tot de request en response objecten. Middleware kan de request en response objecten aanpassen, of de request doorsturen naar de volgende middleware functie in de stack. Je kan dus bijvoorbeeld een functie schrijven die wordt uitgevoerd voordat de request naar de route wordt gestuurd.
Je hebt al een aantal keer middleware gebruikt. Telkens je de app.use()
functie gebruikt, voeg je middleware toe aan de stack. Je hebt bijvoorbeeld de express.static()
functie gebruikt om statische bestanden te serveren. Deze functie is een middleware functie.
Wil je bijvoorbeeld een functie schrijven die een request logt voor elke request die binnenkomt? Dan kan je de volgende functie schrijven:
Vergeet niet om de next()
functie aan te roepen. Anders zal de request niet naar de volgende middleware functie in de stack gaan en zal deze request ook niet naar de route gaan.
Soms is het handig om bepaalde variabelen beschikbaar te maken in alle views. Je kan deze variabelen toevoegen aan de res.locals
object. Deze variabelen zijn dan beschikbaar in alle views. Zo moet je niet elke keer dezelfde variabelen doorgeven aan de render()
functie. Je moet deze dan wel toevoegen aan de res.locals
object in een middleware functie.
Je kan nu in de index.ejs
view de title
variabele gebruiken zonder deze mee te geven aan de render functie.
Je zou eventueel ook een user
object kunnen toevoegen aan de res.locals
object. Zo kan je in alle views de user
variabele gebruiken. Je kan deze variabele dan bijvoorbeeld gebruiken om te bepalen of de gebruiker is ingelogd of niet.
Stel dat je een API hebt waarbij je een security token moet meesturen met elke request. Je kan dan een middleware functie schrijven die de security token controleert. Als de security token niet klopt, dan kan je een error terugsturen. Als de security token wel klopt, dan kan je de request doorsturen naar de volgende middleware functie in de stack. We zullen de authorization header gebruiken om de security token mee te sturen.
Hij zal hier dus eerst de security token controleren. Als de security token niet klopt, dan zal hij een 401 status code terugsturen met de tekst "Unauthorized". Als de security token wel klopt, dan zal hij de request doorsturen naar de volgende middleware functie in de stack. In dit geval is dat de route. Hij zal dus "Hello world" terugsturen. Alle andere routes zullen ook de security token controleren.
Je kan ook een aparte middleware functie schrijven en deze dan toevoegen aan de stack. Dit is handig als je een complexe middleware functie hebt. Je kan dan de middleware functie in een apart bestand schrijven en deze dan toevoegen aan de stack.
We zullen een apart bestand maken voor de middleware functie. We zullen een bestand maken met de naam verifyAuthToken.ts
in de middleware
directory. Dit bestand zal er als volgt uitzien:
en dan kunnen we deze middleware functie toevoegen aan de stack:
Een voorbeeld van een middleware functie die utility functies toevoegd aan de res
object is de volgende:
en dan kan je deze middleware functie toevoegen aan de stack:
In dit voorbeeld voegen we een aantal utility functies toe aan de res.locals
object. Deze functies zijn dan beschikbaar in alle views. Je kan dan bijvoorbeeld de formatDate
functie gebruiken om een datum te formatteren. Je kan de formatCurrency
functie gebruiken om een bedrag te formatteren als een valuta. Je kan de random
functie gebruiken om een willekeurig getal te genereren tussen twee getallen.
We hadden al gezien dat we 404 pagina"s kunnen maken aan de hand van de volgende code:
Eigenlijk is dit ook een middleware functie. Deze middleware functie zal worden uitgevoerd als geen enkele route de request afhandelt. Hij zal dan een 404 status code terugsturen met de tekst "Page not found".
Je kan ook een middleware functie schrijven die errors afhandelt. Deze middleware functie moet 4 parameters hebben. De eerste parameter is de error, de tweede parameter is de request, de derde parameter is de response en de vierde parameter is de next
functie. Als je een error hebt, dan kan je de next
functie aanroepen met de error als parameter. De error zal dan worden afgehandeld door de error handling middleware functie.
We kunnen een nieuw bestand maken met de naam handleError.ts
in de middleware
directory. Dit bestand zal er als volgt uitzien:
en dan kunnen we deze middleware functie toevoegen aan de stack:
In dit voorbeeld zal de error handling middleware functie de error afhandelen. Omdat dit een onafgehandelde error is, zal de error handling middleware functie worden uitgevoerd. Hij zal dan een 500 status code terugsturen en een error pagina renderen met de error message en de error stack.
De error pagina kan er als volgt uitzien:
Je kan ook middleware functies toevoegen aan een specifieke route. Je kan dan ook een extra middleware functie toevoegen aan de route. Deze middleware functie zal dan eerst worden uitgevoerd voordat de route wordt uitgevoerd.
In het bovenstaande voorbeeld zal de loggingMiddleware
functie alleen worden uitgevoerd voor de "/" route. De "/admin" route zal de loggingMiddleware
functie niet uitvoeren.
Je kan ook middleware functies maken die configureerbaar zijn. Je kan dan een functie schrijven die een parameter heeft. Deze functie zal dan op zijn beurt een middleware functie teruggeven. Je kan dan de parameter doorgeven aan de middleware functie.
We zullen een nieuwe middleware functie maken die een parameter heeft. Deze parameter zal de status code zijn. We zullen een bestand maken met de naam errorHandler.ts
in de middleware
directory. Dit bestand zal er als volgt uitzien:
en dan kunnen we deze middleware functie toevoegen aan de stack:
In dit voorbeeld zal de errorHandler
functie een middleware functie teruggeven. Deze middleware functie zal de status code gebruiken die is doorgegeven aan de errorHandler
functie. In dit geval is dat de 500 status code.
Een voorbeeld van een configureerbare middleware functie is een request limiter. Je kan een middleware functie schrijven die een parameter heeft. Deze parameter zal aangeven hoeveel keer een request mag worden uitgevoerd door een bepaalde gebruiker via een bepaald IP adres. Als de gebruiker te veel requests doet, dan kan je een 429 status code terugsturen.
We zullen een nieuwe middleware functie maken die een parameter heeft. Deze parameter zal het aantal requests zijn dat een gebruiker mag doen. We zullen een bestand maken met de naam requestLimiter.ts
in de middleware
directory. Dit bestand zal er als volgt uitzien:
en dan kunnen we deze middleware functie toevoegen aan de stack:
De volgorde van de middleware functies is belangrijk. De eerste middleware functie die wordt toegevoegd aan de stack zal als eerste worden uitgevoerd. De laatste middleware functie die wordt toegevoegd aan de stack zal als laatste worden uitgevoerd.
Doe je bv het volgende:
Dan zal er niet meer gelogd worden als er te veel requests zijn. De requestLimiter zal de request al afhandelen en de loggingMiddleware zal niet meer worden uitgevoerd.
Als je het andersom doet dan zal de loggingMiddleware eerst worden uitgevoerd en dan pas de requestLimiter en zal er dus nog gelogd worden als er te veel requests zijn.
Let op welke characters je gebruikt in een query string. Je kan bv. geen spaties gebruiken. Wil je in jouw client applicatie een random string meegeven als waarde, gebruik dan om deze om te zetten in een geldige string!
200
OK
De request is geslaagd
201
Created
De request is geslaagd en een nieuwe resource is aangemaakt
204
No Content
De request is geslaagd, maar er is geen inhoud om te tonen
400
Bad Request
De request is niet correct
401
Unauthorized
Missende of niet geslaagde authorisatie
403
Forbidden
De client mag deze resource niet bekijken
404
Not Found
De resource is niet gevonden
500
Internal Server Error
Er is een fout opgetreden op de server
text/html
html
HTML
text/plain
text
Plain text
application/json
json
JSON
application/xml
xml
XML
Om MongoDB te gebruiken in TypeScript, moeten we de MongoDB driver installeren. Dit is een package die ons toelaat te connecteren op een MongoDB server en database calls uit te voeren.
Deze package is volledig in TypeScript geschreven en is dus makkelijk te gebruiken in TypeScript. Je hoeft dus geen extra types te installeren.
Eerst importeren we MongoClient van mongodb. Vervolgens maken we een connectie string aan. Deze string bevat de username, password en url van de MongoDB server. Als je MongoDB Atlas gebruikt, kan je deze connectie string vinden in de MongoDB Atlas console. Vervolgens maken we een nieuwe MongoClient aan met deze connectie string.
Vervolgens maken we een async functie aan die de connectie maakt met de MongoDB server. De reden dat we dit in een async functie doen, is omdat de connectie even kan duren. We willen niet dat de rest van de code uitgevoerd wordt vooraleer we verbonden zijn met de database. Bijna alle functies van de MongoDB driver zijn asynchroon en geven een promise terug. Daarom gebruiken we async/await om deze promises af te handelen.
In de try block maken we de connectie met de MongoDB server. In de catch block vangen we eventuele errors op. In de finally block sluiten we de connectie met de MongoDB server. Dit is belangrijk om te doen, anders blijft de connectie openstaan en kan dit problemen veroorzaken
Net zoals we een select kunnen doen op een relationele database, gebruiken we find and findOne om onze objecten terug op te roepen.
findOne geeft ons 1 element terug, nl. het eerste element dat matcht met de query:
Merk op dat we als parameter {} meegeven. Dit komt overeen met een lege "where" clause in relationele database termen. Wanneer we bepaalde velden willen matchen, moeten we een object meegeven. Dit object bevat properties. Deze properties komen overeen met de namen van de properties van het object waar je naar zoekt:
Pokemon objecten hebben de property name. Hierboven zoeken we dus alle Pokemon met "name" gelijk aan "eevee".
Wanneer we meerdere objecten willen ophalen, gebruiken we find:
Let op: find geeft niet direct een resultaat terug, maar een cursor. Je kan dit cursor object gebruiken om de resultaten op te halen, door bv. toArray() te gebruiken (deze geeft een promise terug!).
Als we dit allemaal bij elkaar zetten, krijgen we volgende code:
Net zoals we een select kunnen doen op een relationele database, gebruiken we find and findOne om onze objecten terug op te roepen.
findOne geeft ons 1 element terug, nl. het eerste element dat matcht met de query:
Merk op dat we als parameter {} meegeven. Dit komt overeen met een lege "where" clause in relationele database termen. Wanneer we bepaalde velden willen matchen, moeten we een object meegeven. Dit object bevat properties. Deze properties komen overeen met de namen van de properties van het object waar je naar zoekt:
Pokemon objecten hebben de property name. Hierboven zoeken we dus alle Pokemon met "name" gelijk aan "eevee".
Je kan ook een ObjectId gebruiken om te zoeken naar een specifiek object:
Je moet hier uiteraard ook de ObjectId importeren:
Wanneer we meerdere objecten willen ophalen, gebruiken we find:
Let op: find geeft niet direct een resultaat terug, maar een cursor. Je kan dit cursor object gebruiken om de resultaten op te halen, door bv. toArray() te gebruiken (deze geeft een promise terug!).
Als we dit allemaal bij elkaar zetten, krijgen we volgende code:
In de onderstaande deel van de cursus gaan we dieper in op de limit
en sort
methodes van de MongoDB driver. We gaan er vanuit dat we een collection hebben met de naam students
.
Zo voorkomen we dat we altijd deze regel code moeten herhalen.
Je kan de sort
methode gebruiken om de resultaten van een query te sorteren. Je moet eerst een find
query uitvoeren en dan de sort
methode aanroepen. Je kan deze gewoon achter de find
methode aanroepen. Je kan de richting van de sortering aangeven door een object mee te geven. Als je een 1 meegeeft, dan sorteert hij oplopend. Als je een -1 meegeeft, dan sorteert hij aflopend.
Je kan ook meerdere velden meegeven om op te sorteren. Als je meerdere velden meegeeft, dan sorteert hij eerst op het eerste veld. Als er meerdere documenten zijn met dezelfde waarde voor het eerste veld, dan sorteert hij op het tweede veld.
Je kan ook sorteren op basis van de taal van de gebruiker. Dit is handig als je bijvoorbeeld een applicatie hebt die in meerdere talen beschikbaar is. Je kan dit doen door de collation
methode aan te roepen.
In dit voorbeeld sorteren we op de naam van de student. We geven ook de locale mee als "en" (Engels). Dit zorgt ervoor dat de sortering correct gebeurt voor de Engelse taal. Dit wil zeggen dat de sortering rekening houdt met speciale tekens en hoofdletters. Over het algemeen wil je dat de sortering hoofdlettergevoelig is.
Zonder de collation
methode zou de sortering er als volgt uitzien:
Met de collation
methode ziet de sortering er als volgt uit:
Je kan de limit
methode gebruiken om het aantal resultaten te beperken. Je moet eerst een find
query uitvoeren en dan de limit
methode aanroepen. Je kan deze gewoon achter de find
methode aanroepen.
Vaak wordt de limit
methode gebruikt in combinatie met de skip
methode. De skip
methode slaat een aantal resultaten over.
Deze query zal alle documenten van de collectie ophalen, maar de eerste 5 overslaan. Daarna zal hij de volgende 5 documenten ophalen. Dit is handig om te gebruiken in combinatie met paginering.
We hebben tot nu toe gezien dat je een object kan meegeven aan de find
methode om te filteren. Dit object bevat de velden die je wil filteren en de waarden waarmee je wil filteren. Op dit moment gaven we enkel exacte waarden mee.
In MongoDB kan je ook gebruik maken van query operators. Dit zijn speciale objecten die je kan meegeven aan de find
methode om complexere queries uit te voeren.
We kunnen gebruik maken van de volgende query operators om vergelijkingen uit te voeren:
$eq
Is gelijk aan
$ne
Is niet gelijk aan
$gt
Is groter dan
$gte
Is groter dan of gelijk aan
$lt
Is kleiner dan
$lte
Is kleiner dan of gelijk aan
$in
Is in een array
$nin
Is niet in een array
Het gebruik ervan ziet er soms een beetje vreemd uit, maar het is eigenlijk heel eenvoudig. Je moet een object meegeven met als key de naam van het veld en als value een object met als key de operator en als value de waarde waarmee je wil vergelijken.
Dit geeft alle documenten terug waarvan het veld age
groter is dan 18.
Dit geeft alle documenten terug waarvan het veld age
gelijk is aan 18, 19 of 20.
Je kan ook gebruik maken van logische operatoren om meerdere voorwaarden te combineren. De volgende logische operatoren zijn beschikbaar:
$and
Logische AND
$or
Logische OR
$not
Logische NOT
Het gebruik ervan is gelijkaardig aan de vergelijkingsoperatoren. Je moet een object meegeven met als key de operator en als value een array van objecten die je wil combineren.
Dit geeft alle documenten terug waarvan het veld age
groter is dan 18 of waarvan het veld name
gelijk is aan "John".
Dit geeft alle documenten terug waarvan het veld age
groter is dan 18 en kleiner dan 25. Dus alle documenten waarvan het veld age
tussen 18 en 25 ligt.
Dit geeft alle documenten terug waarvan het veld age
niet groter is dan 18. Dus alle documenten waarvan het veld age
kleiner of gelijk is aan 18.
JSON web tokens (JWT) zijn een manier om informatie op een veilige en standaardiserende manier op te slaan en te verzenden tussen verschillende partijen. Deze tokens bestaan uit drie delen: een header, een payload en een signature. De header bevat informatie over hoe het token is opgebouwd, zoals het type en de hashing algoritme. De payload bevat de informatie die opgeslagen en verzonden moet worden, zoals gebruikersgegevens en toegangsrechten. De signature is een cryptografische hash die gebruikt wordt om de integriteit en authenticiteit van het token te verifiëren. JWT's zijn vaak gebruikt in authenticatie- en autorisatieprocessen in webapplicaties.
JWT's zijn een handige manier om informatie op te slaan en te verzenden tussen verschillende partijen zonder deze informatie op te slaan in een database. Dit betekent dat JWT's een efficiëntere manier zijn om gebruikers te authenticaten en autoriseren, omdat het verificatieproces op de client-side kan plaatsvinden. JWT's zijn ook veilig, omdat de informatie die opgeslagen is in het token niet makkelijk te lezen is zonder de juiste decoderingstechnieken. Bovendien kan de signature gebruikt worden om te verifiëren dat het token niet is gemanipuleerd tijdens het verzenden. Dit maakt JWT's een veelgebruikt mechanisme in het ontwikkelen van webapplicaties.
Hier zijn enkele scenario's waarin JSON Web Tokens nuttig zijn:
Autorisatie: Autorisatie met JWT's gebeurt door het opstellen van een token dat de toegangsrechten van een gebruiker bevat. De token in kwestie wordt dan verzonden naar de server, die de token kan verifiëren en decoderen om te bepalen of de gebruiker toegang heeft tot bepaalde functionaliteiten of gegevens. Dit kan bijvoorbeeld gebruikt worden om te bepalen of een gebruiker toegang heeft tot een bepaalde pagina of API-endpoint. De toegangsrechten die opgeslagen kunnen worden in een JWT variëren en kunnen bijvoorbeeld het recht geven op lezen, schrijven of verwijderen van gegevens.
Informatie-uitwisseling: Informatie-uitwisseling met JWT's gebeurt door het opstellen van een token dat de benodigde informatie bevat. De token wordt dan verzonden naar de ontvanger, die de token kan verifiëren en decoderen om de bijbehorende informatie te gebruiken. De informatie die opgeslagen kan worden in een JWT varieert en kan bijvoorbeeld gebruikersgegevens, toegangsrechten of andere relevante informatie bevatten.
Je vraagt je misschien af waarom de authenticatie server de informatie niet gewoon als een JSON
-object kan verzenden en waarom deze moet worden omgezet in een ondertekende "token".
Als de authenticatie server het als een gewoon JSON
-object verzendt, kan de client niet controleren of de inhoud dat ze ontvangt correct is. Een kwaadwillende aanvaller zou bijvoorbeeld de gebruikers-ID kunnen wijzigen vooraleer het bij de client terechtkomt. De client zou dat op geen enkele manier te weten kunnen komen.
Vanwege dit beveiligingsprobleem moet de authenticatie server deze informatie verzenden op een manier die kan worden geverifieerd door de client, en hier komt het concept van een ondertekende "token" in beeld.
Simpel gezegd, een token is een string die bepaalde informatie bevat die veilig kan worden geverifieerd. Het kan een willekeurige set alfanumerieke tekens zijn die naar een ID in de database verwijzen, of het kan een gecodeerde JSON zijn die door de client zelf kan worden geverifieerd.
In zijn compacte vorm bestaan JSON Web Tokens uit drie delen gescheiden door punten (.), namelijk:
Header, Bestaat uit twee delen:
Het ondertekeningsalgoritme dat wordt gebruikt. Dit bepaalt onder andere of je met één secret werkt of met een public-private key pair;
Het type token, dat in dit geval "JWT
" is;
Payload:
De payload bevat de claims of het JSON-object;
Signature:
Een tekenreeks die wordt gegenereerd via een cryptografisch algoritme dat kan worden gebruikt om de integriteit van de JSON-payload te verifiëren;
Een JWT er meestal als volgt uit.
Laten we de verschillende onderdelen opsplitsen.
Header
De header van een JWT bevat informatie over hoe de token is opgebouwd, zoals het type en de hashing algoritme. Dit is belangrijk om te weten om de token te kunnen decoderen en verifiëren.
De header bestaat doorgaans uit twee delen:
Het type token, dat JWT
is;
Het ondertekeningsalgoritme dat wordt gebruikt, zoals HMAC
SHA256
of RSA
;
Bijvoorbeeld:
Vervolgens is deze JSON Base64Url-gecodeerd om het eerste deel van de JWT te vormen.
Payload
Het tweede deel van de token is de payload, die de claims bevat. Claims zijn uitspraken over een entiteit (meestal de gebruiker) en aanvullende gegevens.
De payload bevat de informatie die opgeslagen en verzonden moet worden in de token, zoals gebruikersgegevens en toegangsrechten. Deze informatie wordt gecodeerd in de token zodat het niet makkelijk te lezen is zonder de juiste decoderingstechniëken.
Een JWT kan bijvoorbeeld een claim met de naam name
bevatten die beweert dat de naam van de gebruiker "AP user" is. In een JWT wordt een claim weergegeven als een key/value-pair waarbij de key altijd een tekenreeks is en de value een JSON-waarde kan zijn. Het volgende JSON-object bevat bijvoorbeeld drie claims (sub
, name
, iat
):
Vervolgens de payload ook JSON Base64Url-gecodeerd om het tweede deel van de JWT te vormen.
Signature
De signature wordt gebruikt om de integriteit en authenticiteit van de token te verifiëren, om te controleren of het token niet is gemanipuleerd tijdens het verzenden. Op deze manier wordt de informatie op een veilige en betrouwbare manier gewaarborgd in de JWT token.
Als je bijvoorbeeld het HMAC SHA256-algoritme wilt gebruiken, wordt de handtekening op de volgende manier gemaakt:
De handtekening wordt gebruikt om te verifiëren dat het bericht onderweg niet is gewijzigd. Ze kan aangemaakt zijn met hetzelfde geheim dat nodig is om te handtekening te verifiëren (dan spreken we over een secret) of kan ondertekend zijn met een geheim stuk data (private key) waarvoor de decryptiesleutel (public key) wel algemeen gekend is.
De uitvoer bestaat uit drie Base64-URL-tekenreeksen gescheiden door punten die gemakkelijk kunnen worden doorgegeven in HTML- en HTTP-omgevingen.
Het volgende toont een JWT met de vorige header en payload gecodeerd en is ondertekend met een geheim:
Wanneer een gebruiker zich succesvol aanmeldt met zijn inloggegevens, genereert de authenticatie-server een JWT die de identiteit van de gebruiker certificeert. De auth-server retourneert de JWT naar de USER die het vervolgens kan gebruiken om de beveiligde routes van de resource server aan te roepen. De resource server verifieert de JWT en stuurt de gewenste gegevens naar de USER.
Gebruiker meldt zich aan met gebruikersnaam en wachtwoord of google/facebook;
Authenticatie server verifieert de inloggegevens en geeft een jwt uit die is ondertekend met een "secret" of een private key;
De gebruiker gebruikt de JWT om toegang te krijgen tot beveiligde bronnen. De JWT wordt meegestuurd in de HTTP-authorization header;
De resource server verifieert vervolgens de authenticiteit van de token met behulp van de "secret" of een public key.
Houd er ook mee rekening dat de informatie opgeslagen in JSON Web Tokens, hoewel beschermd tegen manipulatie, voor iedereen leesbaar is. Plaats geen geheime informatie in de payload of header-elementen van een JWT, tenzij deze versleuteld zijn.
Telkens wanneer de gebruiker toegang wil tot een beschermde route of bron, moet de gebruiker in kwestie de JWT mee verzenden in de header van de request. De beveiligde routes van de server controleren op een geldige JWT in de Authorization-header en als deze aanwezig is, krijgt de gebruiker toegang tot beveiligde bronnen.
Resources
In deze sectie gaan we dieper in op het updaten van documenten in een MongoDB database. We gaan er weer vanuit dat we een collection hebben met de naam students
.
Je kan de updateOne
methode gebruiken om een document te updaten. Je moet een filter meegeven om het document te selecteren dat je wil updaten en een object met de nieuwe waarden.
Dit zal het eerste document met de naam "John" updaten zodat de leeftijd 20 is. Als je meerdere documenten wil updaten, dan kan je de updateMany
methode gebruiken.
Dit zal alle documenten updaten waarvan de leeftijd kleiner is dan 18 zodat de leeftijd 18 is.
Soms wil je een document updaten als het bestaat, maar aanmaken als het niet bestaat. Je kan de upsert
optie meegeven aan de updateOne
methode om dit te doen. Upsert staat voor de combinatie van update en insert.
Dit zal het document met de naam "John" updaten als het bestaat, maar aanmaken als het niet bestaat.
Soms wil je bepaalde gegevens niet hard coderen in je code. Dit kan bijvoorbeeld het geval zijn wanneer je een geheime sleutel hebt die je niet wil delen met anderen of wanneer je een bepaalde instelling wilt kunnen wijzigen zonder je code te moeten aanpassen. In deze gevallen kan je gebruik maken van environment variabelen.
Omgevingsvariabelen in Node worden gebruikt om:
Gevoelige gegevens op te slaan, zoals wachtwoorden, API-referenties en andere informatie die niet rechtstreeks in je code mag worden geschreven om beveiligingsrisico's te voorkomen.
Instellingen te configureren die kunnen verschillen tussen omgevingen. Denk maar aan poorten en verwijzingen naar databanken (development, staging, test of productie).
Je hebt out of the box toegang tot omgevingsvariabelen in Node.js. Wanneer je een Node server opstart, biedt het automatisch toegang tot alle bestaande omgevingsvariabelen door een env-object te maken binnen het globale process object.
Als je bijvoorbeeld:
uitvoert in de terminal, kan je de waarde van de omgevingsvariabele PORT
ophalen met process.env.PORT
.
Als je deze niet hebt ingesteld, zal de waarde undefined
zijn. We kunnen dit oplossen door er een default waarde aan toe te kennen.
Als je er eenmaal een aantal hebt gedefinieerd, zal je snel merken dat het een onderhoudsnachtmerrie wordt. Stel je voor dat je een tiental omgevingsvariabelen gebruikt. Dit schaalt niet goed als je ze allemaal op één regel moet typen.
Omgevingsvariabelen uitvoeren vanaf een terminal is zeker handig. Maar het heeft zijn nadelen:
Je kan de lijst met variabelen niet raadplegen;
Het is veel te gemakkelijk om een typfout te maken;
Een veel gebruikte oplossing voor het organiseren en onderhouden van je omgevingsvariabelen is het gebruik van een .env-bestand. Het helpt ons om alle omgevingsvariabelen op één plek te definiëren en indien nodig te wijzigen.
Bijvoorbeeld, in een .env-bestand:
Om een .env
bestand te gebruiken in je Node.js project, moet je de dotenv
package installeren.
Vervolgens moet je de package importeren in je code en de config
methode aanroepen. Op dat moment zal de package de variabelen uit het .env
bestand laden in process.env
.
Doe je dit niet, dan zal je undefined
zien in de console.
Het is belangrijk om te weten dat omgevingsvariabelen of een .env
bestand nooit mogen worden opgenomen in je versiebeheer. Dit is een beveiligingsrisico omdat het gevoelige informatie bevat. Zorg ervoor dat je deze bestanden toevoegt aan je .gitignore
bestand.
Hashing is een techniek die gebruikt wordt om data onleesbaar te maken. Het is een eenzijdige functie, wat betekent dat je de originele data niet kan herstellen uit de hash. Het is een veelgebruikte techniek om wachtwoorden te beveiligen.
Bcrypt is een populaire library om wachtwoorden te hashen. Het is een implementatie van het Blowfish algoritme en is ontworpen om langzaam te zijn, zodat het moeilijk is om wachtwoorden te kraken.
Om Bcrypt te gebruiken in je Node.js project, moet je de library eerst installeren via npm:
Alle bcrypt functies hebben een asynchrone en een synchrone (blokkerende) variant. Omdat hashing een intensieve taak is, is het aan te raden om de asynchrone variant te gebruiken want deze is geschikt voor asynchrone omgevingen zoals Node.js. Er is een versie met promises en een versie met callbacks. Wij verkiezen die met promises met gebruik van async
en await
.
Je kan op de volgende manier een wachtwoord hashen:
Als je dit uitvoert zal je een hash zien die er ongeveer zo uitziet: $2b$10$UVVA3Gy.0iDmSXTZQfwu8.n96QCw.GkjfTYfb0GcTzM/N0KxsPg8S
Je merkt op dat we hier een saltRounds variabele gebruiken. Voorlopig mag je altijd 10 gebruiken, we zullen later uitleggen wat dit betekent.
Als je twee keer een wachtwoord hash, zal je twee verschillende hashes krijgen. Hier zijn bepaalde redenen voor, maar het belangrijkste dat je moet weten is dat je niet het volgende kan doen:
Hiervoor moet je de compare
functie gebruiken:
Hier moet je geen saltRounds
meegeven, omdat de salt in de hash zit en automatisch wordt gebruikt.
Een salt is een willekeurige waarde die wordt toegevoegd aan de data voordat het gehasht wordt. Dit zorgt ervoor dat twee keer dezelfde data een andere hash zal opleveren. Dit is belangrijk omdat het voorkomt dat een aanvaller een rainbow table kan gebruiken om wachtwoorden te kraken. Een rainbow table is een tabel met hashes van veelgebruikte wachtwoorden. Als je geen salt gebruikt, kan een aanvaller de hash van een wachtwoord opzoeken in de tabel en zo het wachtwoord achterhalen. Bcrypt voegt automatisch een salt toe aan de hash, dit is waarom je je dus nooit geen twee keer dezelfde hash zal krijgen, zelfs voor de zelfde data.
De saltRounds
parameter bepaalt hoeveel werk bcrypt moet doen om een hash te berekenen. Hoe hoger het getal, hoe langer het duurt om een hash te berekenen. Dit is belangrijk omdat het het moeilijker maakt voor een aanvaller om wachtwoorden te kraken. Als je een te laag getal gebruikt, kan een aanvaller met een krachtige computer veel wachtwoorden per seconde kraken. Als je een te hoog getal gebruikt, kan het te lang duren om een hash te berekenen. Dit maakt het bijna onmogelijk om binnen redelijke tijd voor alle combinaties van wachtwoorden een hash te berekenen. Een goede waarde voor saltRounds
is 10. Let er op dat je deze waarde nooit te hoog zet, want dit zal het voor de hacker moeilijker maken om wachtwoorden te kraken, maar ook voor jezelf om wachtwoorden te hashen. Dus dit heeft een impact op de performantie van je applicatie.
Probeer maar eens zelf een wachtwoord te hashen met een saltRounds
van 20 en 5. Je zal merken dat het met 20 veel langer duurt dan met 5.
Timing attacks zijn aanvallen waarbij een aanvaller probeert om informatie te verkrijgen door de tijd te meten die het kost om een bepaalde taak uit te voeren. Dit kan bijvoorbeeld gebruikt worden om een wachtwoord te kraken door te meten hoe lang het duurt om een hash te berekenen. Bcrypt is ontworpen om dit soort aanvallen te voorkomen door een vaste tijd te nemen om een hash te berekenen, ongeacht de input. Dit voorkomt dat een aanvaller informatie kan verkrijgen door de tijd te meten. Bv: als je een lang wachtwoord hebt, zal het niet langer duren om een hash te berekenen dan voor een kort wachtwoord.
Je kan hashing ook gebruiken om de integriteit van bestanden te controleren. Als je een hash berekent van een bestand en deze hash bewaart, kan je later controleren of het bestand is gewijzigd. Als de hash van het bestand overeenkomt met de bewaarde hash, weet je dat het bestand niet is gewijzigd. Als de hash niet overeenkomt, weet je dat het bestand is gewijzigd en kan je actie ondernemen.
Vaak zie je op een website iets zoals dit:
Als je het bestand gedownload hebt kan je met het sha256sum
in je console de hash berekenen en controleren of deze overeenkomt met de hash op de website.
of in Windows:
Als de hashes overeenkomen, weet je dat het bestand niet is gewijzigd en dat je het veilig kan gebruiken.
Encryptie is een techniek die gebruikt wordt om data te versleutelen. Het grote verschil met hashing is dat je de originele data kan herstellen uit de versleutelde data. Dit doe je aan de hand van een sleutel (een soort passwoord). Encryptie wordt vaak gebruikt om data te beveiligen tijdens transport (zoals gebeurd bij HTTPS) of om data te beveiligen op een harde schijf.
Er zijn twee soorten encryptie: symmetrische en asymmetrische encryptie.
Bij symmetrische encryptie wordt dezelfde sleutel gebruikt om data te versleutelen en te ontsleutelen. Dit betekent dat de sleutel geheim moet blijven, anders kan iedereen de data ontsleutelen. Symmetrische encryptie is snel en efficiënt, maar heeft als nadeel dat je de sleutel veilig moet kunnen uitwisselen. Beide partijen moeten de sleutel kennen om data te kunnen uitwisselen.
Het grote nadeel van symmetrische encryptie is dat je de sleutel veilig moet kunnen uitwisselen. Als je de sleutel via een onveilig kanaal verstuurt, kan een aanvaller de sleutel onderscheppen en zo toegang krijgen tot de data.
Bij asymmetrische encryptie worden twee sleutels gebruikt: een publieke en een private sleutel. De publieke sleutel wordt gebruikt om data te versleutelen en de private sleutel wordt gebruikt om data te ontsleutelen. Dit betekent dat je de publieke sleutel veilig kan delen met anderen, zonder dat ze toegang hebben tot de private sleutel. Assymetrische encryptie is veiliger omdat er geen geheimen gedeeld moeten worden, maar is ook trager en minder efficiënt dan symmetrische encryptie.
Pas op dat je nooit een private sleutel deelt met anderen, want dan kan iedereen je data ontsleutelen.
Je kan via https://pgptool.org/ zelf een public en private sleutel genereren aan de hand van een online tool en hier eens mee experimenteren.
HTTPS zorgt voor een beveiligde communicatie tussen een webserver en een browser. Het maakt gebruik van symmetrische en asymmetrische encryptie om data over de lijn te beveiligen. Zo weet je zeker dat er niemand meeluistert of je data kan onderscheppen.
Als je een website bezoekt via HTTPS, kan je zien dat de URL begint met https://
en dat er een slotje in de adresbalk staat. Dit betekent dat de verbinding beveiligd is en dat je data veilig is. Let er altijd op dat je een website bezoekt via HTTPS als je gevoelige data moet invoeren, zoals wachtwoorden of creditcardgegevens.
Het is wel belangrijk om te weten dat je er niet automatisch vanuit mag gaan dat een website veilig is als het begint met https://
. Het betekent enkel dat de verbinding beveiligd is, maar niet dat de website zelf veilig is. Een website kan nog steeds kwetsbaar zijn voor aanvallen zoals cross-site scripting of SQL injection.
Een SSL certificaat is een digitaal certificaat dat wordt gebruikt om de identiteit van een website te verifiëren en om een beveiligde verbinding tot stand te brengen. Het certificaat bevat informatie over de website, zoals de naam van de eigenaar en de geldigheidsduur van het certificaat. Het bevat ook de publieke sleutel van de website, die wordt gebruikt om data te versleutelen.
Je moet hier wel mee opletten want in principe kan iedereen een SSL certificaat zelf aanmaken. Dit betekent dat een website die begint met https://
niet per se veilig is. Hier komt de rol van certificeringsinstanties (CA's) in het spel. Als je een website bezoekt via HTTPS, controleert je browser of het SSL certificaat geldig is en of het is uitgegeven door een vertrouwde CA. Als dit niet het geval is, krijg je een waarschuwing te zien en kan je beslissen of je de website wil bezoeken of niet.
Normaal gezien moet je betalen voor een SSL certificaat, maar er zijn ook gratis alternatieven beschikbaar.
Let's Encrypt is een non-profit certificeringsinstantie (CA's) die gratis SSL certificaten uitgeeft. Het doel van Let's Encrypt is om het internet veiliger te maken door het gebruik van HTTPS te stimuleren. Je kan een SSL certificaat aanvragen via de website van Let's Encrypt of via een hosting provider die Let's Encrypt ondersteunt.
In dit onderdeel gaan we een volledig werkend login systeem maken. We gaan gebruik maken van sessies om de gebruiker ingelogd te houden.
We beginnen deze keer met een volledig nieuwe express app met de volgende code:
Voor deze applicatie hebben we een aantal extra packages nodig. Voer het volgende commando uit in de terminal:
Zorg er ook voor dat je een views
en public
map hebt in de root van je project. In de views
map zullen we onze ejs bestanden plaatsen en in de public
map zullen we onze css en javascript bestanden plaatsen.
We gaan mongodb gebruiken om onze gebruikers op te slaan. We gaan dus gebruik maken van de mongodb
package. Voer het volgende commando uit in de terminal:
Het eerste wat we gaan doen is het aanmaken van een interface voor onze gebruikers. Maak een nieuwe file aan in de root van je project en noem deze types.ts
. Voeg de volgende code toe aan deze file:
We maken hier dus een interface aan voor onze gebruikers. We hebben een email, een wachtwoord en een rol. De rol kan ADMIN
of USER
zijn. Een ADMIN kan bijvoorbeeld extra functionaliteiten hebben die een USER niet heeft.
We gaan nu een database aanmaken waarin we onze gebruikers gaan opslaan.
Maak een nieuwe file aan in de root van je project en noem deze database.ts
. Voeg de volgende code toe aan deze file:
Dis is een simpel bestand dat de connectie met de database opzet. We hebben een connect
functie die de connectie opzet en een exit
functie die de connectie sluit wanneer we de applicatie stoppen. Zorg voor een .env
bestand in de root van je project met de volgende variabele:
en we zorgen dat we de connectie opzetten in onze index.ts
file:
Merk op dat we hier kiezen voor process.exit(1)
zodat de applicatie stopt wanneer er een error is. Onze applicatie kan niet zonder database connectie dus we willen niet dat de server blijft draaien wanneer er een error is.
Omdat we nog geen registratie pagina hebben kan je best altijd een eerste gebruiker toevoegen aan de database. We willen deze niet hardcoden in onze code dus we willen deze gebruiker toevoegen via twee environment variabelen. Voeg de volgende variabelen toe aan je .env
bestand:
Merk op dat dit password nog niet gehashed is. We gaan bcrypt
gebruiken om onze wachtwoorden te hashen. Voer het volgende commando uit in de terminal:
We gaan nu onze eerste gebruiker toevoegen aan de database. Voeg de volgende code toe aan je database.ts
file:
en
en roep deze functie aan in je connect
functie:
We kiezen hiervoor om de gebruiker enkel toe te voegen wanneer er nog geen gebruikers in de database zitten. Zo kan je altijd een nieuwe gebruiker toevoegen door de database te legen. Ook gebruiken we een saltRounds van 10 om ons paswoord te hashen. Vergeet deze niet te definieren bovenaan je database.ts
file:
We gaan nu een login functie maken in de database.ts
file. Voeg de volgende code toe aan deze file:
Deze functie zoekt een gebruiker in de database met de gegeven email. Als de gebruiker gevonden is wordt het wachtwoord gecontroleerd met de gegeven wachtwoord. Als het wachtwoord correct is wordt de gebruiker gereturned. Als de gebruiker niet gevonden is of het wachtwoord incorrect is wordt een error gegooid. We gebruiken de bcrypt.compare
functie om het wachtwoord te controleren.
Nu is alles langs de database kant klaar. We gaan nu een login pagina maken. Maak een nieuwe file aan in de views
map en noem deze login.ejs
. Voeg de volgende code toe aan deze file:
We hebben hier een simpel formulier met een email en een wachtwoord veld. We gaan nu een route maken in onze index.ts
file om deze pagina te tonen. Voeg de volgende code toe aan deze file:
We gaan nu sessies gebruiken om de gebruiker ingelogd te houden. We gaan dus eerst de express-session
package installeren. Voer het volgende commando uit in de terminal:
We zullen de sessie data bijhouden in een mongodb database. We gaan dus ook de connect-mongodb-session
package installeren. Voer het volgende commando uit in de terminal:
We gaan nu een nieuwe file aanmaken in de root van je project en noem deze session.ts
. Voeg de volgende code toe aan deze file:
In onze session data gaan we een User
object bijhouden. We hebben een user
property in onze SessionData
interface. We hebben ook een cookie die 1 week geldig is. We gaan nu deze middleware toevoegen aan onze app. Voeg de volgende code toe aan je index.ts
file:
We gaan nu een POST route maken om de gebruiker in te loggen. Voeg de volgende code toe aan je index.ts
file:
We gaan de gebruiker inloggen en de gebruiker in de sessie data zetten. We verwijderen het wachtwoord van de gebruiker voor we deze in de sessie data zetten. Dit is een extra beveiliging zodat het gehashte wachtwoord niet in de sessie data zit of nooit tot bij de client geraakt. We gaan de gebruiker doorsturen naar de home pagina als de login gelukt is en anders terug naar de login pagina.
Nu is het tijd om een home pagina te maken met bijbehorende routes. Maak een nieuwe file aan in de views
map en noem deze home.ejs
. Voeg de volgende code toe aan deze file:
en voeg de volgende code toe aan je index.ts
file:
Op dit moment zal je applicatie crashen als je naar de home pagina gaat en je nog niet ingelogd bent. We zouden dus best een check toevoegen om te kijken of de gebruiker ingelogd is. Voeg de volgende code toe aan je index.ts
file:
Het probleem bij onze aanpak hierboven is dat we voor elke route gaan moeten controleren of de gebruiker ingelogd is. Dit is veel werk en kan foutgevoelig zijn. We gaan dus een middleware maken die controleert of de gebruiker ingelogd is. Maak een nieuwe file aan en noem deze secureMiddleware.ts
. Voeg de volgende code toe aan deze file:
Deze middleware controleert of de gebruiker ingelogd is. Als de gebruiker ingelogd is wordt de gebruiker toegevoegd aan de res.locals
zodat deze beschikbaar is in de views. Als de gebruiker niet ingelogd is wordt de gebruiker doorgestuurd naar de login pagina.
Nu moeten we deze middleware toevoegen aan onze app. We gaan deze niet toevoegen aan elke route maar aan de routes die beveiligd moeten worden. We gaan deze middleware toevoegen aan de home route. Voeg de volgende code toe aan je index.ts
file:
Let op dat je deze niet aan de login route toevoegt. Anders kan je nooit inloggen.
Voor de volledigheid gaan we ook een logout functie toevoegen. Voeg de volgende code toe aan je index.ts
file:
en voegen we een logout knop toe aan onze home pagina:
Het is ook best om onze routes in aparte files te zetten. We gaan een routes
map maken in de root van je project. In deze map maken we een loginRouter en een homeRouter. De reden hiervoor is dat we op deze manier de volledige homeRouter kunnen beveiligen met de secureMiddleware. Maak een nieuwe file aan in de routes
map en noem deze loginRouter.ts
. Voeg de volgende code toe aan deze file:
en maak een nieuwe file aan in de routes
map en noem deze homeRouter.ts
. Voeg de volgende code toe aan deze file:
We gaan nu deze routers toevoegen aan onze app. Voeg de volgende code toe aan je index.ts
file:
We maken vaak gebruik van try catch
blokken om errors op te vangen bij het inloggen en gebruiken vervolgens een redirect
om de gebruiker terug te sturen naar de login pagina. Dit is niet ideaal. We zouden beter een error message tonen op de login pagina. We kunnen jammer genoeg geen error message meegeven met een redirect. Dus we hebben hier voor een andere oplossing nodig. Het is mogelijk om een error message mee te geven in de sessie. Eerst voorzien we een interface voor een FlashMessage
in onze types.ts
file:
Voeg een nieuwe property toe aan je SessionData
interface in je session.ts
file:
Een flash message is een bericht dat we maar 1 keer willen tonen, en dan verwijderen. We gaan nu een middleware maken die deze flash messages toevoegt aan de res.locals
. Maak een nieuwe file aan en noem deze flashMiddleware.ts
. Voeg de volgende code toe aan deze file:
Hier gaan we dus kijken of er een message in de sessie zit. Als deze er is voegen we deze toe aan de res.locals
en verwijderen we deze uit de sessie. We gaan deze middleware toevoegen aan onze app. Voeg de volgende code toe aan je index.ts
file:
Nu kunnen we heel gemakkelijk een error message toevoegen aan de sessie en deze tonen op de login pagina. Zo kunnen we de gebruiker laten weten wat er mis is gegaan. We kunnen de volgende code in de catch
blok van onze login route toevoegen:
Maar ook de volgende code bij een succesvolle login:
Nu kunnen we de volgende code in onze login.ejs
file toevoegen (of in een aparte partials file als je dit wil hergebruiken):
Het is mogelijk om met css animaties te werken om de messages te laten verdwijnen na een bepaalde tijd. Dit is echter niet de focus van deze cursus. Het is wel aan te raden om dit te doen in een echte applicatie.
Er bestaan ook packages op npm die voor jou de flash messages afhandelen. Je kan deze gebruiken als je dit wil. Maar het is ook goed om te weten hoe je dit zelf kan doen.
jsonwebtoken is een npm package die wordt gebruikt om JSON Web Tokens (JWT) te maken, verifiëren en decoderen in een nodeJS applicatie.
De types installeer je als development dependencies: npm i --save-dev @types/jsonwebtoken
.
De jsonwebtoken package biedt een aantal functies die kunnen worden gebruikt om JWTs te maken, verifiëren en decoderen.
De belangrijkste functies zijn:
jwt.sign()
, die wordt gebruikt om een JWT te maken met behulp van een gegeven payload en een geheime sleutel;
jwt.verify()
wordt gebruikt om een JWT te verifiëren en te decoderen met behulp van een geheime sleutel;
De jwt.sign
functie uit de jsonwebtokens package is een functie die wordt gebruikt om een JSON Web Token (JWT) te genereren. De jwt.sign
functie neemt twee argumenten: het eerste is de informatie die je wilt opslaan in het JWT (dit wordt ook wel de "payload" genoemd), en het tweede is een "geheim" dat wordt gebruikt om de JWT te ondertekenen en te beveiligen. De functie retourneert vervolgens een JWT dat je kunt gebruiken om de opgeslagen informatie te versturen of op te halen.
Hier is een voorbeeld van hoe je de jwt.sign
functie zou kunnen gebruiken in Typescript:
In dit voorbeeld gebruiken we de jwt.sign
functie om een JWT te genereren met de opgegeven informatie (in dit geval de userId
en username
van de gebruiker) en het opgegeven geheim. De functie retourneert een JWT die we kunnen gebruiken om de opgeslagen informatie te versturen of op te halen.
De jwt.verify()
functie is een functie in de jsonwebtoken npm package die wordt gebruikt om een JSON Web Token (JWT) te verifiëren en de bijbehorende gegevens te decoderen. De functie neemt een token en een geheime sleutel als argumenten en geeft de decodering van de token terug als een object. Hier is een voorbeeld van hoe de functie zou kunnen worden gebruikt:
In dit voorbeeld wordt de jwt.verify()
functie gebruikt om een JWT te verifiëren en te decoderen met behulp van de gegeven geheime sleutel. Als de verificatie en decodering succesvol is, worden de gegevens van de JWT weergegeven als een object. Als er een fout optreedt bij het verifiëren of decoderen van de token, wordt een foutmelding weergegeven.
We hebben hierboven ondertekend en geverifieerd met een secret. Dit is normaal voldoende als je je tokens niet over meerdere websites wil delen. Je kan ook tekenen en verifiëren met een combinatie van een public en private key. Dat is handig wanneer de producent van het token niet de (enige) consument is.
HTTP is een stateless protocol. Dit betekent dat de server geen informatie bijhoudt over de client. Elke request is onafhankelijk van de vorige. Doe je een request naar de server, dan weet de server niet wie je bent of wat je vorige requests waren.
Dit maakt het uiteraard moeilijk om bijvoorbeeld bij te houden of een gebruiker ingelogd is of niet.
Ook al heb je de eerste keer een login en paswoord meegegeven zal de tweede keer dat je de pagina bezoekt, de server niet weten wie je bent. Je zal dus opnieuw moeten inloggen.
De oplossing voor dit probleem is gebruik maken van cookies. Cookies zijn kleine stukjes data die de client kan opslaan in de browser. Bij elke request naar de server worden deze cookies meegestuurd. De server kan deze cookies lezen en zo weet de server wie de client is.
Express heeft een middleware die het makkelijk maakt om cookies te gebruiken: cookie-parser
. Je kan deze als volgt installeren:
Vervolgens kan je deze middleware toevoegen aan je Express-applicatie:
Stel dat we nu een formulier hebben waarbij de gebruiker zijn naam kan invullen. Als de gebruiker dit doet, willen we dat de gebruiker op de profielpagina terechtkomt en dat de naam van de gebruiker onthouden wordt de volgende keer dat de gebruiker de pagina bezoekt.
de index.ejs
file:
Nu kunnen we de naam van de gebruiker uitlezen in de profielpagina:
de profile.ejs
file:
We kunnen ook cookies verwijderen aan de hand van de clearCookie
methode:
en kunnen we een link toevoegen in de profile.ejs
file:
Cookies zijn een krachtig instrument om informatie bij te houden over een gebruiker. Maar dit betekent ook dat je voorzichtig moet zijn met cookies.
Je kan cookies aanpassen in de browser. Dit kan handig zijn om te testen wat er gebeurt als een cookie niet meer bestaat of als een cookie een andere waarde heeft. Dit betekent ook dat je niet zomaar gevoelige informatie in een cookie mag opslaan of dat je niet zomaar mag vertrouwen op de data die in een cookie staat.
Je kan ook een cookie instellen met een vervaldatum. Dit doe je door een extra argument mee te geven aan de cookie
methode:
De vervaldatum is een Date
object. In dit geval zal de cookie 15 minuten geldig zijn (900000 milliseconden) en daarna automatisch verwijderd worden.
Je kan ook de maxAge
property gebruiken om de vervaldatum in milliseconden mee te geven:
Een heel belangrijke eigenschap van cookies is HttpOnly
. Als je een cookie instelt met de HttpOnly
eigenschap, dan kan de cookie niet aangepast worden door client-side JavaScript. Dit is belangrijk om te voorkomen dat een aan stuk kwaadaardige JavaScript code de cookie aanpast en zo bijvoorbeeld de sessie van een gebruiker overneemt.
Als je een cookie instelt zonder httpOnly kan je met JavaScript de cookie aanpassen in de browser console:
of hem ophalen:
Je zal opmerken dat de cookie niet kan uitgelezen worden of aangepast worden als je de httpOnly
property instelt.
Een andere belangrijke eigenschap van cookies is Secure
. Als je een cookie instelt met de Secure
eigenschap, dan kan de cookie enkel verstuurd worden over een beveiligde verbinding (HTTPS).
De SameSite
eigenschap van een cookie bepaalt of een cookie meegestuurd mag worden bij een cross-site request. Dit is een belangrijke eigenschap om CSRF-aanvallen te voorkomen.
De SameSite
eigenschap kan drie waarden hebben: strict
, lax
of none
.
strict
: de cookie wordt enkel meegestuurd bij een same-site request. Dit betekent dat de cookie alleen wordt meegestuurd als de request naar dezelfde site is als waar de cookie is ingesteld.
lax
: de cookie wordt meegestuurd bij een same-site request en bij een cross-site request als het via een normale link is. Dit betekent bijvoorbeeld dat de cookie niet wordt meegestuurd als het via een POST request is.
none
: de cookie wordt altijd meegestuurd, ook bij cross-site requests. Dit kan enkel als de cookie ook de Secure
eigenschap heeft.
Een veelgebruikte toepassing van cookies is het bijhouden van een winkelkarretje. Als een gebruiker producten toevoegt aan zijn winkelkarretje, dan kan je deze producten bijhouden in een cookie. Zo weet je welke producten de gebruiker wil kopen.
de cart.ejs
file:
Merk op dat we JSON.stringify
en JSON.parse
om een array van strings op te slaan in een cookie. Cookies kunnen enkel strings opslaan, dus we moeten de array omzetten naar een string. Je kan uiteraard ook andere objecten opslaan in een cookie aan de hand van JSON.stringify
en JSON.parse
.
Een andere toepassing van cookies is het onthouden van gebruikersinstellingen. Als een gebruiker bijvoorbeeld de taal van de website wil veranderen, of het thema van de website wil aanpassen, dan kan je deze instellingen bijhouden in een cookie.
We maken eerst een interface aan voor de gebruikersinstellingen:
Vervolgens maken we een GET route aan om het formulier tonen waar de gebruiker zijn instellingen kan aanpassen en een POST route om de instellingen op te slaan in een cookie:
We kunnen nu de instellingen uitlezen in de settings.ejs
file:
Als je met JWT wilt spelen en deze concepten in de praktijk wilt brengen, kan je gebruiken om JWT's te decoderen, verifiëren en genereren.
Je kan meer informatie vinden op de website van Let's Encrypt:
We zullen ons nu baseren op de Session Based Login en deze omvormen tot een JWT Token Based Login systeem. Het eerste wat we moeten doen is uiteraard het installeren van de nodige packages. We zullen de jsonwebtoken
package gebruiken om JWTs te maken, verifiëren en decoderen.
We gaan nu niet meer gebruik maken van sessies om de user bij te houden maar gaan deze opslaan in de JWT token. We zullen de JWT token opslaan in een cookie. Dit is een veiligere manier om de token op te slaan dan in local storage. Daarom moeten we ook de cookie-parser
package installeren.
We zullen nu de JWT token aanmaken wanneer de user inlogt. We zullen de token opslaan in een cookie en deze terugsturen naar de client. In plaats van deze nu in de sessie op te slagen zullen we deze in de cookie opslagen.
Uiteraard moeten we de nodige imports doen:
en maken we de volgende aanpassing in de POST /login
route:
We geven hier aan dat de cookie enkel via http kan worden uitgelezen, dat de cookie enkel kan worden uitgelezen door de site waar deze is aangemaakt en dat de cookie enkel kan worden uitgelezen als de site via https wordt bezocht. Ook geven we aan dat de token 7 dagen geldig is (zoals in het voorbeeld van de sessie).
We moeten de JWT_SECRET
ook toevoegen aan de .env
file:
Deze moet je uiteraard zelf genereren. Bijvoorbeeld via https://jwtsecret.com/generate
. Deze moet minimum 32 karakters lang zijn.
Vergeet ook niet de User uit het session.ts
bestand te halen want deze gaan we niet meer gebruiken.
We moeten nu ook een aanpassing doen in de secureMiddleware.ts
file. We gaan de JWT token verifiëren in plaats van de sessie te controleren. We zullen de token uit de cookie halen en deze verifiëren met de jsonwebtoken
package.
We moeten ook de JWT token verwijderen wanneer de user uitlogt. Dit doen we door de cookie te verwijderen.
Hou er rekening mee dat in principe de JWT token blijft gelden tot de expiry date is bereikt. Het verwijderen van de cookie is enkel om de user uit te loggen. De token blijft geldig tot de expiry date is bereikt. Als de gebruiker deze zou kopieren en plakken in een andere browser dan zou deze nog steeds kunnen inloggen. Het is daarom belangrijk om de expiry date van de token niet te lang te maken.
Bekijk voor het labo aan te vangen eerst de volgende topics:
Github account aanmaken indien je er nog geen hebt.
Je kan de volgende invite link gebruiken om de repository te clonen: Github Classroom
Volg de instructies in de Devcontainers sectie om een devcontainer op te zetten.
Pas het README.md
bestand aan en vervang de content met je eigen naam en je AP email adres.
Gebruik het commando git add
om je wijzigingen toe te voegen aan de staging area.
Gebruik het commando git commit
om je wijzigingen te committen. Geef je commit een zinvolle boodschap.
Gebruik het commando git push
om je wijzigingen naar de remote repository te pushen.
Ga na of je wijzigingen zichtbaar zijn door via de browser naar de repository te gaan.
Maak een nieuwe map labos
in de root van je project. Dit moet je aan de hand van de terminal doen. Ga vervolgens naar de map labos
in de terminal.
Maak een nieuwe map tooling
in de map labos
en ga naar de map tooling
in de terminal.
Maak een nieuw bestand oefeningen.md
aan in de map tooling
en open het bestand in visual studio code.
Maak een wijziging in het bestand en voeg vervolgens alle wijzigingen toe aan de staging area, commit en push ze naar de remote repository.
Maak een nieuwe map backup
en kopiëer het bestand oefeningen.md
naar de map backup
.
Maak een kopie van het bestand oefeningen.md
in de backup
directory en noem het oefeningen2.md
.
Maak een kopie van de directory backup
en noem het backup2
.
Zorg dat alle wijzigingen in de terminal zichtbaar zijn in de remote repository (git add, commit, push)
Verwijder de map backup2
volledig.
Zorg dat deze wijziging ook zichtbaar is in de remote repository (git add, commit, push)
JSON web tokens kunnen vrij gedeeld worden. Dat biedt veel gebruiksgemak, maar het zorgt er ook voor dat we ze met enige voorzichtigheid moeten opslaan: als een aanvaller een JWT kan stelen, kan die zich ook voordoen als het slachtoffer.
Local storage is, eenvoudig gesteld, opslagruimte in de browser die bij een bepaalde website hoort. Dit lijkt een goede plaats om een JWT op te slaan (en dit wordt ook vaak gedaan). Het risico bestaat echter dat een externe partij JavaScriptcode kan "injecteren" in onze applicatie en dan local storage kan uitlezen. Dit komt omdat alle JavaScriptcode op onze website toegang heeft tot local storage, of we ze nu zelf geschreven hebben of niet. Dit type aanval heet een "cross-site scripting attack", omdat er een script wordt uitgevoerd dat niet van onze eigen site afkomstig is.
Het is niet gegarandeerd dat iemand er in zou slagen JavaScriptcode te injecteren (dat hangt af van hoe veel vrijheid de gebruikers hebben om informatie naar de site te sturen), maar het is niet eenvoudig alles dicht te timmeren.
Cookies zijn iets lastiger in het gebruik dan local storage, maar ze bieden ook persistentie. Ze bieden ook een "HTTP-only" optie aan, zodat ze niet door JavaScriptcode gelezen kunnen worden, maar wel met HTTP-requests naar onze site worden verstuurd. Dit maakt een cross-site scripting attack onmogelijk.
Ze kunnen ook geconfigureerd worden zodat ze enkel over HTTPS verstuurd worden. Gewoon HTTP-verkeer is leesbaar op elke tussenstop, maar HTTPS is dat niet. Dus het is niet erg dat cookies gewone tekstbestanden zijn.
Ten slotte kunnen we cookies zo instellen dat ze enkel naar de bronsite worden verstuurd. Dit is belangrijk om een ander type aanval te voorkomen, namelijk de "Cross Site Request Forgery". Dit is een techniek waarbij een aanvaller het slachtoffer informatie naar de verkeerde website doet sturen (bijvoorbeeld door deze te overtuigen op een bepaalde link te klikken).
Om een JWT token in te stellen in een Express applicatie, kan je volgende code gebruiken:
Maak een nieuw project aan met de naam text-box
.
We gaan in deze oefening een programma maken dat een tekst moet tonen in een text-box. De gebruiker zal een tekst moeten ingeven en de applicatie zal vervolgens de tekst tonen in een text-box. Hij zal de gebruiker blijven vragen om een tekst in te geven tot de gebruiker een lege tekst ingeeft.
Je kan dit doen door gebruik te maken van de console.log
functie en de repeat
functie van een string.
Bekijk voor het labo aan te vangen eerst de volgende topics:
Input Lezen (nog geen menu)
Basis types (enkel string, number en boolean)
De meeste oefeningen hieronder zijn sterk gebaseerd op de oefeningen die je hebt gemaakt in de cursus webtechnologie. Je kan deze oefeningen gebruiken als basis voor de oefeningen hieronder. Het belangrijkste verschil hier is dat je nu gebruik zal maken van NodeJS en TypeScript. Let er dus op dat alle variabelen types hebben en dat je de juiste types gebruikt.
Maak een nieuwe directory labo2
aan in de root van je project.
Maak een nieuw project aan met de naam som-van-getallen
.
We willen een programma maken dat de som van een aantal getallen berekent. De gebruiker zal eerst moeten ingeven hoeveel getallen hij wil optellen. Vervolgens zal hij de getallen moeten ingeven. Het programma zal dan de som van de getallen tonen.
De getallen moeten opgeslagen worden in een array. Je mag een for loop gebruiken om de som te berekenen.
Maak een nieuw project aan met de naam puntenboek
.
We willen een programma maken dat de punten van een aantal studenten bijhoudt. De gebruiker geeft de punten van de studenten op 20 in. Als de gebruiker geen punten meer wil ingeven dan geeft hij een lege string in.
Het programma zal dan het gemiddelde van de punten tonen. Het geeft ook het aantal studenten dat een onvoldoende heeft (minder dan 10 punten).
Maak een nieuw project aan met de naam recepten
.
Je maakt eerst een interface voor het `Recept`` object. Dit bevat een
naam (tekst)
beschrijving (tekst)
personen (getal)
ingredienten (array van ingredienten)
voor de ingredienten maak je een interface Ingredient
. Dit bevat een
naam (tekst)
hoeveelheid (tekst) (bv "1 stuk", "1 kg")
prijs (number)
Maak nu een object aan voor een lasagne recept. Je kan de ingredienten zelf kiezen. Print het recept af en bereken de totale kostprijs van het recept.
Welkom in de cursus WebOntwikkeling. In deze cursus gaan we ons verdiepen in het ontwikkelen van webapplicaties in TypeScript. TypeScript is een variant van JavaScript die het mogelijk maakt om nog beter gestructureerde code te schrijven. We zullen zien hoe we strong typed variabelen kunnen gebruiken, hoe we interfaces kunnen gebruiken en hoe we asynchroon kunnen programmeren.
Nadat we deze basis onder de knie hebben gaan we ons verdiepen in Express, een webframework voor NodeJS. We zullen zien hoe we een webserver kunnen opzetten, hoe we routes kunnen gebruiken en hoe we middleware kunnen implementeren. We zullen ook zien hoe we een template engine kunnen gebruiken om dynamische webpagina's te maken. We zullen ook zien hoe we data kunnen ophalen uit een externe API en hoe we data kunnen opslaan in een MongoDB database.
In het laatste deel zullen we ons verdiepen in secure coding. We zullen zien hoe we best practices kunnen toepassen in een reële programmeeropdracht. We zullen ook zien hoe we encryptie en hashing kunnen toepassen en hoe we cookies, sessions en tokens kunnen gebruiken. We zullen ook zien hoe we HTTPS en SSL kunnen toepassen.
We verwachten in deze cursus een basiskenis van JavaScript die je kan opdoen in de cursus Web Technologie. We verwachten ook dat je een basis kennis hebt van HTML en CSS.
De cursus is opgebouwd uit 4 verschillende delen:
Wat is TypeScript
Strongly typed variables
Typed Methodes
Interfacing
Werken met API's in TypeScript:
Data uit API m.b.v. TS ophalen
Uitlezen van data uit API in webapplicatie
Uitgebreide kennis van JS/TS-frameworks:
Gebruik maken van Node.js
Een webserver opzetten in Express.js
Gebruik maken van een template engine (e.js) in Express.js
Routes gebruiken
Middleware implementeren
Geavanceerde web-API's gebruiken
Zelf een MongoDB opzetten
Een MongoDB koppelen aan een eigen webapplicatie
Basiskennis en -principes van secure coding (OWASP):
Encryptie, hashing
Cookies, sessions, tokens
HTTPS, SSL
Misuse/abuse cases
Vooraleer we kunnen starten met het schrijven van een node applicatie moeten we eerst een nieuwe directory aanmaken waar we onze code in kunnen plaatsen.
We zullen in dit geval een nieuwe directory aanmaken met de naam hello
. Je kan deze in een directory theorie
plaatsen.
Vervolgens zorg je ervoor dat je in de hello
directory zit aan de hand van het cd
commando.
Nu we een nieuwe directory hebben aangemaakt kunnen we een nieuw project aanmaken. Dit doen we aan de hand van het npm init
commando.
Dit commando zal een aantal vragen stellen over jouw project. Je kan deze gewoon beantwoorden door op enter te drukken. Als je dit commando hebt uitgevoerd zal je een nieuw bestand package.json
zien in je directory. Dit bestand bevat alle informatie over jouw project. We zullen hier later nog op terugkomen.
Nu we een nieuw project hebben aangemaakt moeten we een nieuwe TypeScript configuratie aanmaken. Dit doen we aan de hand van het tsc --init
commando.
Dit commando zal een nieuw bestand tsconfig.json
aanmaken in je directory. Dit bestand bevat alle configuratie opties voor de TypeScript compiler.
Nu we een TypeScript configuratie hebben aangemaakt moeten we de node types installeren. Dit zijn de types die nodig zijn om met TypeScript en Node.js te werken.
Je zal zien dat er een nieuwe directory node_modules
is aangemaakt in je project. Hierin zitten alle modules die je nodig hebt om je project te laten werken.
Nu we alle configuratie hebben aangemaakt kunnen we beginnen met het schrijven van onze code. Maak een nieuw bestand hello.ts
aan in de hello
directory. De bestandsnaam mag je zelf kiezen.
Het bestand hello.ts
moet het volgende bevatten:
Nu we ons programma hebben geschreven kunnen we dit uitvoeren. Dit kan je doen aan de hand van het ts-node
commando.
Dit commando zal je programma uitvoeren en je zal Hello, world!
zien verschijnen in je terminal.
npm init
Maakt een nieuw project aan.
tsc --init
Maakt een nieuw tsconfig bestand aan. Het initialiseert een nieuw TypeScript project.
npm install --save-dev @types/node
Installeert alle types die nodig zijn om met TypeScript en Node.js te werken.
ts-node <naam file>.ts
Voert het programma uit dat je geschreven hebt in <naam file>.ts
.
Deze commando's zal je voor elk nieuw project moeten uitvoeren. Het is dus handig om deze te onthouden.
Er zijn talrijke scripts beschikbaar die het opzetten van een TypeScript of JavaScript project aanzienlijk vereenvoudigen. Een voorbeeld hiervan is create-clean-node
, een tool waarmee je met slechts één commando een nieuw project kunt starten. Door het volgende in je terminal te typen:
word je gevraagd om een projectnaam in te voeren, waarna create-clean-node
automatisch alle benodigde afhankelijkheden installeert.
Tot 2009 werd JavaScript exclusief gebruikt in browsers en was het gebruik van deze taal enkel nodig voor web pagina's. In 2009 veranderde dit verhaal helemaal bij het ontstaan van Node.js. Node.js laat het toe om JavaScript buiten de browser te gaan uitvoeren. Zo konden ook andere soorten applicaties worden gemaakt met JavaScript en steeg de populariteit van JavaScript heel snel. Dankzij Node.js konden web ontwikkelaars volledige applicaties bouwen die op web servers draaiden en in de browser met 1 dezelfde taal.
Ondertussen is JavaScript de 7de meest gebruikte programmeertaal in de wereld en kan deze gebruikt worden voor bijna alles te maken zoals:
Mobiele applicaties
Bv. met React native
Web applicaties
Bv. met React.js, angular, Vue.JS,...
Games
Bv. met Phaser, kiwi.js,...
Desktop applicaties
Bv. met electron.js
Meest gebruikte programmeertalen, Aug 2021 vergeleken met vorig jaar:
Rank
Language
Share
Trend
1
Python
29.93 %
-2.2 %
2
Java
17.78 %
+1.2 %
3
JavaScript
8.79 %
+0.6 %
4
C#
6.73 %
+0.2 %
5
C/C++
6.45 %
+0.7 %
6
PHP
5.76 %
-0.0 %
7
R
3.92 %
-0.1 %
8
Objective-C
2.26 %
-0.3 %
9
TypeScript
2.11 %
+0.2 %
10
Swift
1.96 %
-0.3 %
JavaScript is een van de meest veelzijdige talen in de programmeerwereld waar bijna alles mee kan gedaan worden. Dit is allemaal mogelijk geworden dankzij het ontstaan van Node.js in 2009, zonder Node.js zouden we voor eeuwig vast hebben gezeten in de limieten van de browser en was JavaScript nooit uitgegroeid tot de taal die het nu is.
Maak een nieuw project aan met de naam rot13
.
We willen een programma maken dat een string encodeert met de rot13 methode. De rot13 methode is een simpele methode om een string te coderen. Elke letter wordt vervangen door de letter die 13 plaatsen verder in het alfabet staat. Als je aan het einde van het alfabet komt dan ga je terug naar het begin.
De gebruiker geeft een string in en het programma toont de gecodeerde string.
De werkwijze is als volgt:
Je begint met een array van het alfabet in kleine letters.
Je vraagt de gebruiker om een string in te geven.
Je gaat door elke letter van de string en je zoekt de index van de letter in de array van het alfabet.
Je telt 13 op bij de index en je neemt de modulo van 26. Dit is de nieuwe index van de letter.
Je neemt de letter op de nieuwe index en je voegt deze toe aan een nieuwe string.
Als de letter een spatie is of een ander teken dan een letter dan voeg je deze ook toe aan de nieuwe string. Je moet dus controleren of de letter in de array van het alfabet staat.
JavaScript is over de jaren heen gegroeid tot de meest gebruikte taal voor webapplicaties op het internet. Je kan dus JavaScript gebruiken voor front- en backend en wordt deze taal tegenwoordig zelfs gebruikt voor standalone applicaties (mobile en desktop). Maar eigenlijk was het nooit de bedoeling geweest om met JavaScript grote en complexe systemen te bouwen.
Leuk weetje: JavaScript was gemaakt in 1995 door Brandan Eich in 10 dagen tijd. Uiteraard is JavaScript over de jaren heen nog veel geëvolueerd, maar het was nooit de bedoeling dat deze taal zou uitgroeien tot de taal van het internet. Het draagt dus na al die jaren nog veel van de nadelen van het prille begin mee.
Er zijn ondertussen heel wat nieuwe versies van JavaScript gepasseerd die heel wat problemen en missende features hebben toegevoegd. Deze versies worden meestal aangeduid met de letters ES gevolgd door een jaartal of nummer. Zo is ES6 de meest ondersteunde versie van JavaScript. Deze versie introduceerde concepten zoals Classes die object georiënteerd programmeren mogelijk maken in JavaScript (op een gelijkaardig manier als in C#). Zie later voor meer details hierover!
De enige toevoeging die niet aanwezig is die JavaScript tot het niveau van een programmeertaal kan krijgen zoals C# of Java is het toevoegen van types. Door de toevoeging van types wordt het mogelijk sneller fouten op te sporen en wordt de code over het algemeen leesbaarder en meer begrijpbaar. Om types te introduceren heeft Microsoft een nieuwe taal ontworpen genaamd TypeScript. Eigenlijk is TypeScript geen nieuwe taal maar een superset van JavaScript (versie ES6). Dit betekent dat alle features die in JavaScript (ES6) zitten ook in TypeScript zitten. Dus alle code die je in JavaScript zou schrijven is ook geldig voor TypeScript, maar niet andersom.
Je vraagt je dan misschien af. Kunnen browsers (of node.js) dan ook gewoon TypeScript interpreteren en uitvoeren? Het antwoord hier op is jammer "Nee". We moeten hiervoor een TypeScript compiler gebruiken om alle TypeScript code om te zetten naar gewone JavaScript code. Deze code zal dan wel uitvoerbaar zijn. We zien later hoe we dit gaan doen.
In de bovenstaande tabel lijkt JavaScript nog altijd duidelijk de populairste optie van de twee. Je vraagt je waarschijnlijk wel af waarom we dan niet gewoon JavaScript zien. Het antwoord is hier heel eenvoudig. Het is beter om eerst de goede gewoontes van TypeScript aan te leren, en dan over te schakelen naar het lossere JavaScript. Andersom is veel lastiger, slechte gewoontes leer je nu eenmaal niet snel af. En vergeet niet, de verschillen zijn in principe minimaal, en alles wat je leert voor TypeScript is zeer eenvoudig over te zetten naar JavaScript. Eigenlijk leer je in deze cursus twee talen in 1 klap!
Soms is het handig om input te kunnen lezen van de gebruiker om interactie te hebben met de gebruiker. Bijvoorbeeld om de naam van de gebruiker te vragen, zijn leeftijd, ... In NodeJS kan je dit doen aan de hand van de readline-sync
module. Deze module is standaard niet geïnstalleerd dus moet je deze eerst installeren.
We moeten ook nog de types installeren van deze module. Deze zijn niet standaard meegeleverd met de module. Je kan deze installeren aan de hand van het volgende commando:
Je moet deze module elke keer dat je deze nodig hebt importeren in je code:
Nu kan je de question
functie gebruiken om een vraag te stellen aan de gebruiker. Deze functie heeft 1 parameter, namelijk de vraag die je wil stellen. Deze functie geeft een string terug met het antwoord van de gebruiker.
Als je een getal wil lezen van de gebruiker dan moet je de question
functie gebruiken in combinatie met de Number
functie. De Number
functie zet een string om naar een getal. De question
functie geeft een string terug dus moeten we deze omzetten naar een getal.
Je bent hier ook niet zeker dat de gebruiker wel een getal zal ingeven. Als de gebruiker geen getal ingeeft dan zal de Number
functie een NaN
teruggeven. Dit is een speciale waarde die staat voor "Not a Number". Als je deze waarde probeert te gebruiken in een berekening dan zal je een NaN
terugkrijgen. Wil je dit vermijden dan kan je best eerst nakijken of de waarde wel een getal is.
Je kan ook de questionInt
functie gebruiken. Deze functie doet hetzelfde als de question
functie maar zet het antwoord van de gebruiker automatisch om naar een getal. Ook als de gebruiker geen getal ingeeft zal deze functie een foutmelding geven.
Zo verkrijg je dezelfde output als hierboven maar met minder code.
Wil je de error message aanpassen dan kan je een tweede parameter meegeven aan de questionInt
functie.
Wil je een kommagetal lezen dan kan je de questionFloat
functie gebruiken. Deze functie doet hetzelfde als de questionInt
functie maar zet het antwoord van de gebruiker automatisch om naar een kommagetal.
Soms wil je een vraag stellen aan de gebruiker waar hij alleen Yes of No kan op antwoorden. Het resultaat is dan een boolean. Je kan dit doen aan de hand van de keyInYNStrict
functie. Deze functie geeft een boolean terug.
Soms wil je een menu tonen aan de gebruiker waar hij een keuze kan maken. Je kan dit doen aan de hand van de keyInSelect
functie. Deze functie heeft 2 parameters. De eerste parameter is een array met de verschillende keuzes die de gebruiker kan maken. De tweede parameter is een optionele parameter met de vraag die je wil stellen aan de gebruiker. Deze functie geeft een getal terug met de index van de keuze die de gebruiker heeft gemaakt.
Dit ziet er als volgt uit:
Maak een nieuw project aan met de naam short-notation
Deze oefening maak je in bestand short-notation.ts
.
Schrijf de volgende functies in de kortst mogelijke arrow notaties:
In het filmpje wordt er gebruik gemaakt van wsl --install
om wsl te installeren. Gebruik in de plaats wsl --install --no-distribution
want anders zal deze ook Ubuntu installeren.
Een Dev Environment is simpelweg een systeem waar alle software, tools en hardware op geïnstalleerd zijn, zodat jij kunt programmeren aan een specifiek project. Met software en tools wordt echt alles bedoeld dat je gebruikt tijdens het programmeren:
Code Editors (bv. VS Code of Visual Studio)
Plugins (bv. een Markdown extension in VS Code)
Compilers (bv. de .NET compiler voor C#)
Sandbox omgevingen (bv. NodeJS)
...
Meestal heb je op één toestel meerdere Dev Environments geïnstalleerd. Het is nu eenmaal niet praktisch om rond te lopen met 5 laptops...
Een Dev Environment is dus vaak een complex systeem van allerlei software, tools en specifieke instellingen die samenwerken om een stuk software te ontwikkelen. Wat kan er allemaal misgaan?
Oh Nee, mijn Laptop is Kapot!
Je laptop gaat stuk, en je koopt een nieuwe. Nu moet je ALLE software en tools opnieuw installeren. Niet alleen dat, maar je zult er ook op moeten letten dat je EXACT dezelfde versie van die software en tools terug installeert! Weet jij nog of je versie 18.17.1 of versie 17.9.2 had geïnstalleerd op je laptop?
Oh Nee, een Groepswerk!
Je moet samenwerken met iemand anders. Het project werkt perfect op jouw Dev Environment, maar wilt om één of andere reden niet draaien op die van je teamgenoot. Tijd om ELKE tool en software die je gebruikt na te kijken op versie nummer!
Oh Nee, een Oud Project Werkt Niet Meer!
Voor je nieuwste projecten heb je NodeJS geupdate naar de nieuwste versie. Oeps! Nu werken je oude projecten, die gebruik maakten van een oude versie van NodeJS, niet meer!
Deployment Hell
Alles werkt perfect op jouw systeem, en ook op die van je teamgenoten. Maar tijdens het deployen naar de server, merk je dat je software niet werkt. Tijd om ELKE tool en software die je gebruikt (opnieuw) na te kijken op versie nummer!
We kunnen een Docker Container zo samenstellen dat alle tools en instellingen daarin geïnstalleerd staan. Je installeert niets meer op je eigen systeem, alles zit netjes verpakt in een Docker Container! Zo'n Docker Container waarin je je Dev Environment opslaat voor één specifiek project, dàt heet een DevContainer.
Je hebt in feite slechts 3 programma's nodig op je computer:
Git
Docker Desktop*
Visual Studio Code
*Om Docker te laten werken moet je WSL geïnstalleerd hebben op je Windows computer. Dus in principe moet je 3 dingen installeren.
Open Powershell als administrator.
Gebruik het volgende commando om na te kijken of je WSL hebt geinstalleerd, en zo ja, welke versie.
Als WSL geinstalleerd is, zou je output moeten krijgen zoals deze (versie nummers kunnen verschillen).
Indien WSL dus geinstalleerd is, kan je WSL updaten met het volgende command:
Als je WSL NIET geinstalleerd hebt, dan installeer je WSL met het volgende commando:
Download het installie-programma en voer het programma uit.
Download het installie-programma en voer het programma uit.
Download het installie-programma en voer het programma uit.
Open Visual Studio Code.
Open de Extensions tab vanuit de Sidebar.
Dit installeert 4 extensies in VS Code die je helpen met ontwikkeling in DevContainers.
Maak een nieuwe Github Repo (tijdens onze lessen gebruik je de Github url op Digitap) of gebruik een bestaande Github Repo.
Als je Github Repo reeds een Devcontainer gebruikte (meestal een devcontainer.json
bestand in een mapje genaamd .devcontainer
), zal de devcontainer nu gestart worden. De eerste keer zal een tijdje duren, want Docker moet alle nodige bestanden downloaden.
Als je Github Repo nog geen Devcontainer gebruikte, zal VS Code je een aantal vragen stellen:
Wanneer je alle vragen hebt beantwoord, wordt het devcontainer.json
bestand aangemaakt en wordt de devcontainer opgestart.
Als je de DevContainer probeert te openen, maar je krijgt een foutmelding dat je WSL versie niet up-to-date is, dan moet je WSL updaten. Dit kan je doen door het volgende stappenplan te volgen:
Open Powershell als administrator (rechtermuisknop op het Powershell icoontje, en kies voor Run as Administrator)
Voer de volgende commando's uit:
Hierna kan je best je computer herstarten om zeker te zijn dat alles goed werkt.
Bash, een afkorting voor Bourne Again SHell, is een veelgebruikte command-line interface (CLI) of terminal op Linux en Unix-achtige besturingssystemen. Devcontainers gebruiken standaard een bash shell.
Om een nieuwe terminal te openen in visual studio code klik je op Terminal
in de menubalk en vervolgens op New Terminal
. Dit opent een nieuwe terminal in de onderkant van je scherm.
Merk op dat je zelfs al werk je in windows je een bash terminal krijgt en nergens iets van een C schijf of dergelijke ziet. Dit komt omdat je in een devcontainer werkt.
Even de structuur van de terminal uitleggen. De prompt is het stukje tekst dat je ziet voor je cursor. In de afbeelding hierboven is dat node ➜ /workspaces/Deel1-Node-en-Typescript (main)
:
node
is de naam van de gebruiker. Dit is standaard in een nodejs devcontainer, je mag dit negeren.
➜
is een pijltje dat je mag negeren.
/workspaces/Deel1-Node-en-Typescript
is de huidige map waarin je zit. Dit is de map waarin je terminal opent.
(main)
is de naam van de branch waarin je zit in de git repository.
$
is de prompt zelf. Dit is een teken dat je commando's kan beginnen typen.
De cursor is het knipperende streepje dat aangeeft waar je tekst zal verschijnen als je begint te typen.
Je kan ook een terminal openen in een specifieke map door eerst naar de map te navigeren in de file explorer en dan rechts te klikken en te kiezen voor Open in Integrated Terminal
.
Omdat we hier de terminal openen in de folder
directory, zal de terminal ook openen in die map.
We gaan hier de basis commando's behandelen die nodig zijn om te navigeren in een mappen structuur. Er zijn nog veel andere commando's die je gaandeweg zal leren, maar deze zijn voldoende om te starten.
pwd
staat voor print working directory
. Dit commando toont de huidige map waarin je zit.
Je ziet dat we in de map /workspaces/Deel1-Node-en-Typescript
zitten. Dat wist je al omdat je dat ook in de prompt zag.
ls
staat voor list
. Dit commando toont de bestanden en mappen in de huidige map.
Je kan ook de ls -a
gebruiken om ook verborgen bestanden en mappen te tonen. Dit zijn bestanden en mappen die beginnen met een punt.
Let er op dat de bestanden .
en ..
altijd getoond worden. .
staat voor de huidige map en ..
staat voor de bovenliggende map.
cd
staat voor change directory
. Dit commando laat je toe om naar een andere map te gaan.
Stel je voor dat je een mappen structuur hebt zoals deze:
By default zit je in de map Deel1-Node-en-Typescript
. Als je naar de map src
wil gaan, dan typ je cd src
.
Wil je terug naar de map Deel1-Node-en-Typescript
, dan typ je cd ..
. Dit commando staat voor ga naar de bovenliggende map
. Je kan ook twee
Je kan ook rechstreeks van de ene map naar de andere gaan. Als je in de map Deel1-Node-en-Typescript
zit en je wil naar de map dir1
, dan typ je cd src/dir1
.
Tab completion werkt ook in de terminal. Als je begint te typen en dan op tab drukt, dan zal de terminal proberen aan te vullen wat je aan het typen bent. Als er meerdere mogelijkheden zijn, dan zal de terminal een lijst tonen van mogelijke aanvullingen.
mkdir
staat voor make directory
. Dit commando laat je toe om een nieuwe map aan te maken.
Stel dat je in de map Deel1-Node-en-Typescript
zit en je wil een nieuwe map labos
aanmaken, dan typ je mkdir labos
.
touch
is een commando dat je toelaat om een nieuw bestand aan te maken. Als je in de map Deel1-Node-en-Typescript
zit en je wil een nieuw bestand index.html
aanmaken, dan typ je touch index.html
.
cp
staat voor copy
. Dit commando laat je toe om een bestand of map te kopiëren. Als je een bestand index.html
wil kopiëren naar index2.html
, dan typ je cp index.html index2.html
.
Je kan ook een map kopiëren. Als je een map src
wil kopiëren naar src2
, dan typ je cp -r src src2
. De -r
staat voor recursive
en zorgt ervoor dat de map en alle bestanden en mappen in de map gekopieerd worden.
mv
staat voor move
. Dit commando laat je toe om een bestand of map te verplaatsen. Als je een bestand index.html
wil verplaatsen naar de map src
, dan typ je mv index.html src
.
Je kan ook een bestand hernoemen. Als je een bestand index.html
wil hernoemen naar index2.html
, dan typ je mv index.html index2.html
.
rm
staat voor remove
. Dit commando laat je toe om een bestand of map te verwijderen. Als je een bestand index.html
wil verwijderen, dan typ je rm index.html
.
Je kan ook een map verwijderen. Als je een map src
wil verwijderen, dan typ je rm -r src
. De -r
staat voor recursive
en zorgt ervoor dat de map en alle bestanden en mappen in de map verwijderd worden.
Meer informatie vind je op:
Ga naar
Ga naar
Ga naar
Zoek naar het "Remote Development" extension pack van Microsoft.
Een belangrijk mechanisme om asynchrone code te schrijven is het gebruik van Promises. Een Promise is een object dat een waarde bevat die pas op een later moment beschikbaar zal zijn. Zoals het engelse woord al aangeeft, is een Promise een belofte dat de functie die een promise teruggeeft, op een later moment een waarde zal teruggeven.
Een van de meest bekende functies die een Promise gebruikt is de fetch functie. Deze functie wordt gebruikt om data op te halen van een server. Alle communicatie tussen je programma en de server moet asynchroon gebeuren. Dit komt omdat je niet wil dat je programma wacht tot er een antwoord komt van de server. Zelfs al gaat de communicatie met de server heel snel, ze gaat in vergelijking met de uitvoering van een gewone instructie veel trager.
We gaan het gebruik van een Promise bekijken aan de hand van een voorbeeld. We gaan een Promise maken die een getal teruggeeft. Deze zal een vermenigvuldiging uitvoeren. We maken een Promise aan met de new Promise constructor. Deze constructor heeft als argument een functie die twee argumenten heeft: resolve en reject. Deze twee argumenten zijn functies die we kunnen aanroepen om de Promise te laten veranderen van status. Het type dat de promise teruggeeft zetten we tussen de < > tekens. In ons geval is dit een number.
Je ziet dat we hier een setTimeout functie gebruiken om de Promise na 1 seconde te laten de promise te resolven. De resolve functie wordt aangeroepen met de waarde die de Promise zal teruggeven. In dit geval is dit het getal 4.
Het gebruiken van de promise gebeurt net zoals in JavaScript met de then functie. Deze functie heeft als argument een functie die de waarde van de promise zal ontvangen. Deze functie wordt pas uitgevoerd als de promise resolved is.
Het datatype van het result argument is het type dat we hebben opgegeven bij het aanmaken van de promise. In dit geval is dit een number. Je mag het type ook weglaten. TypeScript zal dan zelf het type bepalen aan de hand van het type dat je hebt opgegeven bij het aanmaken van de promise.
Meestal maken we niet zelf een Promise aan, maar gebruiken we een functie die een Promise teruggeeft. Deze functie kan dan als return type Promise hebben. We breiden ons voorbeeld uit met een functie die een Promise teruggeeft. Deze functie zal een vermenigvuldiging uitvoeren. We geven de functie een argument mee: number1 en number2. Deze functie zal de vermenigvuldiging van deze twee getallen teruggeven.
Als je deze functie gewoon aanroept alsof het een normale functie is kan je zien dat deze een Promise teruggeeft.
Je zal hier als output het volgende krijgen:
Dit betekent dat de Promise nog niet afgerond is. We kunnen de then functie aanroepen op deze Promise om de waarde te gebruiken.
of we kunnen de then functie meteen aanroepen op de functie.
Als je zelf een promise maakt, dan kan je ook een reject functie aanroepen. Deze functie heeft als argument een error object. Dit object kan je zelf aanmaken. Het is een goed idee om een error object te maken dat een boodschap bevat die uitlegt wat er fout is gegaan.
Als je een reject functie aanroept, dan zal de then functie niet uitgevoerd worden. Je kan een catch functie aanroepen om de fout af te handelen. Deze functie heeft als argument een functie die het error object zal ontvangen.
Toegang tot het filesysteem is iets dat over het algemeen traag gaat, dus is het ook een ideaal voorbeeld van asynchrone code. De fs
module heeft een aantal functies die je kan gebruiken om bestanden te lezen en te schrijven. Deze functies hebben een variant die promises gebruikt. Deze functies kan je importeren uit de fs/promises
module.
Deze functie zal de inhoud van het bestand test.txt
inlezen en teruggeven als een string. Als er een fout optreedt, dan zal de catch functie uitgevoerd worden. Bijvoorbeeld als het bestand niet bestaat.
Nog een voorbeeld van een ingebouwde promise is de lookup
functie van de dns
module. Deze functie zal een IP adres teruggeven als een string. Als er een fout optreedt, dan zal de catch functie uitgevoerd worden.
De frontend is wat de gebruiker ziet: met HTML, CSS en TypeScript (of JavaScript) zorg je voor een structuur waarin de gebruiker kan navigeren. CSS helpt je met layout en opmaak, en TypeScript zorgt voor interactie. Het proces speelt zich volledig in de browser af. Scripts gebruik je om interactie toe te laten door de gebruiker. Deze interactie kan lokale aanpassingen veroorzaken in de client, bv het veranderen van een kleur van een knop, het sorteren van een lijst etc. Maar scripts kunnen ook communiceren buiten het proces in de browser. Aan de hand van API calls kan de client data ophalen van de backend, of data versturen naar de backend.
De backend zorgt voor de connectie met databases, het verwerken van data en het beschikbaar stellen van deze data via een API. Dit proces speelt zich op de server af. Client applicaties kunnen deze server applicatie raadplegen om data op te halen of te verwerken.
Naast de then en catch functies kan je ook gebruik maken van de async en await keywords. Deze keywords zorgen ervoor dat je code eruit ziet alsof het synchroon is, maar dat het eigenlijk asynchroon is. Over het algemeen wordt het gebruik van async en await aangeraden boven het gebruik van then en catch omdat het de code leesbaarder maakt.
We grijpen terug naar het voorbeeld van de multiply
functie:
Stel je voor dat we eerst de getallen 2 en 2 willen vermenigvuldigen en daarna het resultaat willen vermenigvuldigen met 5. We kunnen dit doen met de then functie:
Dit valt nog mee, maar als we nog een vermenigvuldiging willen uitvoeren wordt het al snel onleesbaar:
Dit probleem noemen we ook wel de callback hell. Om dit probleem op te lossen kunnen we gebruik maken van async en await. We maken de functie waarin we de vermenigvuldigingen willen uitvoeren async. We kunnen dan de await keyword gebruiken om te wachten tot de Promise is afgerond.
Let wel op dat als je deze code plaatst in de globale scope, je een error zal krijgen. Dit komt omdat je de await keyword enkel kan gebruiken in een async functie. Je kan dit oplossen door de code in een async functie te plaatsen of door de code in een IIFE (Immediately Invoked Function Expression) te plaatsen.
Ook de catch functie kan je vervangen door een try catch blok.
Het is nu mogelijk om complexe logica te schrijven zonder dat je code totaal onleesbaar wordt. Stel je voor dat je twee getallen wil uitlezen uit een bestand getal1.txt
en getal2.txt
. Vervolgens wil je een vermenigvuldiging uitvoeren en het resultaat wegschrijven naar een bestand resultaat.txt
.
Dit zou er met promises als volgt uitzien:
Dit kan met async en await als volgt:
Zo zie je dat de code veel leesbaarder is geworden en veel minder indentatie heeft.
Maak een nieuw project aan met de naam fake-fetch
waarin je jouw bronbestanden voor deze oefening kan plaatsen.
Vorm de onderstaande code om zodat het gebruik maakt van promises in plaats van callbacks. Daarna kan je de code omvormen zodat het gebruik maakt van async/await in plaats van promises.
Momenteel hebben we een werkende Express.js applicatie die we lokaal kunnen draaien. Maar hoe kunnen we deze applicatie nu online zetten zodat iedereen erbij kan? We kunnen deze manueel op een server zetten, maar dit is niet altijd even eenvoudig. Gelukkig zijn er verschillende platformen die ons hierbij kunnen helpen.
Een Platform as a Service (PaaS) is een cloud computing service die een platform biedt aan ontwikkelaars om applicaties te bouwen, testen en te deployen. Het grote voordeel van een PaaS is dat je je geen zorgen hoeft te maken over de infrastructuur. Je kan je volledig focussen op het bouwen van je applicatie.
is een PaaS die ons toelaat om onze applicatie eenvoudig online te zetten. Render ondersteunt verschillende programmeertalen en frameworks waaronder Node.js en Express.js. Het heeft github integratie waardoor je eenvoudig je code kan pushen naar je repository en deze automatisch wordt gedeployed.
Hieronder een kort stappenplan om je Express.js applicatie online te zetten op Render:
Maak een account aan op
Vervolgens maak je een nieuwe web service
aan
Kies voor Build and deploy from a git repository
zodat je rechstreeks kan verbinden met je github repository.
Zoek je github repository en kies de branch waarop je applicatie staat en druk op Connect
Hier krijg je een aantal instellingen die je kan aanpassen. Hieronder een aantal belangrijke instellingen:
Root Directory: De directory waarin je package.json
staat. Als je applicatie in een subdirectory staat, kan je deze hier aanpassen.
Runtime: Hier kies je voor Node.js
Build command: npm install
Start command: npm start
Instance type: Free tier
Als je gebruik maakt van environment variables kan je deze hier ook instellen. Deze worden dan veilig opgeslagen en niet meegegeven in je code.
Vervolgens zal je applicatie worden gebuild en gedeployed. Je krijgt een unieke URL waarop je applicatie online staat.
Tot op heden hebben we alle routes gedefinieerd in 1 bestand. Dit is prima voor kleine applicaties, maar als je applicatie groter wordt, kan het al snel onoverzichtelijk worden. Om dit te voorkomen, kan je routes definiëren in aparte bestanden. Dit kan je doen met het express.Router
object.
Een router is een mini-applicatie die je kan gebruiken om routes te definiëren net zoals je in een normale express applicatie zou doen. Je kan een router gebruiken om routes te definiëren voor een bepaald deel van je applicatie. Dit kan handig zijn om je code te structureren en overzichtelijk te houden.
Over het algemeen worden routes gedefinieerd in een aparte map, bv. routers
. In deze map kan je dan een bestand maken voor elke resource die je wilt definiëren. Bijvoorbeeld, als je een blog applicatie hebt, kan je een bestand maken voor de routes van de blogposts, een bestand voor de routes van het gebruikersbeheer, een bestand voor de routes van de comments, etc.
Je bestandsstructuur zal er dan ongeveer zo uitzien:
Die dan overeenkomen met de routes (en eventueel subroutes) die je wilt definiëren.
Om een router aan te maken, gebruik je de express.Router
functie. Deze functie retourneert een router object dat je kan gebruiken om routes te definiëren. We bouwen hieronder bijvoorbeeld een router voor de posts van een blog applicatie. We geven deze router een array van posts mee, zodat we deze kunnen gebruiken in de routes.
We maken een bestand posts.ts
aan in de routers
map en definiëren daar de routes voor de posts.
In je hoofdbestand kan je dan deze functie importeren en gebruiken om de routes te definiëren.
Alle routes die je definieert in de posts
router, zullen dan beginnen met /posts
. Dus in het geval van het voorbeeld hierboven:
Soms bestaat een formulier niet enkel uit tekstvelden, maar ook uit bestanden. Denk maar aan een formulier waar je een foto kan uploaden. In dit geval is het niet mogelijk om de data van het formulier te versturen via een GET request. We moeten dan gebruik maken van een POST request. Ook moeten we aangeven dat het formulier een bestand bevat. Dit doen we door de enctype
van het formulier aan te passen.
De enctype
van het formulier is nu multipart/form-data
. Dit betekent dat de data van het formulier in meerdere delen wordt opgesplitst. We kunnen jammer genoeg niet zomaar de data van het formulier via de req.body
variabele ophalen. We moeten hiervoor een aparte module gebruiken. Deze module heet multer
. Deze module zorgt ervoor dat we de data van het formulier kunnen ophalen via de req.file
(voor 1 file) en req.files
(voor meerdere files) variabele.
Vooraleer we met multer
kunnen werken, moeten we deze eerst installeren. Dit doen we door het volgende commando in de terminal uit te voeren:
Om multer
te kunnen gebruiken, moeten we eerst een multer
object aanmaken. Dit object bevat de configuratie van multer
.
De basisconfiguratie van multer
ziet er als volgt uit:
De dest
property van het multer
object bevat de map waar de bestanden terecht komen. In dit geval is dit de map uploads
. Als we dit niet opgeven, worden de bestanden in het geheugen opgeslagen en worden ze niet opgeslagen op de harde schijf.
By default, Multer zal de bestanden hernoemen zodat er geen conflicten ontstaan. Dus upload je twee keer een bestand met dezelfde naam, dan zal de tweede upload een andere naam krijgen.
Om een bestand te kunnen uploaden, moeten we de upload.single()
functie gebruiken. Deze functie heeft 1 parameter nodig: de naam van het inputveld waar het bestand in staat. In ons voorbeeld is dit het inputveld met de naam file
.
Dit is een voorbeeld van een formulier waarbij we een bestand kunnen uploaden. Als we dit formulier invullen en verzenden, dan wordt het bestand opgeslagen in de map uploads
. Omdat we aangegeven hebben dat de map uploads
een statische map is, kunnen we het bestand oproepen via de url http://localhost:3000/<bestandsnaam>
. In ons voorbeeld is dit bv http://localhost:3000/5f7b9b0e8b9c4a0b8c9d9e0f
.
Om meerdere bestanden te kunnen uploaden, moeten we de upload.array()
functie gebruiken. Deze functie heeft 2 parameters nodig: de naam van het inputveld waar de bestanden in staan en het aantal bestanden dat geüpload mag worden. In ons voorbeeld is dit het inputveld met de naam photos
en mogen er 5 bestanden geüpload worden.
nu kunnen we de bestanden ophalen via de req.files
variabele
Als je een formulier hebt met meerdere file inputs, dan moet je de upload.fields()
functie gebruiken. Deze functie heeft 1 parameter nodig: een array met objecten. Elk object bevat de naam van het inputveld en het aantal bestanden dat geüpload mag worden.
De types van de req.files
variabele zijn niet zo duidelijk. We kunnen de types van de req.files
variabele dus beter zelf definiëren. Dit doen we door een interface aan te maken met de naam FilesDictionary
. Deze interface bevat een index signature. Dit is een manier om een object te definiëren waarbij de keys van het object niet vooraf gekend zijn. In ons geval weten we niet hoeveel bestanden er geüpload worden. Daarom gebruiken we een index signature.
Hoewel het perfect mogelijk is om data te versturen via een GET
request, is het niet altijd de beste keuze. De data die je verstuurd via een GET
request is zichtbaar in de URL. Bovendien is de hoeveelheid data die je kan versturen beperkt. De meeste browsers hebben een limiet van 2048 karakters. Dit is niet altijd voldoende. Daarom gebruiken we POST
requests.
Bij een POST
request wordt de data niet zichtbaar in de URL. De data wordt verstuurd in de body van het request. De body is een onderdeel van het request dat gebruikt wordt om data te versturen. Je zou de body kunnen zien als een brief die je in een enveloppe steekt. Bij de GET
request zou je het bericht op de enveloppe schrijven, bij de POST
request steek je het bericht in de enveloppe. Beide zullen de bestemming bereiken, maar de inhoud van de enveloppe is niet direct zichtbaar voor iedereen.
Opgelet: Het is niet zo dat POST
requests veilig zijn. De inhoud van een POST
request is ook leesbaar. Als we teruggrijpen naar de analogie van de enveloppe, dan kan iedereen de enveloppe openen en de inhoud lezen. Als je echt zeker wil zijn dat de data veilig verstuurd wordt, moet je de data versleutelen. Dit is een proces dat we later in de cursus zullen behandelen.
GET
requests zijn ook niet geschikt voor operaties die een aanpassing teweegbrengen (zoals het versturen van een formulier). Dat komt onder meer omdat een paginarefresh zorgt dat het request opnieuw wordt uitgevoerd!
Over het algemeen worden POST
requests gebruikt om formulieren te versturen. Een formulier is een manier om data te versturen naar een server. Een formulier bestaat uit een aantal velden die ingevuld kunnen worden. Deze velden worden verstuurd naar de server wanneer de gebruiker op de knop "Verzenden" klikt. De data wordt verstuurd als een POST
request.
Zo"n formulier kan er als volgt uit zien:
Er zijn hier twee belangrijke dingen op te merken in dit voorbeeld:
Het attribuut action
bevat de URL waar de data naartoe gestuurd wordt. Dit is de route die we zullen gebruiken om de data te behandelen.
Het attribuut method
bevat de HTTP method die gebruikt wordt. In dit geval is dat post
.
Het proces om de data van een formulier te behandelen noemen we form handling. Dit is het proces waarbij de data van het formulier wordt opgevraagd en verwerkt. Hieronder een flowchart van het proces:
De meeste formulieren werken op deze manier.
De gebruiker krijgt een leeg formulier te zien bij de eerste keer dat de pagina wordt opgevraagd.
Het formulier kan lege velden bevatten (bv. als je een nieuw record aanmaakt) of het kan al ingevuld zijn met initiele waarden (bv. als je een record aanpast of nuttige standaardwaarden hebt).
De gebruiker vult dit formulier in en de data wordt verstuurd via een POST
request naar de url die in het action
attribuut van het form
element staat.
De server ontvangt de data en valideert en sanitizeert deze.
Validatie is het proces waarbij de data wordt gecontroleerd op correctheid.
Sanitization is het proces waarbij de data wordt schoongemaakt. Dit is belangrijk om bv. SQL injecties te voorkomen (zie later)
Als er data ongeldig is, wordt het formulier opnieuw getoond, maar nu met de ingevulde waarden en foutmeldingen voor de velden die niet correct zijn.
Als alle data geldig is, worden de nodige acties uitgevoerd (bv. de data wordt opgeslagen in de database, een notificatie email wordt verstuurd, het resultaat van een zoekopdracht wordt teruggegeven, een bestand wordt geupload, etc.)
Als alle acties zijn uitgevoerd, wordt de gebruiker doorgestuurd naar een andere pagina.
Vaak wordt form handling code geïmplementeerd met een GET route voor de initiële weergave van het formulier en een POST route naar dezelfde route voor de validatie en verwerking van de formuliergegevens.
Het eerste wat je moet doen is een route maken die het lege formulier toont. Dit is een GET
route die de HTML van het formulier teruggeeft.
Naast een GET
route, maken we ook een POST
route die de data van het formulier verwerkt. Die mag op dezelfde URL staan als de GET
route. Express weet welke functie uitgevoerd moet worden op basis van de HTTP method die gebruikt wordt.
Willen we de inhoud van de body van het POST
request lezen dan moeten we nog een kleine aanpassing maken aan onze applicatie. Bovenaan onze applicatie (bij de andere app.use
statements) voegen we de volgende regels toe:
De waarde voor limit
kies je zelf. Dit is de maximale grootte van het request. De tweede lijn zorgt ervoor dat de inhoud van de POST
omgezet wordt in een handig JSON object.
We kunnen nu de velden van het formulier uitlezen met req.body
.
Omdat je nooit kan vertrouwen op de data die je ontvangt, zelfs als er al in de browser zelf validatie is gebeurd, is het belangrijk om de data te valideren. Misschien hebben we een veld verplicht gemaakt, of willen we zeker zijn dat een emailadres een @
bevat.
En als er iets misloopt willen we ook een foutmelding tonen. Deze moeten we dan meegeven aan de hand van het object in de res.render
methode.
We moeten dan ook de error tonen in de view.
Vergeet ook geen lege error message mee te geven in de GET
route of we krijgen een error.
Als de data geldig is, kunnen we de nodige acties uitvoeren. Dit kan vanalles zijn: de data opslaan in een database, een notificatie email versturen, een bestand uploaden, etc. En vervolgens sturen we de gebruiker door naar een andere pagina aan de hand van een redirect.
Hier onder een formulier met een groot aantal mogelijke form elementen.
Deze kan je op de volgende manier uitlezen:
Twijfel je? Dan kan je altijd gewoon het req.body
object afprinten via console.log
en kijken wat erin zit.
Maak een nieuw project aan met de naam joke-api
.
Bij het opstarten van de applicatie worden eerst alle categorieën van grappen opgehaald. Die kan je op de volgende URL met fetch ophalen:
https://v2.jokeapi.dev/categories
Nadat je de categorieën hebt opgehaald, kan je de gebruiker vragen om een categorie te kiezen. Vervolgens wordt er de gebruiker gevraagd om het type van de grap te kiezen. De mogelijke types zijn: single
, twopart
.
Vervolgens wordt er een grap opgehaald van de gekozen categorie en type. Je kan de grappen ophalen op de volgende URL:
https://v2.jokeapi.dev/joke/<categorie>
/<type>
bv. https://v2.jokeapi.dev/joke/Programming/single
Let op dat de single
grappen een joke
veld hebben en de twopart
grappen een setup
en delivery
veld. Hou hier dus rekening mee bij het tonen van de grap.
Na het vertonen van de grap wordt de gebruiker gevraagd of hij nog een grap wil zien. Als de gebruiker ja
antwoordt, wordt de gebruiker opnieuw gevraagd om een categorie en type te kiezen. Als de gebruiker nee
antwoordt, wordt de applicatie afgesloten.
Maak een nieuw project bitcoin-api
waarin je jouw bronbestanden voor deze oefening kan plaatsen.
Bij het opstarten van de applicatie wordt er gevraagd welke valuta je wilt zien. De keuze bestaat uit de volgende valuta: EUR, USD, GBP. Vervolgens zal de applicatie de huidige prijs van bitcoin in de gekozen valuta tonen. Maak gebruik van de fetch
functie om de data op te halen.
Voor meer informatie over multer
, kan je terecht op de volgende pagina:
Er zijn ook libraries die je kan gebruiken om de validatie te vereenvoudigen. Een populaire library is .
Maak een nieuw project aan met een bestand at-least-two
met de volgende inhoud:
Schrijf een arrow functie isOdd
die deze interface implementeert die teruggeeft of een getal oneven is.
Schrijf een arrow functie isEven
die deze interface implementeert die teruggeeft of een getal even is.
Verzin twee andere functie's die deze interface implementeert.
Schrijf een arrow functie genaamd atLeastTwo
die twee argumenten aanvaard. Het eerste argument is een array van getallen en de tweede argument is een functie van het type TestFunction
Deze functie geeft true terug als minstens twee elementen voldoen aan de meegegeven functie.
Bijvoorbeeld:
geeft de volgende output:
Maak een nieuw project aan met de naam unix-timestamp-api
.
We willen een programma maken dat een unix timestamp omzet naar een leesbaar tijdsformaat. We zullen hiervoor gebruik maken van een REST API. Je kan de API aanroepen door een GET request te sturen naar https://helloacm.com/api/unix-timestamp-converter/?cached&s=
, gevolgd door de unix timestamp. De API zal een eenvoudige string terugsturen met daarin de omgezette tijd.
Je kan de API aanroepen met de fetch functie. Probeer deze opgave op te lossen met behulp van de async
en await
keywords. Let er wel op dat je de code in een async
functie moet plaatsen.
Maak een nieuw project aan met de naam school-api
.
We gaan in deze oefening een API gebruiken die het mogelijk maakt om alle hogescholen en universiteiten op te vragen van een bepaald land. De API die we hiervoor gaan gebruiken is http://universities.hipolabs.com/search?country=Netherlands (vervang Netherlands
door het land dat je wil opvragen).
De gebruiker krijgt eerst een keuze menu te zien met de landen waarvoor er data beschikbaar is. De landen mag je hardcoden in je applicatie.
Vervolgens krijgt de gebruiker een lijst te zien van alle hogescholen en universiteiten van het gekozen land. Je mag zelf kiezen hoe je deze data toont aan de gebruiker.
Na het tonen van de data wordt de gebruiker gevraagd of hij nog een land wil opvragen. Als de gebruiker ja
antwoordt, wordt de gebruiker opnieuw gevraagd om een land te kiezen. Als de gebruiker nee
antwoordt, wordt de applicatie afgesloten.
Maak een nieuw project aan met de naam cocktails-api
.
Maak gebruik van de cocktail API om de gebruiker een ingrediënt te laten opgeven. Vervolgens toon je alle cocktails waarin dit ingrediënt voorkomt.
Je kan de cocktails met een bepaald ingrediënt opvragen via de volgende URL:
Blijf cocktails opvragen tot de gebruiker een lege string opgeeft.
Voor het toevoegen van 1 element gebruiken we de functie insertOne
. Door een object mee te geven als parameter wordt dit object toegevoegd aan de database:
Aan de hand van de db
en collection
functies kunnen we de database en collectie selecteren waar we willen toevoegen. In dit geval voegen we een Pokemon object toe aan de collectie "pokemon".
Let op: elk object krijgt automatisch een _id wanneer die wordt toegevoegd aan de database. MongoDB kiest hier zelf een uniek id. Om later deze property te kunnen aanspreken, hebben we dit veld voorzien in de interface van Pokemon. We maken die echter optioneel zodat we die zelf geen waarde geven.
Wanneer we verschillende elementen willen toevoegen, gebruiken we insertMany
. Stel dat we een array van Pokemon objecten hebben:
dan kunnen we deze allemaal tegelijk toevoegen:
MongoDB laat toe verschillende types in 1 collectie toe te voegen. Stel dat we een array van objecten hebben met verschillende properties:
dan kunnen we deze toevoegen in 1 collectie:
Alhoewel dit mogelijk is, is dit niet altijd een goed idee. Het is beter om een duidelijke structuur te hebben in je collecties. Dit maakt het makkelijker om queries uit te voeren. Maar het is wel een van de voordelen van NoSQL databases.
In deze sectie gaan we leren hoe we data kunnen verwijderen uit een MongoDB database.
Er zijn twee methodes om data te verwijderen: deleteOne
en deleteMany
. Die werken op dezelfde manier als de updateOne
en updateMany
methodes.
Je kan de deleteOne
methode gebruiken om een document te verwijderen. Je moet een filter meegeven om het document te selecteren dat je wil verwijderen.
Dit zal het eerste document met de naam "John" verwijderen.
Je kan de deleteMany
methode gebruiken om meerdere documenten te verwijderen. Je moet een filter meegeven om de documenten te selecteren die je wil verwijderen.
Dit zal alle documenten verwijderen waarvan de leeftijd kleiner is dan 18. Je ziet dat we hier ook gebruik maken van query operators om de filter te maken.
Je kan ook de deleteMany
methode gebruiken zonder filter om alle documenten in de collectie te verwijderen.
Dit zal alle documenten in de collectie verwijderen. Dit is handig om te gebruiken in testen, maar wees voorzichtig in productie.
Tot nu toe hebben we enkel de MongoDB driver gebruikt in een Node.js applicatie. In dit hoofdstuk gaan we de MongoDB driver gebruiken in een Express.js applicatie. Over het algemeen is het gebruik van de MongoDB driver in een Express.js applicatie niet veel anders dan in een Node.js applicatie. We gaan echter wel enkele best practices bespreken.
Een aanpak die je kan gebruiken is om een connectie met de database te maken bij elke request die binnenkomt. Dit zorgt ervoor dat je altijd een verse connectie hebt met de database. Dit is echter niet de meest efficiënte manier van werken. Het is beter om een connectie te maken bij het opstarten van de applicatie en deze connectie open te houden zolang de applicatie draait. Die manier zullen we verder in dit hoofdstuk bespreken.
We gebruiken een try catch finally
blok om ervoor te zorgen dat de connectie met de database altijd wordt afgesloten. Dit is belangrijk omdat je anders een connectie lek kan krijgen. Dit kan ervoor zorgen dat je applicatie vastloopt of dat je database overbelast raakt.
Het is een goed idee om een aparte module (in een apart bestand) aan te maken waarin je al je database gerelateerde code plaatst. Dit zorgt ervoor dat je code beter georganiseerd is en dat je je database code makkelijker kan hergebruiken.
We zullen afspreken dat we deze module database.ts
noemen. In deze module plaatsen we alle code die gerelateerd is aan de database.
In dit voorbeeld maken we een connectie met de MongoDB database. We maken ook een functie exit
die we gebruiken om de connectie met de database te sluiten. We gebruiken de process.on
methode om een event listener toe te voegen voor het SIGINT
event. Dit event wordt getriggerd als je CTRL+C
drukt in de terminal. Zo zorgen we ervoor dat de connectie met de database netjes wordt afgesloten als je de applicatie stopt.
Vervolgens kunnen we deze module gebruiken in onze Express.js applicatie.
Het is een goed idee om de collections die je gebruikt in je applicatie te exporteren vanuit je database.ts
module. Dit zorgt ervoor dat je de collections makkelijk kan hergebruiken in andere modules.
Je kan zo alle collections exporteren die je nodig hebt in je applicatie.
Het is een goed idee om je database te vullen bij de opstart van je applicatie. Zo kan je ervoor zorgen dat je altijd data hebt om mee te werken. Deze code plaatsen we eveneens in de database.ts
module. We noemen deze functie seed
. Dit is afkomstig van het Engelse woord voor zaaien en betekent dat we de database vullen met data.
We kunnen deze functie aanroepen in de connect
functie.
Nu we de connectie met de database hebben gemaakt en de database hebben gevuld met data, kunnen we deze data ophalen in onze Express.js applicatie. Het enige wat we nu moeten importeren is de collection die we willen gebruiken.
en dan kunnen we deze collection gebruiken in onze Express.js routes.
We zouden eventueel ook de functies om data op te halen kunnen exporteren vanuit de database.ts
module. Dit zorgt ervoor dat we de code beter kunnen hergebruiken en blijft de code in onze routes overzichtelijk en leesbaar. Nog een voordeel hiervan is dat we de code later makkelijker kunnen testen.
en deze functie gebruiken in onze routes.
Hieronder vind je een voorbeeld van hoe je de code kan structureren in je Express.js applicatie.
Types module (types.ts
):
Database module (database.ts
):
Je Express.js applicatie (index.ts
):
Dit is een voorbeeld van hoe je de code kan structureren in je Express.js applicatie. Je kan deze structuur aanpassen naar eigen inzicht. Het belangrijkste is dat je de code goed organiseert en dat je de code herbruikbaar maakt. Zo kan je makkelijk nieuwe functionaliteit toevoegen en blijft je code overzichtelijk en leesbaar.
Tot nu toe hebben we de connectie string voor de MongoDB database hard gecodeerd in onze applicatie. Dit is echter geen goede manier van werken. Het is beter om de connectie string op te slaan in een environment variabele. Zo kan je de connectie string makkelijk aanpassen zonder dat je de code moet aanpassen.
Je kan meer over environment variabelen lezen in dit hoofdstuk.
Je maakt dan een .env
bestand aan in de root van je project en plaatst daar de connectie string in.
Vervolgens kan je de connectie string ophalen in je code met process.env.MONGO_URI
.
Tot nu toe hebben we enkel gewerkt met in-memory data. Dit wil zeggen dat we data in een array steken en deze array in het geheugen houden. Wanneer de server stopt, is deze data weg. Dit is niet handig wanneer we data willen bijhouden die we later willen opvragen. Daarom gaan we nu werken met een database. We zouden kunnen werken met een relationele database zoals MySQL, maar we gaan werken met MongoDB omdat dit beter aansluit bij de manier waarop we data bijhouden in TypeScript (JSON).
Door een database te gebruiken, kunnen we data bijhouden die we later kunnen opvragen. We kunnen data toevoegen, aanpassen, verwijderen en opvragen. Dit noemen we CRUD operaties (Create, Read, Update, Delete).
MongoDB is een NoSQL database. Hier zijn enkele verschillen tussen een relationele database en een NoSQL database:
Een relationele database:
data verspreid over tabellen
gestructureerde data
structuur moeilijk aan te passen
goed voor ingewikkelde queries
Een NoSQL database:
data wordt bijgehouden als "documents" / JSON
dynamische data
structuur is makkelijk aanpasbaar
goed voor simpele queries
Je kan echter de logica van relationele databases mappen op die van NoSQL:
een record in RDB komt overeen met een MongoDB"s document (JSON object)
een tabel in RDB komt overeen met MongoDB"s collection
_id
is unieke identifier (indexed) voor elk document net zoals je een ID met primary key zou toevoegen aan een relationele database tabel
Laten we een eenvoudig voorbeeld nemen van een tabel met persoonsgegevens:
Sven
Charleer
sven.charleer@ap.be
Andie
Similon
andie.similon@ap.be
In NoSQL stellen we dit als volgt voor:
Om MongoDB te gebruiken, moeten we uiteraard een MongoDB server hebben. Het is mogelijk om een MongoDB server te installeren op je eigen machine, maar dit is niet altijd handig. Daarom kan je gebruik maken van MongoDB Atlas. Dit is een cloud service die ons toelaat een MongoDB server te gebruiken zonder dat we deze zelf moeten installeren.
Om MongoDB Atlas te gebruiken, kan je een account aanmaken op https://www.mongodb.com/atlas en hier een database aanmaken. Je kan dan een connectie string genereren die je later kan gebruiken om te connecteren op de database.
Let zeker op dat je voor deze cursus een gratis cluster aanmaakt. Dit is voldoende voor onze doeleinden.
Als je applicatie in productie draait moet je uiteraard gebruik maken van een mongodb server die online staat (zoaals MongoDB Atlas). Maar tijdens het ontwikkelen kan je gebruik maken van een mongodb server die lokaal draait. Dit kan je doen door een mongodb server te installeren op je eigen machine of door gebruik te maken van een devcontainer die een mongodb server bevat.
Er is jammer genoeg geen standaard devcontainer die typescript en mongodb bevat. Maar dankzij de flexibiliteit van devcontainers kunnen we zelf een devcontainer maken die typescript en mongodb bevat. Dit is buiten de scope van deze cursus, dus we gaan gebruik maken van een voorgemaakte template.
Als je wil connecteren met een lokale mongodb server in een devcontainer, kan je gebruik maken van volgende connectie string: mongodb://localhost:27017
.
Om MongoDB te gebruiken in Visual Studio Code, kan je de MongoDB for VS Code extension installeren. Deze extension laat je toe om MongoDB databases te beheren vanuit VS Code.
Om deze extension te installeren, ga je naar de extensions tab in VS Code en zoek je naar "MongoDB for VS Code". Klik op install om de extension te installeren.
Als je gebruik maakt van MongoDB Atlas, kan je de connectie string die je daar hebt aangemaakt, gebruiken in de MongoDB for VS Code extension. Dit laat je toe om de database te beheren vanuit VS Code. Bij een lokale MongoDB server kan je de connectie string mongodb://localhost:27017
gebruiken.
Tot nu toe hebben we enkel gezien hoe we exacte waarden kunnen gebruiken om te filteren. MongoDB heeft ook een text search functionaliteit die je kan gebruiken om tekstuele velden te doorzoeken. Dit kan je gebruiken om bijvoorbeeld een zoekfunctionaliteit te maken.
Stel dat we een collectie hebben met de naam books
en we willen alle boeken vinden waarvan de titel het woord "MongoDB" bevat.
en
Deze collection bevat de volgende boeken:
Je kan reguliere expressies gebruiken om tekstuele velden te doorzoeken. Je kan een reguliere expressie meegeven aan de find
methode om te filteren op een bepaald patroon.
Dit geeft alle documenten terug waarvan het veld title
het woord "MongoDB" bevat.
Als je een case-insensitive zoekopdracht wil uitvoeren, dan kan je de i
vlag toevoegen aan de reguliere expressie.
Dit geeft alle documenten terug waarvan het veld title
het woord "MongoDB" bevat, ongeacht de case.
Wil je nu zoeken op een bepaalde variabelen, dan kan je de reguliere expressie dynamisch maken.
Je kan ook een text index aanmaken op een veld om te zoeken op tekst. Je kan een text index aanmaken door de createIndex
methode aan te roepen met als argument een object met als key het veld dat je wil indexeren en als value "text"
.
Dit zal een text index aanmaken op het veld title
. Je kan nu de $text
operator gebruiken om te zoeken op tekst.
Over het algemeen is het aanmaken van een text index en text search efficiënter dan het gebruik van reguliere expressies.
Dit geeft alle documenten terug waarvan het veld title
de woorden "MongoDB" en "database" bevat.
Text search is zelfs zo krachtig dat het het onderscheid kan maken tussen enkelvoud en meervoud.
zal ook het boek "MongoDB for Dummies" teruggeven want het bevat het woord "dummy" in het meervoud.
Text search houdt ook rekening met stopwoorden. Dit zijn woorden die vaak voorkomen en geen betekenis hebben. Deze worden genegeerd in de zoekopdracht.
Dit geeft geen resultaten terug omdat "the" een stopwoord is.
Als je wil zoeken op meerdere velden, dan kan je een text index aanmaken op meerdere velden.
De kans bestaat dat je een foutmelding krijgt omdat er al een index bestaat op het title veld. Je kan dit oplossen door eerst de index te verwijderen.
Dit zal alle indexen verwijderen.
Je kan nu zoeken op meerdere velden.
Dit geeft alle documenten terug waarvan het veld title
of summary
het woord "database" bevat.
Je kan ook de $language
optie meegeven aan de $text
operator om de taal van de tekst te specificeren.
Dit geeft alle documenten terug waarvan het veld title
of summary
het woord "konijnen" bevat in het Nederlands. En ja, het boek "Lief klein konijn" zal teruggegeven worden.
Wil je volledig taal onafhankelijk zoeken dan moet je de index aanmaken met de default_language
optie en deze op none
zetten.
Dit is ook handig als je meerdere talen in je collectie hebt en je geen rekening wil houden met stopwoorden en dergelijke.
Sessions zijn een manier om data te bewaren tussen verschillende requests. Eigenlijk doet het vrijwel hetzelfde als cookies, maar zonder dat de data op de client wordt opgeslagen. In plaats daarvan wordt er een unieke identifier naar de client gestuurd, waarmee de server de data kan ophalen. Deze identifier wordt in een cookie opgeslagen in de browser, maar de data zelf wordt op de server bewaard. Dit maakt het veiliger dan cookies, omdat de data niet kan worden aangepast door de client. Daarom dat sessies wel gepast zijn voor gebruik in een login-systeem.
In Express kan je gebruik maken van de express-session
package om sessies te gebruiken. Deze package zorgt ervoor dat er een sessie wordt aangemaakt voor elke client die de server bezoekt. Je kan deze package installeren met npm:
Het is aan te raden om alles van de sessie in een aparte file te zetten, zodat je het makkelijk kan importeren in je Express applicatie. Maak een nieuw bestand aan, bijvoorbeeld session.ts
, en zet daar de volgende code in:
De secret
optie is een string die wordt gebruikt om de sessie te beveiligen. Het is belangrijk dat deze string geheim blijft, omdat het wordt gebruikt om de sessie te versleutelen. Als iemand deze string weet, kan hij de sessie van een andere gebruiker overnemen. Daarom is het een goed idee om deze string in een environment variabele te zetten, zodat deze niet in de code staat.
De store
optie is een object dat de sessie data opslaat. In dit geval gebruiken we een MemoryStore
, wat betekent dat de data in het geheugen van de server wordt opgeslagen. Dit is handig voor ontwikkeling en debugging, maar niet geschikt voor productie. In productie gebruik je best een externe database, zoals MongoDB of Redis.
De resave
optie bepaalt of de sessie opnieuw moet worden opgeslagen als er geen veranderingen zijn. De standaardwaarde is true
.
De saveUninitialized
optie bepaalt of de sessie moet worden opgeslagen als er geen data in zit. De standaardwaarde is true
.
De maxAge
optie bepaalt hoelang de sessie geldig is. In dit geval is de sessie 1 week geldig. Als je deze optie niet instelt, is de sessie geldig tot de browser wordt gesloten.
Vervolgens moeten we deze sessie middleware toevoegen aan onze Express applicatie. Eerst importeren we de sessie middleware in ons app.ts
bestand:
en dan voegen we onze eigen session middleware toe aan de Express applicatie:
Als je nu je express server opstart en een request doet naar eender welke route, zal er een sessie worden aangemaakt voor de client. Je kan dit zien in de developer tools van je browser, waar je een cookie ziet met de naam connect.sid
. Deze cookie bevat de identifier van de sessie en zal worden gebruikt om de sessie te identificeren op de server. Deze wordt automatisch meegestuurd vanuit de browser bij elke request.
Vooraleer we data kunnen opslaan in een sessie moeten we definieren wat we willen opslaan. Als we bijvoorbeeld "username" willen bijhouden in de sessie, moeten we in ons session.ts
bestand de volgende code aanpassen:
Het voordeel van een sessie t.o.v. een cookie is hier dat je ook complexe objecten kan opslaan in de sessie, zoals arrays of objecten. Dus je hoeft deze niet te serialiseren naar een string.
Vervolgens kunnen we data opslaan in de sessie door de req.session
object te gebruiken:
Om de sessie data uit te lezen kunnen we hetzelfde object gebruiken:
Om data uit de sessie te verwijderen, kan je de property verwijderen uit het req.session
object:
De inhoud van de sessie wordt langs de serverzijde opgeslagen. Dit impliceert dat, met bovenstaande code, sessies geen herstart van de server kunnen overleven, want er is nergens een opslagmechanisme vermeld.
Het defaultmechanisme van express-session is een "in-memory store", dus tijdelijke opslag. Voor ontwikkeling en debugging kan dit handig zijn. In productie gebruik je een "session store". Dit is een externe database die de sessiegegevens opslaat. Er zijn verschillende session stores beschikbaar, zoals connect-redis
, connect-mongo
, connect-mysql
, enz. Deze moet je zelf installeren en configureren.
Wil je bijvoorbeeld mongodb gebruiken als session store, dan installeer je de package connect-mongodb-session
:
Wil je deze gebruiken, dan moet je de MemoryStore
vervangen door de MongoDBStore
:
Willen we nu het winkelkarretje van een gebruiker bijhouden op de server in plaats van in de browser, dan kunnen we dit doen met sessies. We kunnen in een sessie een array bijhouden van producten die de gebruiker heeft toegevoegd aan zijn winkelkarretje. Deze keer zullen we geen array van strings bijhouden, maar een array van objecten. Elk object zal een product voorstellen, met een naam en een prijs.
Het eerste wat we moeten doen is het product definiëren in een interface (bv in types.ts
):
In het session.ts
bestand moeten we deze interface importeren en toevoegen aan de SessionData
interface:
De rest van de code in session.ts
blijft hetzelfde. Vergeet deze niet te importeren en toe te voegen aan de Express applicatie in index.ts
.
In het index.ts
bestand definieren we nu een array van producten die we later kunnen toevoegen aan ons winkelkarretje:
De code om dan een product toe te voegen aan het winkelkarretje ziet er als volgt uit:
Een veel voorkomende vorm van web applicatie is een CRUD applicatie. CRUD staat voor Create, Read, Update en Delete. Dit zijn de vier basis operaties die je kan uitvoeren op een database. Eigenlijk is elke eenvoudige admin dashboard een CRUD applicatie. In dit artikel gaan we een eenvoudige CRUD applicatie maken met MongoDB en Express. De initiele data is afkomstig van https://jsonplaceholder.typicode.com/users
wat een lijst van gebruikers bevat. We willen dus een applicatie maken waar we gebruikers kunnen toevoegen, verwijderen, updaten en bekijken.
We gaan geen rekening houden met error afhandeling in dit deel van de cursus. We gaan er vanuit dat alles goed gaat. In een productie omgeving is het belangrijk om error afhandeling te voorzien.
We gaan eerst de JSON data vanuit de API inladen in onze MongoDB database. We plaatsen de volgende code in een nieuw bestand database.ts
.
En we roepen de connect
functie aan in ons index.ts
bestand.
Bij het opstarten van de server gaan we de connectie maken met de database en de data inladen vanuit de API. We gaan de data enkel inladen als de database leeg is. We gaan de data inladen in de users
collectie van de exercises
database.
We gaan nu de data lezen vanuit de database en tonen op de webpagina. We gaan de volgende code toevoegen aan ons index.ts
bestand. Eerst importeren we de getUser
functie vanuit de database.ts
bestand.
en maken we een index.ejs
bestand aan in de views/users
map. We zorgen ook voor een partials
map in de views
map en maken een header.ejs
en footer.ejs
bestand aan.
We tonen de gebruikers in een lijst op de webpagina. We tonen de naam, gebruikersnaam, email, adres, telefoonnummer, website en bedrijfsnaam van de gebruiker.
We maken nu een nieuw formulier aan om een gebruiker toe te voegen en plaatsen deze code in een nieuw bestand views/users/create.ejs
.
We voorzien hier voor elk veld een input veld. We gebruiken een fieldset
element om de adres en bedrijfsgegevens te groeperen. Door de naam van de input velden te voorzien van address[street]
en company[name]
kunnen we deze gegevens later makkelijk groeperen in een object. Als je dan de body van de request bekijkt in de Express server, dan zie je dat de address
en company
gegevens in een object komen te staan. We gaan nu twee routes aanmaken om de data te verwerken: een GET route om het formulier te tonen en een POST route om de data te verwerken.
We doen op het einde van de POST route een redirect naar de gebruikerslijst. Dit zorgt ervoor dat de gebruiker na het toevoegen van een gebruiker terug naar de gebruikerslijst gaat. We voorzien hier nog geen error afhandeling.
We gaan nu de createUser
functie toevoegen aan ons database.ts
bestand.
We voorzien hier ook een getNextId
functie die het volgende id ophaalt uit de database. We sorteren de gebruikers op id in aflopende volgorde en nemen de eerste gebruiker. Als er geen gebruikers zijn dan geven we 1 terug. We voegen dan 1 toe aan het id en geven dit terug. We gaan nu de gebruiker toevoegen aan de database. Vergeet de createUser
functie niet te importeren in het index.ts
bestand.
We gaan nu een button toevoegen aan de gebruikerslijst om een gebruiker te verwijderen. We voegen de volgende code toe aan het index.ejs
bestand.
Er wordt dus voor elke gebruiker in de lijst een formulier aangemaakt met een button om de gebruiker te verwijderen. Dit doen we omdat we geen POST requets kunnen doen vanuit een anchor tag. Merk ook op dat we hier een POST gebruiken en geen DELETE. Dit is omdat we geen DELETE requests kunnen doen vanuit een formulier. We gaan nu de route aanmaken om de gebruiker te verwijderen.
We voorzien een nieuwe route in het index.ts
bestand.
We gaan nu de deleteUser
functie toevoegen aan ons database.ts
bestand.
We gaan nu de gebruiker verwijderen uit de database. Vergeet de deleteUser
functie niet te importeren in het index.ts
bestand.
We kunnen nu de code van het create formulier hergebruiken om een update formulier te maken. We maken een nieuw bestand views/users/update.ejs
aan.
We zorgen ervoor dat de input velden gevuld zijn met de huidige waarden van de gebruiker en dat we als action de update route gebruiken. We gaan nu de route aanmaken om de gebruiker te updaten.
We voorzien weer twee routes in het index.ts
bestand. Een GET route om het formulier te tonen en een POST route om de data te verwerken.
We redirecten ook hier op het einde van de POST route naar de gebruikerslijst. We gaan nu de getUserById
en updateUser
functies toevoegen aan ons database.ts
bestand.
Vergeet de getUserById
en updateUser
functies niet te importeren in het index.ts
bestand.
Testen van applicaties gebeurt op verschillende niveaus. Hoewel niet iedereen dezelfde niveaus van elkaar onderscheidt, maakt men in het algemeen een onderscheid tussen unit testing en end-to-end testing.
Unit testing omvat het testen van individuele onderdelen van de code, zoals functies of methoden. Meestal wordt hier een white-box principe gehanteerd: de tester kent de inhoud van de unit en mag code schrijven die gebruik maakt van deze kennis. Typische frameworks voor unit testing van Express applicaties zijn Mocha en Jest.
End-to-end testing omvat het testen "zoals een gebruiker". Deze vorm volgt het black-box principe. In essentie omvat dit het automatiseren van volledige browserinteracties. Typische frameworks zijn Cypress of Selenium.
Jest is een testframework dat origineel ontwikkeld werd door Facebook. Het is een van de meest populaire testframeworks voor JavaScript. Jest is een all-in-one oplossing die zowel de testrunner als de assertion library bevat. Jest is zeer eenvoudig in gebruik en heeft een goede documentatie.
Om Jest te installeren, voer je volgend commando uit:
Om Jest te kunnen gebruiken (met TypeScript), voer je dit commando uit:
Om te zorgen dat je al je Jest-tests kan laten lopen met npm test, voeg je dit toe aan package.json:
Om een bepaalde functie te kunnen testen, moet je deze functie exporteren. Daarom is het belangrijk om zoveel mogelijk modules te gebruiken die je kan exporteren.
Stel dat je een functie hebt die een string omzet naar hoofdletters in een bestand string-utils.ts
:
Om deze functie te testen, maak je een bestand string-utils.test.ts
:
it
is een functie die een test definieert. De eerste parameter is een beschrijving van de test, de tweede parameter is een functie die de test uitvoert. Je kan ook test
gebruiken in plaats van it
.
We kunnen nu de tests uitvoeren met npm test
. We krijgen dan volgende output:
Jammer genoeg is hier de tester hier niet in geslaagd om de bug te vinden. De functie toUpperFunction
is namelijk niet correct. Als de input speciale tekens bevat zoals de duitse karacters met umlauten, dan zal de functie deze niet omzetten naar hoofdletters. De volgende test zou dit kunnen aantonen:
Deze test zal falen. De correcte implementatie van de functie zou zijn:
Als je een functie hebt die een exception kan gooien, kan je dit testen met toThrow
:
We kunnen deze nu testen met:
Let op dat we hier een arrow functie gebruiken om de functie calculateSquareRoot
op te roepen. Dit is nodig omdat we anders de exception niet zouden kunnen opvangen en de test zou falen.
Als je een functie hebt die asynchroon werkt, kan je dit testen met async
en await
:
We kunnen deze nu testen met:
Als je bepaalde code wil uitvoeren voor en na elke test, kan je dit doen met beforeEach
, afterEach
, beforeAll
en afterAll
. Deze kunnen zich in de describe
blokken bevinden of globaal in het bestand.
Dit wordt gebruikt om bijvoorbeeld een database connectie te openen en te sluiten voor en na elke test.
Als we een Express applicatie willen testen, kunnen we gebruik maken van de supertest
library. Deze library maakt het mogelijk om HTTP requests te versturen naar een Express applicatie en de response te testen.
We moeten deze dan ook nog installeren:
Stel dat we een Express applicatie hebben die een GET request afhandelt op de route /hello
:
Let wel op dat we nu wel de app moeten exporteren. Dit is nodig om de app te kunnen testen.
We kunnen deze nu testen met:
Als je deze test nu uitvoert met npm test
, dan krijg je de volgende error:
Om dit op te lossen kunnen we de app code in een apart bestand zetten en de code in index.ts
aanpassen:
en de rest van de code in app.ts
:
Als je een route hebt die query parameters verwacht, kan je deze testen met:
en de test:
Als je een route hebt die POST requests afhandelt, kan je deze testen met:
en de test:
Als je een route hebt die HTML responses teruggeeft, kan je deze testen met:
en de test:
of je kan de HTML parsen met node-html-parser
en dan de inhoud van de h1 tag testen:
Jest kan ook gebruikt worden om de code coverage te berekenen. Dit is het percentage van de code dat door de tests gedekt wordt. Hoe hoger dit percentage, hoe beter je code getest is. Eerst moet je wel in je package.json
de volgende lijn toevoegen bij de scripts.
Nu kan je de coverage berekenen met npm run coverage
. Je krijgt dan een overzicht van de coverage van je code.
Je krijgt een uitgebreid overzicht van welke lijnen er wel en niet getest zijn. Dit kan je helpen om te zien welke delen van je code nog niet getest zijn en waar je nog extra tests moet schrijven. Je kan dit verslag vinden in de map coverage/lcov-report/index.html
.
Unit testen wordt vaak lastiger wanneer je code interageert met "de buitenwereld": filesystemen, databanken, invoer van de gebruiker, uitvoer naar de terminal, externe servers,...
Om deze reden wordt vaak gebruik gemaakt van "mocks": waarden die de plaats innemen van onderdelen die het moeilijk maken om unit testen te schrijven. Deze leveren vooraf vastgelegde data af eerder dan de echte handelingen uit te voeren. Achteraf kunnen we ook controleren dat deze gebruikt zijn zoals verwacht. Dit past binnen het black box principe dat gehanteerd wordt voor unit testen. Jest bevat ingebouwde functionaliteit voor het maken van mocks.
We hebben gekozen om onze database altijd in een aparte module te steken die onze collection exporteert. Dit maakt het makkelijk om deze te mocken. We gaan hierbij gebruik maken van de spyOn
functie van Jest om de functies van de database module te mocken.
De spyOn
functie maakt een mock van de find
functie van de collection
module. We geven aan dat deze mock de toArray
functie moet teruggeven met de waarde mockPets
. We controleren dan of de find
functie van de collection
module aangeroepen is met de juiste parameters.
We gebruiken fetch om requests op externe services te doen. Omdat dit iets is dat je vaak wil mocken (om te vermijden dat netwerkstoringen testen doen falen, om te vermijden dat je API-limieten bereikt,...) is hier speciale ondersteuning voor.
We installeren eerst fetch-mock-jest (als development dependency).
De clientcode:
De testcode:
Om te vermijden dat andere operaties die fs.readFile nodig hebben niet fout lopen, moeten we zorgen dat de mock enkel in deze testfunctie gebruikt wordt. Daarom voegen we in de testfile deze regel toe:
Als we dit buiten de describe-blokken doen, gebeurt dit na elke test.
Maak een nieuw project aan met de naam hello-name
en installeer de readline-sync module.
We willen een programma maken dat de naam van de gebruiker vraagt en vervolgens "Hello, !" toont.
Maak een nieuw project aan met de naam wisselgeld
.
We gaan in deze oefening een programma maken dat een bedrag moet omzetten naar het kleinste aantal briefjes en munten. De gebruiker zal een bedrag moeten ingeven en de applicatie zal vervolgens het aantal briefjes en munten tonen.
Je kan dit doen door gebruik te maken van de modulo operator. Deze operator geeft de rest van een deling terug.
Maak een nieuw project aan in de labo2
directory met de naam bmi-calculator-multi
.
We gaan de applicatie uitbreiden zodat we de BMI van meerdere personen kunnen berekenen. De gebruiker zal een lijst van personen moeten ingeven. Voor elke persoon zal hij zijn gewicht en lengte moeten ingeven. De applicatie zal vervolgens de BMI van elke persoon berekenen en tonen.
Maak een nieuw project aan met de naam bmi-calculator
.
We gaan in deze oefening een BMI calculator maken. De gebruiker zal zijn gewicht en lengte moeten ingeven en de applicatie zal de BMI berekenen en tonen.
Het gewicht wordt ingegeven in kilogram en de lengte in meter.
De gebruiker zal zijn gewicht en lengte moeten ingeven. Dit kan je doen door gebruik te maken van de readline
module. Deze module laat toe om input te lezen van de gebruiker.
Zorg ervoor dat je de BMI afrondt op 2 cijfers na de komma.
Maak een nieuw project aan met de naam interest-calculator
.
We gaan in deze oefening een interest calculator maken. De gebruiker zal een bedrag en een interest percentage moeten ingeven. De applicatie zal vervolgens het bedrag na 1 jaar, 2 jaar en 5 jaar tonen.
Het totaal bedrag kan berekend worden met de volgende formule:
Maak een nieuw project aan met de naam uren-en-minuten
.
We gaan in deze oefening een programma maken dat een aantal minuten moet omzetten naar uren en minuten. De gebruiker zal een aantal minuten moeten ingeven en de applicatie zal vervolgens het aantal uren en minuten tonen.
Je kan dit doen door gebruik te maken van de modulo operator. Deze operator geeft de rest van een deling terug.
Maak een nieuw project aan met de naam tic-tac-toe
.
We willen een programma maken dat het spelletje Tic Tac Toe kan spelen. We gaan dit doen met een 2D array. We gaan het spel spelen met 2 spelers. De eerste speler zal altijd "X" zijn en de tweede speler "O".
We werken met een 2D array van 3x3. We gaan het spel spelen met de coordinaten van de array. De bovenste rij is 0, de middelste rij 1 en de onderste rij 2. De meest linkse kolom is 0, de middelste kolom 1 en de meest rechtse kolom 2.
De gebruiker geeft de coordinaten in in de vorm van rij,kolom. Dus bijvoorbeeld 0,0 is de bovenste rij en de meest linkse kolom. 2,2 is de onderste rij en de meest rechtse kolom.
Als de gebruiker een zet doet op een plaats waar al een zet is gedaan dan krijgt hij een melding en moet hij opnieuw een zet doen.
Als de gebruiker een zet doet op een plaats die niet bestaat dan krijgt hij een melding en moet hij opnieuw een zet doen.
Als de gebruiker een zet doet die geldig is dan wordt het bord getoond. Als er een winnaar is dan wordt dit getoond en het programma stopt. Als het bord vol is en er is geen winnaar dan wordt dit getoond en het programma stopt.
Maak een nieuw project aan met de naam name-from-email
.
We gaan in deze oefening een programma maken dat de naam van een email adres moet tonen. De gebruiker zal een email adres moeten ingeven en de applicatie zal vervolgens de naam tonen. De naam wordt hierbij als volgt geformatteerd: <eerste (hoofd)letter van voornaam>. <achternaam>
.
Je mag er van uitgaan dat het email adres altijd correct ingegeven wordt. Vervolgens zal de applicatie vragen of de gebruiker nog een email adres wil ingeven (adhv keyInYNStrict
).
Maak een nieuw project aan met de naam pokemon
Gegeven is de volgende array van 20 pokemon:
Maak een variabele team
van het type string[]. Deze array bevat de pokemon van de gebruiker van het programma.
Gebruik een lus om de pokemon te tonen aan de gebruiker. Toon eerst de index gevolgd door de naam van de pokemon. Je gebruikt dus nog NIET de ingebouwde keyInSelect
van de readline-sync library.
Vraag daarna aan de gebruiker welke pokemon er moet toegevoegd worden aan het team. Dit doe je aan de hand van de index van de pokemon. Dit doe je tot de gebruiker STOP ingeeft. Je kan dit doen aan de hand van een do while
loop.
Als de gebruiker een pokemon ingeeft die al in het team zet dan krijgt hij hiervan een melding en wordt de pokemon niet toegevoegd:
Als de pokemon niet bekend is (dus het ingegeven nummer groter is dan de lengte van de array) wordt er ook een melding gegeven:
Als je STOP hebt ingegeven dan wordt het team van de gebruiker op het scherm getoond:
Voorbeeld interactie:
Maak een nieuw project met de naam todo-list-objects
. Gebruik de vorige todo list als basis.
Je maakt eerst een interface voor het Todo
object. Dit bevat een
id (number)
title (string)
completed (boolean)
Bij het opstarten van het programma laad je de todos in vanuit een bestand todos.json
.
Voor de rest moet de functionaliteit hetzelfde zijn als de vorige todo list maar deze keer gebruik je geen 2 arrays van strings meer maar een array van Todo
objecten.
Maak een nieuw project aan met de naam movies-objects
.
Maak een JSON bestand movie.json
met de volgende inhoud:
Maak een interface voor het bovenstaande Movie object en lees het in aan de hand van een import
statement. Ken deze toe aan een variabele movie
en print deze af.
Maak een tweede variable aan myfavoritemovie van het type Movie en geef die een object mee die de info over jouw favoriete film bevat en print deze af.
Maak een derde variable aan myworstmovie van het type Movie en geef die een object mee die de info over jouw meest gehate film bevat en print deze af.
Maak een nieuw project aan met de naam array-sum
.
Maak een nieuwe functie sum
die de som van alle getallen in een array berekent. Gebruik hiervoor een for loop en probeer ook eens de reduce
functie van een array.
als je de volgende array meegeeft aan de functie:
dan moet de functie 15 teruggeven want 1 + 2 + 3 + 4 + 5 = 15.
Bekijk voor het labo aan te vangen eerst de volgende topics:
Maak een nieuw project aan met de naam todo-list-string
.
We willen een programma maken dat een todo lijst bijhoudt. De gebruiker kan taken toevoegen aan de lijst. De gebruiker kan ook taken op de lijst afvinken. Voorzie bij het opstarten van het programa twee arrays. Eén voor de taken en één voor de taken die afgevinkt zijn. Een taak is gewoon een string.
Bij het opstarten van de applicatie wordt er een menu getoond. De gebruiker kan kiezen uit de volgende opties:
Gebruik hiervoor de keyInSelect
functie van de readline-sync library.
Als de gebruiker kiest voor "Add a task" dan kan hij een taak toevoegen aan de lijst. De taak wordt toegevoegd aan de array van de taken.
Als de gebruiker kiest voor "Show tasks" dan worden de taken getoond. De taken die afgevinkt zijn worden getoond met een "X" ervoor. De taken die nog niet afgevinkt zijn worden getoond met een " " ervoor. Dit zal er als volgt uitzien:
Als de gebruiker kiest voor "Check a task" dan kan hij een taak afvinken. De gebruiker geeft het nummer van de taak in die hij wil afvinken. De taak wordt dan verplaatst van de array van de taken naar de array van de afgevinkte taken.
Als de gebruiker kiest voor "Exit" dan stopt het programma.
Deze oefening gaat verder op de Movies oefening.
Maak 3 functies aan en probeer deze uit:
de functie wasMovieMadeInThe90s
:
met de parameter movie van het type Movie
met return waarde true als de film in de jaren 90 gemaakt is, anders false
print of de film The Matrix in de jaren 90 gemaakt is adhv deze functie
de functie averageMetaScore
met de parameter movies die een array van het type Movie bevat
met return waarde de gemiddelde score van alle films in die array
print het gemiddelde van metascore van de 3 films adhv deze functie
de functie fakeMetaScore
met de parameters
movie van het type Movie
newscore die een nieuwe score bevat
met return waarde een nieuw Movie object met de nieuwe score
Maak een nieuw project aan met de naam math-fun
.
Schrijf de volgende functies:
add
die twee getallen optelt
subtract
die twee getallen van elkaar aftrekt
multiply
die twee getallen met elkaar vermenigvuldigt. Zorg voor een default waarde van 1 als de tweede parameter niet meegegeven wordt.
divide
die twee getallen deelt. Zorg voor een default waarde van 1 als de tweede parameter niet meegegeven wordt.
Zorg dat je deze kan schrijven met het function keyword en met een arrow function.
Gebruik deze functies om de volgende berekening uit te voeren:
Print het resultaat van de berekening af.
Maak een nieuw project cocktails-promise-all
waarin je jouw bronbestanden voor deze oefening kan plaatsen.
Maak gebruik van Promise.all
om de drie volgende cocktails via de cocktail api met de volgende ids in te lezen: 11000, 11001, 11002 en vervolgens de naam van de drie cocktails op het scherm te laten zien.
Je kan een cocktail via een id via de volgende api call binnenhalen:
Maak een nieuw project aan met de naam slow-sum
waarin je jouw bronbestanden voor deze oefening kan plaatsen.
Plaats de onderstaande code in een bestand index.ts
Dit zijn 2 functies die een promise terug geven. Ze simuleren een trage som functie en een trage vermenigvuldigings functie.
Roep de slowSum
functie aan met de getallen 1 en 5 en zorg dat ze het resultaat van deze functie op het scherm laat zien. (zie output)
Roep de slowSum
functie opnieuw aan met de getallen 1 en 5 maar zorg deze keer dat na het optellen de vermenigvuldigings functie `slowMult
wordt aangeroepen dat het resultaat vermenigvuldigd met 2 en dan op het scherm laat zien. (zie output)
Maak een eigen slowDiv
functie dat een deling doet (laat deze 2000 milliseconden duren). Zorg ervoor dat als je een deling door nul doet dat je de promise afkeurt met de melding "You cannot divide by zero".
Roep deze functie aan met de getallen 6 en 3 en laat het resultaat op het scherm zien. (zie output)
Roep deze functie aan met de getallen 6 en 0 en laat de error op het scherm zien. (zie output)
Maak een kopie van het index.ts
oefening en noem deze index_async.ts
Gebruik nu async/await in plaats van promises.
Bekijk voor het labo aan te vangen eerst de volgende topics:
Maak een nieuw project aan met de naam filter-numbers
.
Schrijf een functie filterPositive
die een array van getallen als parameter verwacht. De functie filterPositive
moet een nieuwe array teruggeven met enkel de positieve getallen uit de array die als parameter werd meegegeven. Deze functie MOET aan de hand van een for
-loop geschreven worden en mag geen gebruik maken van de ingebouwde functie filter
van een array.
Roep de functie filterPositive
aan met de volgende array als parameter:
Schrijf een functie filterNegative
die een array van getallen als parameter verwacht. De functie filterNegative
moet een nieuwe array teruggeven met enkel de negatieve getallen uit de array die als parameter werd meegegeven.
Roep de functie filterNegative
aan met de volgende array als parameter:
Schrijf een functie filterEven
die een array van getallen als parameter verwacht. De functie filterEven
moet een nieuwe array teruggeven met enkel de even getallen uit de array die als parameter werd meegegeven.
Roep de functie filterEven
aan met de volgende array als parameter:
Schrijf nu een functie filter
die een array van getallen als eerste parameter verwacht en een functie als tweede parameter. De functie filter
moet een nieuwe array teruggeven met enkel de getallen uit de array die als eerste parameter werd meegegeven waarvoor de functie die als tweede parameter werd meegegeven true
teruggeeft.
Herschrijf de functies filterPositive
, filterNegative
en filterEven
door gebruik te maken van de functie filter
.
Voorbeeld van gebruik:
Maak een nieuw project aan met de naam math-module
. Maak een nieuwe file aan met de naam math.ts
. In deze file maak je een module aan met de naam MathModule
. Deze module bevat de volgende functies:
add(a: number, b: number): number
- Deze functie accepteert twee getallen en geeft de som van deze getallen terug.
subtract(a: number, b: number): number
- Deze functie accepteert twee getallen en geeft het verschil van deze getallen terug.
multiply(a: number, b: number): number
- Deze functie accepteert twee getallen en geeft het product van deze getallen terug.
divide(a: number, b: number): number
- Deze functie accepteert twee getallen en geeft het resultaat van de deling van deze getallen terug.
Exporteer deze functies en importeer deze in een nieuwe file met de naam index.ts
. In deze file maak je gebruik van de functies die je hebt geëxporteerd. Test of de functies werken door de resultaten van de functies te loggen naar de console.
Na hoeveel tijd zal deze code "done!" op het scherm tonen? Voer de code dus niet uit maar denk even zelf na.