4.4 HTTP-protocol voor REST-API’s

Beheerst HTTP als protocol voor REST-API’s (methods, status codes, headers, caching).

HTTP-basics voor API’s

HTTP (Hypertext Transfer Protocol) vormt de basis van moderne web-API’s. In REST-architectuur wordt HTTP gebruikt zoals het bedoeld is: als een complete application-layer protocol voor resource-manipulatie.

HTTP Request-structuur

POST /api/personen HTTP/1.1
Host: api.gemeente.nl
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
User-Agent: Gemeente-App/2.1.0
Content-Length: 156

{
  "bsn": "123456789",
  "voornaam": "Jan",
  "achternaam": "Berg",
  "geboortedatum": "1985-03-15"
}

HTTP Response-structuur

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/personen/pers_abc123
ETag: "v1.2.3"
Cache-Control: private, max-age=3600
Content-Length: 234

{
  "id": "pers_abc123",
  "bsn": "123456789", 
  "voornaam": "Jan",
  "achternaam": "Berg",
  "geboortedatum": "1985-03-15",
  "created_at": "2024-03-05T14:30:00Z",
  "updated_at": "2024-03-05T14:30:00Z",
  "_links": {
    "self": "/api/personen/pers_abc123",
    "adres": "/api/personen/pers_abc123/adres"
  }
}

HTTP Methods voor CRUD-operaties

GET - Resource opvragen

# Single resource
GET /api/personen/123456789 HTTP/1.1
Host: api.gemeente.nl
Accept: application/json

# Collection met filters
GET /api/personen?achternaam=Berg&geboortejaar=1985 HTTP/1.1
Host: api.gemeente.nl
Accept: application/json

# Response
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=300

{
  "data": [
    {
      "id": "pers_123",
      "voornaam": "Jan",
      "achternaam": "Berg"
    }
  ],
  "pagination": {
    "total": 1,
    "page": 1,
    "per_page": 25
  }
}

POST - Resource aanmaken

POST /api/personen HTTP/1.1
Host: api.gemeente.nl
Content-Type: application/json
Content-Length: 95

{
  "bsn": "987654321",
  "voornaam": "Maria",
  "achternaam": "Jansen"
}

# Success Response
HTTP/1.1 201 Created
Location: /api/personen/pers_xyz789
Content-Type: application/json

{
  "id": "pers_xyz789",
  "bsn": "987654321",
  "voornaam": "Maria", 
  "achternaam": "Jansen",
  "created_at": "2024-03-05T14:30:00Z"
}

# Error Response
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://api.gemeente.nl/problems/validation-error",
  "title": "Validation Failed",
  "status": 400,
  "detail": "Het verstrekte BSN is al in gebruik",
  "instance": "/api/personen",
  "invalid_params": [
    {
      "name": "bsn",
      "reason": "BSN moet uniek zijn"
    }
  ]
}

PUT - Resource volledig vervangen

PUT /api/personen/pers_123 HTTP/1.1
Host: api.gemeente.nl
Content-Type: application/json
If-Match: "v1.2.3"

{
  "bsn": "123456789",
  "voornaam": "Johannes",
  "achternaam": "van den Berg",
  "geboortedatum": "1985-03-15"
}

# Success Response
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "v1.2.4"

{
  "id": "pers_123",
  "bsn": "123456789",
  "voornaam": "Johannes",
  "achternaam": "van den Berg",
  "geboortedatum": "1985-03-15",
  "updated_at": "2024-03-05T14:35:00Z"
}

# Conflict Response (optimistic locking)
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json

{
  "type": "https://api.gemeente.nl/problems/stale-data",
  "title": "Resource Modified",
  "status": 412,
  "detail": "De resource is gewijzigd door een andere gebruiker"
}

PATCH - Gedeeltelijke resource-update

PATCH /api/personen/pers_123 HTTP/1.1
Host: api.gemeente.nl
Content-Type: application/json-patch+json
If-Match: "v1.2.4"

