3. Route handlers
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 en 5 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.
Startbestanden
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.[1]
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:
- Gebruikers moeten de mogelijkheid hebben om hun data programmatorisch aan te spreken en te gebruiken in hun eigen applicaties.
- Naast de Next applicatie moet er ook een mobiele applicatie of een desktop applicatie voorzien worden die dezelfde data gebruikt.
- 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 en 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 (representational state transfer) 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[3]:
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): 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.
Client-server met zwakke coupling[4]: 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).
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) |
|---|---|
| Create | POST |
| Read | GET |
| Update | PUT, PATCH |
| Delete | DELETE |
Elk request naar de API heeft vier onderdelen:
- Operatie: Een HTTP-methode.
- Endpoint: Het laatste deel van URL, in deze cursus begint dit steeds met /api.
- 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.
- 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
Begrip: Route Handler
Route handlers zijn bestanden die niet omgevormd worden naar een pagina in een Next.js applicatie, maar bedoeld zijn om programmatorisch op te roepen.
Een route handler kan eender waar in de app router geplaatst worden. De enige voorwaarde is dat de route handler in een bestand met de naam route.ts gedefinieerd wordt.
In dit bestand moet een functie geëxporteerd worden per HTTP-methode, de methodes krijgen een NextRequest object als parameter en geven een NextResponse object terug.
import type {NextRequest} from 'next/server'
import {NextResponse} from 'next/server'
export async function GET(request: NextRequest): Promise<NextResponse> {}
export async function POST(request: NextRequest): Promise<NextResponse> {}
export async function PUT(request: NextRequest): Promise<NextResponse> {}
export async function PATCH(request: NextRequest): Promise<NextResponse> {}
export async function DELETE(request: NextRequest): Promise<NextResponse> {}
export async function HEAD(request: NextRequest): Promise<NextResponse> {}
export async function OPTIONS(request: NextRequest): Promise<NextResponse> {}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.
import type {NextRequest} from 'next/server'
import {NextResponse} from 'next/server'
import {getContacts} from '@/dal/contacts'
export async function GET(request: NextRequest): Promise<NextResponse> {
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 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[5]. Op deze manier weet Postman onmiddellijk hoe de data weergegeven/geïnterpreteerd moet worden.
export async function GET(request: NextRequest): Promise<NextResponse> {
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 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.
interface RouteParams {
params: Promise<{contactId: string}>
}
export async function GET(_request: NextRequest, {params}: RouteParams): Promise<NextResponse> {
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.
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,
},
})
}import {ok} from '@/lib/routeResponses'
export async function GET(request: NextRequest): Promise<NextResponse> {
const contactName = request.nextUrl.searchParams.get('name') ?? ''
const contacts = await getContacts(contactName)
return ok(contacts)
}import {ok} from '@/lib/routeResponses'
export async function GET(_request: NextRequest, {params}: RouteParams): Promise<NextResponse> {
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[6].
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 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.
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<NextResponse> {
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.
export async function PUT(request: NextRequest, {params}: RouteParams): Promise<NextResponse> {
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<NextResponse> {
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:
Zoals de foutmelding aangeeft, wordt deze fout veroorzaakt door CORS.
Begrip: CORS
CORS (Cross-Origin Resource Sharing) is een beveiligingsmechanisme dat bepaalt hoe webapplicaties in een domein (origin) kunnen communiceren met bronnen die gehost worden op een ander domein. Via HTTP-headers geeft een server expliciet aan welke origins toegang hebben tot specifieke bronnen. Wanneer een HTTP-request afkomstig is van een andere origin dan de server, wordt het door de browser geblokkeerd, tenzij de server expliciet toestemming geeft via de juiste CORS-headers.
CORS is specifiek gericht op verzoeken die vanuit een browser worden geïnitieerd via JavaScript (fetch). Dit betekent dat CORS niet gebruikt wordt bij verzoeken die uitgevoerd worden buiten een browseromgeving, zoals verzoeken vanaf een Node.js server, desktopapplicatie, native mobiele applicatie (geschreven in Kotlin of Swift) of een andere server-side applicatie.
Vanuit beveiligingsoogpunt blokkeert een browser standaard elk HTTP-verzoek dat wordt gedaan naar een andere origin. Dit betekent dat wanneer een Single Page Application (SPA) probeert om gegevens op te halen van een externe API, de browser deze verzoeken zal blokkeren, tenzij de server expliciet toestemming heeft verleend via de juiste CORS-instellingen. Deze aanpak helpt om aanvallen zoals Cross-Site Request Forgery (CSRF) te voorkomen. Kwaadwillenden kunnen dan niet zomaar vanuit een andere website (of via een kwaadaardige extensie of bookmark) JavaScript-code uitvoeren om ongeautoriseerde HTTP-verzoeken te versturen en gevoelige gegevens te stelen.
Stel dat CORS niet bestond en dat een gebruiker in tabblad A is ingelogd op een bankwebsite en in tabblad B een kwaadaardige website bezoekt. De kwaadaardige website kan dan HTTP-requests versturen naar de bankwebsite, alsof de gebruiker deze verzoeken zelf heeft gestuurd. Omdat er geen CORS is, en omdat de browser cookies automatisch meestuurt bij elk request naar de bankwebsite, kan de kwaadwillende dus overschrijvingen uitvoeren op de bankrekening van de gebruiker. CORS helpt om dit soort aanvallen te voorkomen, het is natuurlijk nog steeds mogelijk dat een kwaadwillende JavaScript code injecteert in een website en de requests uitvoert vanuit het banktabblad, waarbij CORS niet van toepassing is. CORS is dus slechts een klein, maar belangrijk, deel van de puzzel.
Natuurlijk zijn er API's die zonder problemen publiek beschikbaar gesteld kunnen worden, met name read-only API's die het weerbericht, nieuws, of andere openbare informatie ophalen. Voor deze API's moeten de Access-Control-Allow-Origin header ingesteld worden. Hier kunnen meerdere origins aan meegegeven worden, de origin * kan gebruikt worden om de route overal beschikbaar te maken.
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.
export type ChainedProxy = (request: NextRequest, response: NextResponse) => Promise<NextResponse> | NextResponseNu 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.
export type ChainedProxy = (request: NextRequest, response: NextResponse) => Promise<NextResponse> | NextResponse
export async function chainProxy(request: NextRequest, ...functions: ChainedProxy[]): Promise<NextResponse> {
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.
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.
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.
import type {NextRequest, NextResponse} from 'next/server';
import {cookies} from 'next/headers'
export async function loggingProxy(request: NextRequest, response: NextResponse): Promise<NextResponse> {
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
}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.
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
}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
corsDemo
async function createContact(contact: CreateContactParams): Promise<IContact> {
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.
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.
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.
Begrip: CORS Preflight Requests
Als een HTTP Request een side-effect heeft, i.e. als er data aangepast wordt op de server (PUT, DELETE, POST (enkel als het content type niet application/x-www-form-urlencoded, multipart/form-data of text/plain is)), wordt er een preflight request uitgevoerd.
Het preflight request wordt verstuurd voor het echte request. De browser vraagt via de HTTP OPTIONS methode aan de server of het PUT, DELETE of POST request uitgevoerd mag worden. Als de server de juiste headers teruggeeft, wordt het echte request verstuurd, anders wordt het geblokkeerd.
Een preflight request moet minstens twee headers teruggeven, de eerste header Access-Control-Allow-Origin wordt ook gebruikt voor GET en POST requests. De tweede header is Access-Control-Allow-Methods, hiermee wordt aangegeven welke HTTP-methodes toegestaan zijn voor een bepaald endpoint.
Naast deze twee verplichte headers zijn er ook verschillende optionele:
- Access-Control-Allow-Headers: De headers die toegestaan zijn in het PUT, DELETE of POST request.
- Access-Control-Allow-Credentials: Of credentials (cookies of de Authorization header) toegestaan zijn (true/false).
- Access-Control-Expose-Headers: De headers die door de client uitgelezen mogen worden in JavaScript code.
- Access-Control-Max-Age: Geeft aan hoeveel seconden het resultaat van een preflight request bewaard mag worden door de browser.
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.
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
Uitgewerkt lesvoorbeeld met commentaar
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[2] 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 ↩︎
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. ↩︎
Bronnen: https://www.ibm.com/topics/rest-apis, https://blog.postman.com/rest-api-examples/, https://aws.amazon.com/what-is/restful-api/ ↩︎
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. ↩︎
Een volledige lijst van MIME-types kan je vinden op de site van de Internet Assigned Numbers Authority (IANA). ↩︎
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. ↩︎