# 3. Route handlers In deze les bespreken we hoe we API-routes kunnen toevoegen aan een Next.js project en hoe we deze kunnen beveiligen met CORS. Tijdens deze les gebruiken we geen authenticatie, in hoofdstukken [4](/lessen/backend/lecture4.md) en [5](/lessen/backend/lecture5.md) voegen we authenticatie en autorisatie toe met respectievelijk cookies en JSON web tokens. Onderstaande startbestanden bevatten twee mappen, in de _nextApp_ map vind je een Next.js applicatie waarin we een API uitbouwen, in de _corsDemo_ map vind je een Vite applicatie die we gebruiken om CORS te demonstreren. ## Voor- en nadelen van een API Zoals we vorige lessen gedemonstreerd hebben, kan een Next applicatie perfect gebouwd worden zonder dat een API nodig is. Door de hechte integratie tussen server components, client components en server functions moeten we voor de meeste applicaties geen expliciete API voorzien. Next doet dit natuurlijk wel impliciet door elke server function om te vormen naar een HTTP POST endpoint.[^next15] [^next15]: Sinds versie 15.0.0 vormt Next enkel de server functions die ook daadwerkelijk geïmporteerd worden in een client component om naar een HTTP POST endpoint. Op deze manier wordt de bundle size[^bundle] kleiner en zijn er minder functions beschikbaar die via HTTP aangeroepen kunnen worden, wat de kans op een beveiligingslek verkleint. Daarbovenop randomiseert Next de id's die een function identificeren tussen builds op een niet-deterministische manier. Dit maakt het moeilijker voor een aanvaller om een actie te identificeren en aan te roepen. Server functions zijn nog steeds API endpoints en moeten ook dusdanig beveiligd worden, zie [hoofdstuk 4](/lessen/backend/lecture4.md) [^bundle]: De bundle size is de grootte van de JavaScript, HTML en CSS-code die naar de client gestuurd wordt in een productieomgeving. Hoe groter deze is, hoe langer het duurt voor de client de applicatie kan gebruiken. Het is duidelijk dat we dus niet zomaar naar een API-route mogen grijpen in een modern Next project. Er zijn echter wel enkele situaties waarin dit toch nuttig kan zijn: 1. Gebruikers moeten de mogelijkheid hebben om hun data programmatorisch aan te spreken en te gebruiken in hun eigen applicaties. 2. Naast de Next applicatie moet er ook een mobiele applicatie of een desktop applicatie voorzien worden die dezelfde data gebruikt. 3. Er zijn routes nodig die geen HTML-pagina's teruggeven, bijvoorbeeld een route die een PDF genereert, een route die een JSON dump van de gebruikersdata teruggeeft (GDPR), ... Twee concrete voorbeelden zijn de [https://gitpub.pit-graduaten.be/api/public/raw/3f161108-5890-4777-b788-3849c5238c1c?file=/lecture3.md](https://gitpub.pit-graduaten.be/api/public/raw/3f161108-5890-4777-b788-3849c5238c1c?file=/lecture3.md) en [https://gitpub.pit-graduaten.be/api/public/raw/3f161108-5890-4777-b788-3849c5238c1c?file=/lecture3.md&download=true](https://gitpub.pit-graduaten.be/api/public/raw/3f161108-5890-4777-b788-3849c5238c1c?file=/lecture3.md&download=true) routes waarmee je de raw (niet geformatteerde) inhoud van deze les respectievelijk kunt bekijken en downloaden. Er zijn dus verschillende use-cases voor een API-route, in de rest van de les focussen we ons op optie 1 en 2 uit voorgaande lijst, desondanks kan dezelfde manier van werken eenvoudig overgedragen worden naar een route die onder optie 3 valt. In dat geval moeten enkel de headers aangepast worden zodat de browser het request correct afhandelt. Aangezien dat we in de eerste twee situaties de volledige data van de applicatie moeten beheren, is het belangrijk dat we deze routes goed structureren. Hiervoor gebruiken we REST. ## REST Een REST (**re**presentational **s**tate **t**ransfer) API is een architecturaal patroon voor web API's. Het is geen framework, programmeertaal, standaard, of protocol en kan dus geïmplementeerd worden in elke programmeertaal. Verder zijn er ook geen limieten op de structuur van de teruggegeven data, deze kan als JSON, HTML, Plain Text, XML, ... teruggegeven worden. Al is JSON wel veruit het meest populaire formaat. Omdat REST geen echte standaard is, zijn de vereisten voor een REST API ook niet sterk afgebakend. De originele thesis die REST beschrijft onderstaande vereisten[^restSources]: [^restSources]: Bronnen: [https://www.ibm.com/topics/rest-apis](https://www.ibm.com/topics/rest-apis), [https://blog.postman.com/rest-api-examples/](https://blog.postman.com/rest-api-examples/), [https://aws.amazon.com/what-is/restful-api/](https://aws.amazon.com/what-is/restful-api/) * **Uniforme interface**: * Alle API requests voor een resource (e.g. een rij in een tabel of een document in een document database) geven data op dezelfde manier terug. Dit wil zeggen dat de volledige API ofwel in JSON-formaat ofwel in XML-formaat (of nog iets anders) aangeboden kan worden, maar geen mix van verschillende formaten. Het gekozen formaat hoeft niet overeen te komen met het formaat dat intern door de server gebruikt wordt. * De resource die teruggegeven wordt door de API bevat alle nodige informatie om de resources aan te passen of te verwijderen. * Elke resource is uniek en kan met één URL geïdentificeerd worden. * [Hypermedia as the engine of application state (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS): De resources moeten een overzicht geven van de andere beschikbare resources aan de hand van hyperlinks. Net zoals bij een browser moet er, bij de een correcte HATEOAS implementatie slechts één URL gekend zijn, de andere kunnen bezocht worden door "door te klikken" in het antwoord dat de server geeft op een request voor de root-url. HATEOAS wordt echter weinig gebruikt in productie omdat de meeste API's bedoeld zijn voor programmeurs en CRUD-operaties moeten ondersteunen. Omdat er op verschillende pagina's in de client-applicatie verschillende acties ondersteund moeten worden en omdat deze pagina's rechtstreeks bezocht kunnen worden (in de plaats van via de root pagina te gaan), moet de applicatie de URL's van de API sowieso kennen. Voor een read-only API kan HATEOAS wel nuttig zijn. Een goed voorbeeld hiervan is de [Star Wars API](https://swapi.dev/api). * **Client-server met zwakke coupling[^coupling]**: De client en server moeten zo weinig mogelijk van elkaar weten, de client en server kunnen onafhankelijk van elkaar ontwikkeld worden en communiceren enkel via HTTP(S). [^coupling]: Loose coupling is een principe van goed software ontwerpt. Twee klassen (of in dit geval de server en de client) moeten zo weinig mogelijk van elkaar weten. Eén van de twee klassen (de client) kan de andere (de server) aanpreken en gebruiken, maar dit mag absoluut geen wederzijdse connectie zijn. Het aanpreken van de andere klasse mag ook niet steunen op kennis van de interne werking van de klasse die aangesproken wordt, maar mag enkel afgaan op de publieke API van de aangesproken klassen. Voor meer informatie verwijzen we door naar de [Wikipedia pagina voor het derde GRASP principe](https://en.wikipedia.org/wiki/Loose_coupling). * **Statelessness**: Elk request van de client naar de server moet alle informatie bevatten om het request correct af te handelen. De server mag geen data bewaren over de state op de client, er zijn dus geen server-side sessions. * **Caching**: Waar mogelijk moeten resources gecached worden. Elke antwoord op een request moet informatie bevatten die aangeeft of een resource client-side gecached mag worden of niet. * **Layered system architecture**: Noch de client, noch de server mag ervan uitgaan dat de communicatie tussen client en server rechtstreeks gebeurd. Deze communicatie kan eventueel via een derde partij gaan. Zo is het bijvoorbeeld mogelijk dat een verzoek als volgt afgehandeld wordt: Client ↔ Authentication/Authorization server ↔ API server. Voor de client lijkt het alsof deze rechtstreeks communiceert met de API server. Deze architectuur maakt het mogelijk om, op elk moment, een proxy of load balancer toe te voegen tussen de client en de server zonder dat de client hier iets van merkt. ### REST Requests Voor elke *resource* zijn CRUD-operaties beschikbaar en met elke CRUD-operatie komt een HTTP-methode overeen. Voor een update operatie zijn er twee mogelijke methodes, PUT wordt gebruikt als het object als geheel vervangen (geupdatet) wordt en PATCH als slechts een deel bijgewerkt wordt. | CRUD-operatie | HTTP-methode(s) | |---------------|-----------------| | **C**reate | POST | | **R**ead | GET | | **U**pdate | PUT, PATCH | | **Delete** | DELETE | Elk request naar de API heeft vier onderdelen: 1. Operatie: Een HTTP-methode. 2. Endpoint: Het laatste deel van URL, in deze cursus begint dit steeds met */api*. 3. Parameters: Data die de door de API gebruikt wordt om het request af te handelen. Voor een GET en DELETE request worden de parameters meegegeven in de URL, voor POST, PUT en PATCH in de body. 4. Header: HTTP-headers die zaken zoals authentication data bevatten. Afhankelijk van de methode worden er parameter toegevoegd in de body of worden er een parameter toegevoegd in de URL. Voor PUT en POST wordt body data gebruikt en voor GET, PUT en DELETE een URL-parameter. ## Route Handler Alhoewel de route handler overal in de _app_ directory geplaatst kan worden, spreken we binnen deze cursus af dat we alle API-routes afzonderen in de _/src/app/api_ directory. ### GET We bouwen een route waarmee alle contacten opgehaald kunnen worden. In een REST API bevatten de URL's steeds de naam van de resources die opgehaald of aangepast moeten worden, in dit geval willen we de contacten ophalen en wordt de URL dus _/api/contacts_. Via de _nextURL.searchParams_ property halen we de optionele zoekterm op uit de URL. Als antwoord geeft de functie een de contacten terug, maar ook de statuscode 200 OK. :::tabs @tab /src/app/api/contacts/route.ts ```typescript{} import type {NextRequest} from 'next/server' import {NextResponse} from 'next/server' import {getContacts} from '@/dal/contacts' export async function GET(request: NextRequest): Promise { const contactName = request.nextUrl.searchParams.get('name') ?? '' const contacts = await getContacts(contactName) // 200 OK response return new NextResponse(JSON.stringify(contacts), {status: 200}) } ``` ::: Via [Postman](https://www.postman.com/) kunnen we de API-route testen, onderstaande video demonstreert dit. #### Headers Alhoewel bovenstaande video de juiste data teruggeeft, is het duidelijk dat deze niet echt leesbaar is in Postman (of de browser). Dit is niet noodzakelijk een probleem, als we de API gebruiken in een applicatie zien we de ruwe data nooit, maar wordt deze rechtstreekst verwerkt door de applicatie. Het is desalniettemin beter om de client duidelijk te maken wat voor soort data er teruggegeven wordt, dit kan via de _Content-Type_ header. In dit geval moeten we JSON-code teruggeven en gebruiken we dus _application/json_ als type[^mime]. Op deze manier weet Postman onmiddellijk hoe de data weergegeven/geïnterpreteerd moet worden. [^mime]: Een volledige lijst van MIME-types kan je vinden op de site van de [Internet Assigned Numbers Authority (IANA)](https://www.iana.org/assignments/media-types/media-types.xhtml). :::tabs @tab /src/app/api/contacts/route.ts ```typescript{8-10} export async function GET(request: NextRequest): Promise { const contactName = request.nextUrl.searchParams.get('name') ?? '' const contacts = await getContacts(contactName) // 200 OK response return new NextResponse(JSON.stringify(contacts), { status: 200, headers: { 'Content-Type': 'application/json', }, }) } ``` ::: #### Dynamische routes Om één specifiek contact op te vragen moeten we het id van het contact meegeven in de URL. Via de tweede parameter van een route handler functie kunnen we het dynamisch segment van de URL opvragen, zoals in [les 1](/lessen/backend/lecture1.mdl#route-parameters) besproken, wordt deze parameter asynchroon doorgegeven. Aangezien de route handlers nog steeds in de app router staan, moeten we opnieuw vierkante haken gebruiken om de parameter aan te duiden in de mappenstructuur. :::tabs @tab /src/app/api/contacts/[contactId]/route.ts ```typescript{1-3,5-7} interface RouteParams { params: Promise<{contactId: string}> } export async function GET(_request: NextRequest, {params}: RouteParams): Promise { const {contactId} = await params const contact = await getContact(contactId) // 200 OK response return new NextResponse(JSON.stringify(contact), { status: 200, headers: { 'Content-Type': 'application/json', }, }) } ``` ::: ### Status codes Alhoewel we nog maar twee route handlers geschreven hebben, is het al duidelijk dat de objecten die teruggegeven worden steeds dezelfde structuur hebben. Om herhaling te vermijden en de code leesbaarder te maken, bevatten de startbestanden een verzameling van functies voor de veelgebruikte statuscodes. Elk van de functies heeft een beschrijvende naam die duidelijker is dan een status code, en accepteert twee optionele parameters, de body en de headers. Per default wordt de _Content-Type_ header ingesteld op _application/json_. :::tabs @tab /src/lib/routeResponses.ts ```typescript{} import 'server-only' import {NextResponse} from 'next/server' export function ok(body?: unknown, headers: HeadersInit = {}): NextResponse { return buildRequest(200, body, headers) } // De andere status codes worden niet vermeld // aangezien enkel de naam van de methode anders is. function buildRequest(status: number, body?: unknown, headers: HeadersInit = {}): NextResponse { return new NextResponse(body ? JSON.stringify(body) : null, { status, headers: { 'Content-Type': 'application/json', ...headers, }, }) } ``` @tab /src/app/api/contacts/route.ts ```typescript{1,6} import {ok} from '@/lib/routeResponses' export async function GET(request: NextRequest): Promise { const contactName = request.nextUrl.searchParams.get('name') ?? '' const contacts = await getContacts(contactName) return ok(contacts) } ``` @tab /src/app/api/contacts/[contactId]/route.ts ```typescript{1,6} import {ok} from '@/lib/routeResponses' export async function GET(_request: NextRequest, {params}: RouteParams): Promise { const {contactId} = await params const contact = await getContact(contactId) return ok(contact) } ``` ::: ### POST Om het gebruik van een POST endpoint te bespreken, bouwen we een route waarmee een nieuw contact aangemaakt kan worden. Deze route moet natuurlijk de naam en contactgegevens van het nieuwe contact ontvangen. De data wordt doorgegeven in de body van het request en kan opgehaald worden via de _request.json()_ methode[^formdata]. [^formdata]: Alhoewel een body met JSON-data veruit het meest gebruikt wordt, kan een API route ook gebruikt worden om data die door een formulier ingezonden wordt te verwerken. In dit geval wordt _request.formData()_ gebruikt. Hieronder geven we de data rechtstreeks door aan de DAL-laag, natuurlijk zou deze eerste gevalideerd moeten worden. Hoe je dit doet, bespreken we in de [hoofdstuk 6](/lessen/backend/lecture5.md) Als er errors zijn, geven we dan de statuscode 400 terug die een bad request aanduidt. In het andere geval geven we de nieuw aangemaakte data terug met de statuscode 201 CREATED. Merk op dat één _route.ts_ bestand meerdere route handlers kan bevatten, zolang deze verschillende HTTP-methodes implementeren. :::tabs @tab /src/app/api/contacts/route.ts ```typescript{6} import type {CreateContactParams} from '@/dal/contacts'; import {createContact, getContacts} from '@/dal/contacts' import {created, ok} from '@/lib/routeResponses' export async function POST(request: NextRequest): Promise { const data: unknown = await request.json() const newContact = await createContact(data as CreateContactParams) return created(newContact) } ``` ::: Onderstaande video demonstreert zowel de huidige code als de validatie die we in hoofdstuk 6 toevoegen. ### PUT & DELETE Om een contact te updaten gebruiken we een PUT endpoint en om een contact te verwijderen een DELETE endpoint. Merk op dat we het _id_ dat we als parameter meegeven aan de _updateContact_ functie niet mogen uitlezen uit de body, we moeten de route parameter gebruiken. Doen we dit niet, dan is het mogelijk dat het endpoint het foute contact update, wat tegen de principes van REST ingaat (we zouden dan verschillende URL's kunnen gebruiken om hetzelfde contact te updaten) Daarbovenop kan dit ook rare/moeilijk op te sporen bugs als gevolg hebben. :::tabs @tab /src/app/api/contacts/[contactId]/route.ts ```typescript{} export async function PUT(request: NextRequest, {params}: RouteParams): Promise { const {contactId} = await params const data = await request.json() as UpdateContactParams const updatedContact = await updateContact({...data, id: contactId}) return ok(updatedContact) } export async function DELETE(_request: NextRequest, {params}: RouteParams): Promise { const {contactId} = await params await deleteContact(contactId) return ok() } ``` ::: ## CORS Zoals hierboven aangetoond, werken de API-routes in Postman. Er is echter nog een belangrijk probleem, om dit te demonstreren gebruiken we de Vite applicatie in de _corsDemo_ map. Deze applicatie gebruikt TanStack queries om data op te halen van de API. Als we deze applicatie openen krijgen we onderstaande foutmeldingen te zien. In de console wordt de volgende foutmelding weergegeven: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:3000/api/contacts. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200. Zoals de foutmelding aangeeft, wordt deze fout veroorzaakt door CORS. ### Access Control Allow Origin We lossen dit probleem heel eenvoudig op door de _Access-Control-Allow-Origin_ header toe te voegen aan de requests die momenteel mislukken. Omdat elk request standaard mislukt, schrijven we een proxy functie die deze header toevoegt aan elk request. #### Proxy utilities In het eerste hoofdstuk hebben we in de proxy functie code geschreven die een _requestId_ toevoegt aan elk request en dit gebruikt voor logging. Nu moeten we de proxy functie uitbreiden met code die CORS-headers toevoegt, het is duidelijk dat dit snel onoverzichtelijk wordt. Om dit probleem op te lossen voorzien we een utility functie die gebruikt kan worden om verschillende proxy functies één per één uit te voeren. We beginnen met een algemeen type te definiëren dat een proxy functie beschrijft. Aangezien een proxy functie werkt op basis van het inkomende request geven we dit mee als eerste parameter, daarnaast krijgt de functie ook een response object als parameter. Dit object kan dan uitgebreid worden of rechtstreeks teruggegeven worden, afhankelijk van de noden van de proxy functie. Tenslotte moet de functie een response object teruggeven dat vervolgens gebruikt kan worden als invoer voor de volgende proxy functie. Aangezien een proxy functie zowel synchroon als asynchroon kan zijn, kan het responseobject als promise of als gewoon object teruggegeven worden. :::tabs @tab /src/proxy/withProxy.ts ```typescript export type ChainedProxy = (request: NextRequest, response: NextResponse) => Promise | NextResponse ``` ::: Nu _ChainedProxy_ gedefinieerd is, kunnen we dit gebruiken om een functie te bouwen die een willekeurig aantal _ChainedProxy_ functies als argument krijgt en deze één-per-één uitvoert. De spread operator (...) voor het _functions_ argument geeft aan dat er nul, één, of meer _ChainedProxy_ functies meegegeven kunnen worden als argument. Denk hier bijvoorbeeld aan de argumenten van de _console.log_ functie, die werken op exact dezelfde manier. :::tabs @tab /src/proxy/withProxy.ts ```typescript{3-11} export type ChainedProxy = (request: NextRequest, response: NextResponse) => Promise | NextResponse export async function chainProxy(request: NextRequest, ...functions: ChainedProxy[]): Promise { let response = NextResponse.next() if (request.nextUrl.pathname.startsWith('/_next')) return response for (const fn of functions) { response = await fn(request, response) } return response } ``` ::: We kunnen de _chainProxy_ functie niet rechtstreeks gebruiken in _proxy.ts_ tenzij we dat als volgt doen. ```typescript{} import {chainProxy} from './withProxy' export const proxy = (request: NextRequest) => { return chainProxy( request, proxy1, proxy2, ... ) } ``` Aangezien we de _chainProxy_ functie in verschillende projecten willen gebruiken, voegen we nog een utility toe die bovenstaande syntax afzondert. :::tabs @tab /src/proxy/withProxy.ts ```typescript export function withProxy(...functions: ChainedProxy[]): NextProxy { return async (request: NextRequest) => await chainProxy(request, ...functions) } ``` ::: Nu kan de logging functie uit _proxy.ts_ verhuist worden naar een nieuwe functie die we vervolgens met de _withProxy_ utility oproepen in _proxy.ts_. :::tabs @tab /src/proxy/loggingProxy.ts ```typescript import type {NextRequest, NextResponse} from 'next/server'; import {cookies} from 'next/headers' export async function loggingProxy(request: NextRequest, response: NextResponse): Promise { const awaitedCookies = await cookies() const requestId = crypto.randomUUID() response.headers.set('x-request-id', requestId) response.headers.set('x-request-path', request.nextUrl.pathname) response.headers.set('x-request-method', request.method) awaitedCookies.set({ name: 'requestId', value: requestId, httpOnly: false, }) return response } ``` @tab /src/proxy.ts ```typescript{4} import {withProxy} from '@/proxy/withProxy' import {loggingProxy} from '@/proxy/loggingProxy' export const proxy = withProxy(loggingProxy) ``` ::: #### CORS proxy We schrijven een proxy functie die de _Access-Control-Allow-Origin_ header toevoegt aan elk request dat gedaan wordt naar de API. Via deze header weet de browser dat de API geraadpleegd mag worden van eender welke website. :::tabs @tab /src/proxy/corsProxy.ts ```typescript{} import type {NextRequest, NextResponse} from 'next/server' export function corsProxy(request: NextRequest, response: NextResponse): NextResponse { if (!request.nextUrl.pathname.startsWith('/api')) return response response.headers.set('Access-Control-Allow-Origin', '*') return response } ``` @tab /src/proxy.ts ```typescript{5} import {withProxy} from '@/proxy/withProxy' import {loggingProxy} from '@/proxy/loggingProxy' import {corsProxy} from '@/proxy/corsProxy' export const proxy = withProxy(corsProxy, loggingProxy) ``` ::: ### Access Control Allow Headers Na deze aanpassing worden de contacten correct weergegeven in de Vite applicatie, maar het is nog steeds niet mogelijk om een contact aan te maken. Het probleem in bovenstaand screenshot wordt veroorzaakt door de code voor het post request. :::info :::center corsDemo :::tabs @tab /src/app/api/contactsApi.ts ```typescript{5} async function createContact(contact: CreateContactParams): Promise { const result = await fetch(`http://localhost:3000/api/contacts`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(contact), }) if (!result.ok) { throw new Error('Failed to create contact') } return (await result.json()) as IContact } ``` ::: In bovenstaande code wordt de '_Content-Type_' header gebruikt om aan te geven dat de body van het request JSON-data bevat. Momenteel is de proxy functie nog niet geconfigureerd om deze header toe te staan, standaard worden alle requests met een extra header geblokkeerd. De proxy functie is eenvoudig uit te breiden zodat deze header toegestaan is. :::tabs @tab /src/proxy/corsProxy.ts ```typescript{7} import type {NextRequest, NextResponse} from 'next/server' export function corsProxy(request: NextRequest, response: NextResponse): NextResponse { if (!request.nextUrl.pathname.startsWith('/api')) return response response.headers.set('Access-Control-Allow-Origin', '*') response.headers.set('Access-Control-Allow-Headers', 'Content-Type') return response } ``` ::: ### Preflight De contacten kunnen uitgelezen en aangemaakt worden, maar het is nog steeds niet mogelijk om een contact te verwijderen of te bewerken. Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:3000/api/contacts/c33b9522-48e5-4dbd-90a4-1ac8105bb53c. (Reason: Did not find method in CORS header ‘Access-Control-Allow-Methods’). Om bovenstaande foutmelding op te lossen, kunnen we natuurlijk net zoals hierboven opnieuw de juiste header toevoegen aan de proxy functie. Het is echter interessant om de reden achter al deze verschillende foutmeldingen te bespreken. Nadat de _Access-Control-Allow-Methods_ header toegevoegd is aan de proxy functie, werken de PUT en DELETE operaties wel. Aangezien de _Access-Control-Allow-Headers_ en _Access-Control-Allow-Methods_ header enkel gebruikt worden tijdens preflight requests, voegen we deze conditioneel toe. :::tabs @tab /src/proxy/corsProxy.ts ```typescript{6-9} export function corsProxy(request: NextRequest, response: NextResponse): NextResponse { if (!request.nextUrl.pathname.startsWith('/api')) return response response.headers.set('Access-Control-Allow-Origin', '*') if (request.method === 'OPTIONS') { response.headers.set('Access-Control-Allow-Headers', 'Content-Type') response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE') } return response } ``` ::: ## Logging Het is vanzelfsprekend dat de API-routes ook voorzien moeten worden van log statements. Aangezien hier geen nieuwe leerstof voor nodig is, laten we dit voor de geïntereseerde lezer en verwijzen we door naar het uitgewerkte voorbeeld. ## Voorbeeldcode