[
  {
    "op": "replace",
    "path": "/voornaam",
    "value": "Jan"
  },
  {
    "op": "add",
    "path": "/emailadres", 
    "value": "jan.vandenberg@example.com"
  }
]

# Alternative: JSON Merge Patch
PATCH /api/personen/pers_123 HTTP/1.1
Content-Type: application/merge-patch+json

{
  "voornaam": "Jan",
  "emailadres": "jan.vandenberg@example.com"
}

DELETE - Resource verwijderen

DELETE /api/personen/pers_123 HTTP/1.1
Host: api.gemeente.nl
If-Match: "v1.2.4"

# Success Response (no content)
HTTP/1.1 204 No Content

# Success Response (with info)
HTTP/1.1 200 OK
Content-Type: application/json

{
  "message": "Persoon succesvol verwijderd",
  "deleted_at": "2024-03-05T14:40:00Z"
}

# Conflict Response
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.gemeente.nl/problems/cannot-delete",
  "title": "Cannot Delete Resource",
  "detail": "Persoon kan niet worden verwijderd vanwege actieve zaken"
}

HTTP Status Codes

Success Codes (2xx)

200 OK              # Successful GET, PUT, PATCH
201 Created         # Successful POST (resource created)
202 Accepted        # Request accepted (async processing)
204 No Content      # Successful DELETE or PUT (no response body)
206 Partial Content # Partial GET (range requests)

Client Error Codes (4xx)

400 Bad Request         # Malformed request
401 Unauthorized        # Authentication required
403 Forbidden           # Authorization failed
404 Not Found           # Resource doesn't exist
405 Method Not Allowed  # HTTP method not supported
406 Not Acceptable      # Unsupported Accept header
409 Conflict           # Resource conflict (e.g., duplicate)
410 Gone               # Resource permanently deleted
412 Precondition Failed # If-Match, If-None-Match failed
413 Payload Too Large   # Request body too large
415 Unsupported Media Type # Unsupported Content-Type
422 Unprocessable Entity   # Semantic validation failed
429 Too Many Requests      # Rate limiting

Server Error Codes (5xx)

500 Internal Server Error  # Generic server error
501 Not Implemented        # Feature not implemented
502 Bad Gateway           # Upstream service error
503 Service Unavailable   # Temporary service outage
504 Gateway Timeout       # Upstream service timeout

HTTP Headers voor API’s

Request Headers

Authentication/Authorization:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
X-API-Key: abc123-def456-ghi789

Content Negotiation:

Accept: application/json
Accept: application/xml, application/json;q=0.8
Accept-Language: nl-NL, en;q=0.8
Accept-Encoding: gzip, deflate

Conditional Requests:

If-Match: "v1.2.3"              # Only if resource unchanged
If-None-Match: "v1.2.3"         # Only if resource changed  
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
If-Unmodified-Since: Wed, 21 Oct 2024 07:28:00 GMT

Custom Headers:

X-Request-ID: req_abc123        # Request tracing
X-Correlation-ID: corr_xyz789   # Cross-service correlation
X-Client-Version: 2.1.0         # Client application version
X-Organization-Code: 0363       # Municipal organization code

Response Headers

Content Information:

Content-Type: application/json; charset=utf-8
Content-Length: 1234
Content-Language: nl-NL
Content-Encoding: gzip

Caching:

Cache-Control: public, max-age=3600
Cache-Control: private, no-cache, no-store
ETag: "v1.2.3"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
Expires: Thu, 22 Oct 2024 07:28:00 GMT

CORS (Cross-Origin Resource Sharing):

Access-Control-Allow-Origin: https://gemeente-app.nl
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Security:

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

Rate Limiting:

X-RateLimit-Limit: 1000        # Requests per time window
X-RateLimit-Remaining: 856      # Remaining requests
X-RateLimit-Reset: 1609459200   # Reset timestamp
Retry-After: 60                 # Retry after N seconds

HTTP Caching voor API’s

Cache-Control Directive

Public resources:

# Publicly cacheable (CDN, proxy, browser)
Cache-Control: public, max-age=3600

# Example: Reference data, configuration
GET /api/landen HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: public, max-age=86400
ETag: "landen-v2.1"

{
  "landen": [
    {"code": "NL", "naam": "Nederland"},
    {"code": "BE", "naam": "België"}
  ]
}

Private resources:

# Only client-cacheable (sensitive data)
Cache-Control: private, max-age=300

# Example: Personal data
GET /api/personen/123456789 HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: private, max-age=300
ETag: "pers-123456789-v1.5"

{
  "bsn": "123456789",
  "voornaam": "Jan",
  "achternaam": "Berg"
}

No caching:

# Never cache (dynamic, sensitive data)
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

# Example: Authentication tokens
POST /api/auth/token HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, must-revalidate
Expires: 0

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_in": 3600
}

ETag-based Caching

Strong ETags:

# Initial request
GET /api/personen/123 HTTP/1.1

HTTP/1.1 200 OK  
ETag: "v1.2.3"
Content-Type: application/json

{"id": "123", "naam": "Jan Berg"}

# Subsequent request with If-None-Match
GET /api/personen/123 HTTP/1.1
If-None-Match: "v1.2.3"

HTTP/1.1 304 Not Modified
ETag: "v1.2.3"
Cache-Control: private, max-age=300

Weak ETags:

# For semantically equivalent resources
HTTP/1.1 200 OK
ETag: W/"v1.2.3"    # Weak ETag (W/ prefix)

Conditional Updates

# Safe updates with optimistic locking
PUT /api/personen/123 HTTP/1.1
If-Match: "v1.2.3"
Content-Type: application/json

{"naam": "Johannes Berg"}

# Success response
HTTP/1.1 200 OK
ETag: "v1.2.4"

# Conflict response (resource changed by another client)
HTTP/1.1 412 Precondition Failed
ETag: "v1.2.5"

{
  "error": "Resource was modified by another client",
  "current_version": "v1.2.5"
}

REST API Design Patterns

Resource Naming

# Collections (plural nouns)
GET /api/personen
POST /api/personen

# Individual resources  
GET /api/personen/123456789
PUT /api/personen/123456789
DELETE /api/personen/123456789

# Nested resources
GET /api/personen/123456789/adressen
POST /api/personen/123456789/adressen
GET /api/personen/123456789/adressen/456

# Actions (when CRUD isn't sufficient)
POST /api/personen/123456789/verify-identity
POST /api/personen/123456789/change-address

Query Parameters

# Filtering
GET /api/personen?achternaam=Berg&woonplaats=Amsterdam

# Sorting  
GET /api/personen?sort=achternaam,voornaam
GET /api/personen?sort=-geboortedatum  # Descending

# Pagination
GET /api/personen?page=2&limit=25
GET /api/personen?offset=50&limit=25

# Field selection
GET /api/personen?fields=bsn,voornaam,achternaam

# Search
GET /api/personen?q=Jan+Berg
GET /api/personen?search=achternaam:Berg AND woonplaats:Amsterdam

Response Envelopes

Simple data response:

{
  "id": "123",
  "voornaam": "Jan",
  "achternaam": "Berg"
}

Collection response:

{
  "data": [
    {"id": "123", "naam": "Jan Berg"},
    {"id": "124", "naam": "Maria Jansen"}
  ],
  "meta": {
    "total": 245,
    "count": 2,
    "current_page": 1,
    "total_pages": 123
  },
  "links": {
    "self": "/api/personen?page=1",
    "first": "/api/personen?page=1",
    "next": "/api/personen?page=2",
    "last": "/api/personen?page=123"
  }
}

Error response (RFC 7807 Problem Details):

{
  "type": "https://api.gemeente.nl/problems/validation-error",
  "title": "Input validation failed",
  "status": 400,
  "detail": "Het verstuurde BSN heeft een incorrect formaat",
  "instance": "/api/personen",
  "invalid-params": [
    {
      "name": "bsn",
      "reason": "BSN must be exactly 9 digits"
    }
  ],
  "trace-id": "req_abc123"
}

HTTP Performance Optimization

Connection Management

// HTTP/1.1 Keep-Alive
const http = require('http');
const agent = new http.Agent({
  keepAlive: true,
  keepAliveMsecs: 1000,
  maxSockets: 25
});

const options = {
  hostname: 'api.gemeente.nl',
  agent: agent
};

Compression

# Request with compression support
GET /api/personen HTTP/1.1
Accept-Encoding: gzip, deflate, br

# Response with compression
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 512     # Compressed size
Vary: Accept-Encoding

HTTP/2 Features

# Server Push (preemptively send related resources)
Link: </api/personen/123/adres>; rel=preload; as=fetch
Link: </api/personen/123/documenten>; rel=preload; as=fetch

# Header Compression (HPACK)
# Automatically handled by HTTP/2 protocol

# Multiplexing
# Multiple requests over single connection

Security Considerations

HTTPS Enforcement

# Redirect HTTP to HTTPS
HTTP/1.1 301 Moved Permanently
Location: https://api.gemeente.nl/api/personen
Strict-Transport-Security: max-age=31536000

Input Validation

# Request
POST /api/personen HTTP/1.1
Content-Type: application/json

{
  "bsn": "123456789',DROP TABLE personen;--",
  "voornaam": "<script>alert('xss')</script>"
}

# Response (proper validation)
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://api.gemeente.nl/problems/validation-error",
  "title": "Input validation failed",
  "invalid-params": [
    {
      "name": "bsn",
      "reason": "BSN may only contain digits"
    },
    {
      "name": "voornaam", 
      "reason": "HTML tags are not allowed"
    }
  ]
}

Rate Limiting Implementation

// Express.js rate limiting
const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 1000, // Limit each IP to 1000 requests per windowMs
  message: 'Te veel requests van dit IP-adres',
  standardHeaders: true,
  legacyHeaders: false,
  handler: (req, res) => {
    res.status(429).json({
      error: 'Rate limit exceeded',
      retry_after: Math.ceil(req.rateLimit.resetTime / 1000)
    });
  }
});

app.use('/api/', apiLimiter);

Debugging HTTP API’s

curl Examples

# Basic GET request with headers
curl -H "Accept: application/json" \
     -H "Authorization: Bearer $TOKEN" \
     https://api.gemeente.nl/api/personen/123456789

# POST with JSON data
curl -X POST \
     -H "Content-Type: application/json" \
     -d '{"bsn":"123456789","voornaam":"Jan"}' \
     https://api.gemeente.nl/api/personen

# PUT with optimistic locking
curl -X PUT \
     -H "Content-Type: application/json" \
     -H "If-Match: v1.2.3" \
     -d '{"voornaam":"Johannes"}' \
     https://api.gemeente.nl/api/personen/123

# Verbose output for debugging
curl -v -X GET \
     -H "Accept: application/json" \
     https://api.gemeente.nl/api/personen/123456789

# Test caching behavior
curl -H "If-None-Match: v1.2.3" \
     https://api.gemeente.nl/api/personen/123456789

HTTP Client Tools

  • Postman: GUI-based API testing met collections
  • Insomnia: Alternative REST client
  • HTTPie: User-friendly command-line HTTP client
  • Thunder Client: VS Code extension voor API testing

Het HTTP-protocol vormt de fundament van moderne REST-API’s. Door HTTP-features zoals methoden, status codes, headers en caching correct te gebruiken, kunnen overheidsorganisaties robuuste, performante en veilige API’s bouwen die voldoen aan moderne standaarden en verwachtingen.

Resources: