Hoe wordt een goede API gemaakt.

7 mrt. 2024 | by Ralph Van Der Horst

Hoe wordt een goede API gemaakt.

Inleiding: De Fundamenten van Excellente API Development

Een API (Application Programming Interface) fungeert als een digitale bruggenbouwer tussen verschillende softwaresystemen. Het stelt applicaties in staat om naadloos te communiceren en functionaliteiten uit te wisselen. In de moderne, interconnected digital wereld zijn APIs essentieel voor scalable, maintainable software architectures.

Deze guide leidt je door de drie fundamentele pilaren van API excellence: Functionaliteit, Performance en Security.

Pilaar 1: Functionele API Design

Core Principes

Consistency & Predictability: Je API moet gemakkelijk te lezen en te begrijpen zijn. Developers moeten kunnen voorspellen hoe endpoints werken op basis van eerdere ervaringen met je API.

// ✅ Consistent naming convention
// Alle endpoints gebruiken dezelfde structuur: /api/version/resource/{id}
GET /api/v1/users/{userId}           // Specifieke gebruiker ophalen
GET /api/v1/users/{userId}/orders    // Orders van een gebruiker
GET /api/v1/orders/{orderId}         // Specifieke order ophalen

// ❌ Inconsistent patterns - verwarrend voor developers
GET /api/getUser/{id}                // Mengeling van REST en RPC stijl
GET /api/v1/user_orders/{user_id}    // Verschillende naming conventions
GET /api/fetchOrder/{order-id}       // Verschillende URL structuren

RESTful HTTP Verb Usage: Elk HTTP werkwoord heeft een specifieke betekenis. Gebruik ze consistent om duidelijk te maken wat een operatie doet.

// Standard CRUD operaties op /api/v1/invoices resource
GET     /api/v1/invoices          // Lijst van facturen ophalen (Read)
POST    /api/v1/invoices          // Nieuwe factuur aanmaken (Create)  
PUT     /api/v1/invoices/{id}     // Volledige factuur vervangen (Update/Replace)
PATCH   /api/v1/invoices/{id}     // Gedeeltelijke factuur update (Update/Modify)
DELETE  /api/v1/invoices/{id}     // Factuur verwijderen (Delete)

// Voor sub-resources gebruik je nested URLs
GET     /api/v1/invoices/{id}/payments     // Payments van een factuur
POST    /api/v1/invoices/{id}/payments     // Nieuwe payment aan factuur toevoegen

OpenAPI Specification

OpenAPI (voorheen Swagger) is de industriestandaard voor het documenteren van REST APIs. Het biedt een machine-readable specificatie die automatisch documentatie, client libraries en testing tools kan genereren.

openapi: 3.0.3
info:
  title: SAP Accounts Payable API
  description: API for managing accounts payable operations
  version: 2.1.0

paths:
  /vendors/{vendorId}:
    get:
      summary: Get vendor details
      parameters:
        - name: vendorId
          in: path
          required: true
          schema:
            type: string
            pattern: '^VND-[0-9]{6}

### Error Handling Patterns

Goede error handling is cruciaal voor een positive developer experience. Errors moeten informatief zijn en developers helpen om problemen snel op te lossen.

**Validation Errors (400 Bad Request)** - wanneer de client ongeldige data stuurt:
```javascript
{
  "status": "error",
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Request contains invalid data",
    "details": [
      {
        "field": "vendorId",           // Specifiek veld dat fout is
        "code": "REQUIRED",            // Type fout voor programmatische handling
        "message": "Vendor ID is required"  // Human-readable boodschap
      },
      {
        "field": "amount",
        "code": "INVALID_RANGE",
        "message": "Amount must be greater than 0",
        "value": -100                  // De ongeldige waarde voor debugging
      }
    ]
  }
}

Resource Not Found (404) - wanneer gevraagde resource niet bestaat:

{
  "status": "error",
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Invoice with ID INV-12345 was not found",
    "details": {
      "resourceType": "invoice",     // Type resource voor context
      "resourceId": "INV-12345",     // ID dat niet gevonden werd
      "suggestions": [               // Helpende suggesties
        "Verify the invoice ID is correct",
        "Check if you have access to this invoice",
        "Use GET /api/v1/invoices to list available invoices"
      ]
    }
  }
}

Belangrijke principes:

  • Consistente structuur: Alle errors hebben zelfde format
  • Error codes: Machine-readable codes voor programmatische handling
  • Helpful messages: Human-readable uitleg van het probleem
  • Actionable suggestions: Concrete stappen om probleem op te lossen

Pilaar 2: Performance Optimization

Authentication & Rate Limiting

OAuth 2.0 Implementation: OAuth 2.0 is de industriestandaard voor API authenticatie. Het geeft users controle over welke toegang ze verlenen zonder passwords te delen.

components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.company.com/oauth2/authorize
          tokenUrl: https://auth.company.com/oauth2/token
          scopes:
            read:invoices: "Read access to invoices"      # Alleen lezen van facturen
            write:invoices: "Write access to invoices"    # Maken/wijzigen van facturen

Advanced Rate Limiting: Rate limiting voorkomt API misbruik en zorgt voor eerlijke resource verdeling tussen users.

import { RateLimiterRedis } from 'rate-limiter-flexible';

// Redis-backed rate limiter voor distributed systems
const rateLimiter = new RateLimiterRedis({
  storeClient: redis,        // Redis connection voor shared state
  keyPrefix: 'rl_api',       // Prefix voor cache keys
  points: 100,               // 100 requests toegestaan
  duration: 3600,            // Per 3600 seconden (1 uur)
});

app.use(async (req, res, next) => {
  try {
    // Consume 1 point per request for this IP
    await rateLimiter.consume(req.ip);
    next();
  } catch (rejRes) {
    // Rate limit exceeded - rejRes contains timing info
    res.status(429).json({
      error: {
        code: 'RATE_LIMIT_EXCEEDED',
        message: 'Too many requests',
        retryAfter: Math.round(rejRes.msBeforeNext / 1000)  // Seconds until reset
      }
    });
  }
});

Waarom dit belangrijk is:

  • Security: Voorkomt brute force attacks en API misbruik
  • Performance: Beschermt server resources tegen overload
  • Fairness: Zorgt dat alle users eerlijke toegang hebben

Intelligent Caching

Caching is een van de meest effectieve manieren om API performance te verbeteren. Een multi-layer strategie combineert snelheid van memory cache met persistentie van Redis.

Multi-Layer Cache Strategy:

class CacheManager {
  constructor() {
    // Layer 1: In-memory cache (snelste, maar beperkt tot één server instance)
    this.memoryCache = new NodeCache({ stdTTL: 300 });  // 5 minuten default TTL
    
    // Layer 2: Redis cache (gedeeld tussen alle server instances)
    this.redisCache = new Redis(process.env.REDIS_URL);
  }
  
  async get(key, fallback, ttl = 300) {
    // Stap 1: Probeer memory cache eerst (microseconden response time)
    let result = this.memoryCache.get(key);
    if (result) {
      console.log(`Cache HIT: Memory - ${key}`);
      return result;
    }
    
    // Stap 2: Probeer Redis cache (milliseconden response time)
    const redisResult = await this.redisCache.get(key);
    if (redisResult) {
      console.log(`Cache HIT: Redis - ${key}`);
      result = JSON.parse(redisResult);
      
      // Populate memory cache voor volgende request
      this.memoryCache.set(key, result, ttl);
      return result;
    }
    
    // Stap 3: Cache MISS - execute fallback functie (database/API call)
    if (fallback) {
      console.log(`Cache MISS: Executing fallback - ${key}`);
      result = await fallback();
      
      // Populate beide cache layers
      this.memoryCache.set(key, result, ttl);
      await this.redisCache.setex(key, ttl, JSON.stringify(result));
      return result;
    }
    
    return null;
  }
}

// Praktisch gebruik in een API endpoint
app.get('/api/v1/vendors/:id', async (req, res) => {
  const vendorId = req.params.id;
  const cacheKey = `vendor:${vendorId}`;
  
  const vendor = await cacheManager.get(
    cacheKey,
    () => database.getVendor(vendorId),  // Fallback: database query
    600  // Cache voor 10 minuten
  );
  
  res.json({ data: vendor });
});

Performance impact:

  • Memory cache: ~1ms response time
  • Redis cache: ~5-10ms response time
  • Database query: ~50-200ms response time
  • External API: ~500-2000ms response time

Pilaar 3: Security

SQL Injection Prevention

SQL injection is een van de meest voorkomende en gevaarlijke security vulnerabilities. Het ontstaat wanneer user input direct in SQL queries wordt gebruikt zonder proper validation.

Dangerous Approach - NEVER do this:

// ❌ EXTREMELY DANGEROUS - Direct string interpolation
app.get('/api/search', (req, res) => {
  const keyword = req.query.keyword;
  
  // Dit is kwetsbaar voor SQL injection!
  const query = `SELECT * FROM products WHERE name LIKE '%${keyword}%'`;
  
  db.query(query, (err, results) => {
    res.json(results);
  });
});

// Een attacker kan dit misbruiken door te zoeken naar:
// '; DROP TABLE products; --
// Dit resulteert in: SELECT * FROM products WHERE name LIKE '%'; DROP TABLE products; --%'
// En kan je hele database vernietigen!

Secure Implementation - Always use this:

// ✅ SAFE - Parameterized queries
import { query, validationResult } from 'express-validator';

app.get('/api/search', [
  // Input validation VOOR database query
  query('keyword')
    .trim()                           // Remove whitespace
    .isLength({ min: 1, max: 100 })  // Length limits
    .escape()                         // Escape HTML characters
    .withMessage('Search keyword must be 1-100 characters'),
], async (req, res) => {
  // Check voor validation errors
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: { code: 'VALIDATION_FAILED', details: errors.array() }
    });
  }
  
  const keyword = req.query.keyword;
  
  // Parameterized query - database driver handles escaping
  const query = 'SELECT id, name FROM products WHERE name LIKE ? AND status = ?';
  const params = [`%${keyword}%`, 'active'];  // Parameters array
  
  const results = await db.query(query, params);
  res.json({ data: results });
});

Waarom parameterized queries veilig zijn:

  • Database driver behandelt parameter escaping automatisch
  • Input wordt nooit geïnterpreteerd als SQL code
  • Zelfs malicious input wordt behandeld als pure data

Input Validation

import { body, validationResult } from 'express-validator';

const invoiceValidation = [
  body('vendorId')
    .matches(/^VND-[0-9]{6}$/)
    .withMessage('Invalid vendor ID format'),
  
  body('amount')
    .isFloat({ min: 0.01, max: 999999.99 })
    .withMessage('Amount must be between 0.01 and 999,999.99'),
  
  body('currency')
    .isIn(['EUR', 'USD', 'GBP'])
    .withMessage('Invalid currency code')
];

app.post('/api/v1/invoices', invoiceValidation, (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: {
        code: 'VALIDATION_FAILED',
        details: errors.array()
      }
    });
  }
  // Process valid request...
});

Security Headers

import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true
  }
}));

Testing Strategy

Functional Testing

import request from 'supertest';

describe('Invoice API', () => {
  test('should create invoice with valid data', async () => {
    const response = await request(app)
      .post('/api/v1/invoices')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        vendorId: 'VND-123456',
        amount: 1250.00,
        currency: 'EUR',
        dueDate: '2024-04-07'
      })
      .expect(201);
    
    expect(response.body.status).toBe('success');
    expect(response.body.data.amount).toBe(1250.00);
  });
  
  test('should reject invalid amount', async () => {
    await request(app)
      .post('/api/v1/invoices')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        vendorId: 'VND-123456',
        amount: -100
      })
      .expect(400);
  });
});

Performance Testing

# artillery-config.yml
config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 10
    - duration: 120
      arrivalRate: 50

scenarios:
  - name: "Invoice operations"
    flow:
      - post:
          url: "/api/v1/auth/login"
          json:
            username: "test@company.com"
            password: "password"
          capture:
            - json: "$.token"
              as: "authToken"
      
      - post:
          url: "/api/v1/invoices"
          headers:
            Authorization: "Bearer {{ authToken }}"
          json:
            vendorId: "VND-123456"
            amount: 1000
            currency: "EUR"

Tool Recommendations

Functional Testing

  • Postman: API testing and documentation
  • Jest + Supertest: JavaScript testing framework
  • REST Assured: Java DSL for API testing
  • Insomnia: Open-source API client

Performance Testing

  • Artillery: Modern load testing
  • k6: Developer-centric performance testing
  • JMeter: Comprehensive load testing platform
  • Gatling: High-performance testing framework

Security Testing

  • OWASP ZAP: Open-source security scanner
  • Burp Suite: Professional security testing
  • Postman Security: Built-in security testing
  • Snyk: Dependency vulnerability scanning

Conclusie

Het creëren van een excellente API vereist een holistische aanpak die functionaliteit, performance en security in balans brengt. Door consistent design, proper error handling, intelligent caching, rate limiting, en comprehensive security measures te implementeren, bouw je APIs die niet alleen technisch excellent zijn, maar ook developer-friendly en business-ready.

Key Takeaways

Functionele Excellence: Consistente design patterns, comprehensive error handling, proper versioning Performance Optimization: Multi-layer caching, intelligent rate limiting, advanced authentication Security First: Input validation, SQL injection prevention, security headers, automated testing

Een excellente API is nooit “klaar” - het is een living system dat evolueert met je business needs. Blijf investeren in continuous improvement, community feedback en emerging best practices om je API ecosystem future-proof te houden. # Enforces format VND-123456 responses: ‘200’: description: Vendor details retrieved successfully content: application/json: schema: $ref: ‘#/components/schemas/Vendor’ ‘404’: description: Vendor not found

/invoices: post: summary: Create new invoice requestBody: required: true content: application/json: schema: $ref: ‘#/components/schemas/CreateInvoiceRequest’ responses: ‘201’: description: Invoice created successfully ‘400’: description: Invalid input data

components: schemas: Vendor: type: object properties: vendorId: type: string example: “VND-123456” name: type: string example: “Tech Solutions Ltd” email: type: string format: email

CreateInvoiceRequest:
  type: object
  required:                    # Deze velden zijn verplicht
    - vendorId
    - amount
    - currency
  properties:
    vendorId:
      type: string
    amount:
      type: number
      minimum: 0.01           # Voorkomt negatieve bedragen
    currency:
      type: string
      enum: [EUR, USD, GBP]   # Beperkt tot toegestane valuta

**Waarom OpenAPI belangrijk is:**
- **Automatische documentatie**: Tools zoals Swagger UI genereren interactieve documentatie
- **Code generatie**: Client libraries kunnen automatisch worden gegenereerd
- **Validatie**: Request/response validation kan geautomatiseerd worden
- **Testing**: API testing tools kunnen direct de specificatie gebruiken

### Error Handling Patterns

**Validation Errors (400)**:
```javascript
{
  "status": "error",
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Request contains invalid data",
    "details": [
      {
        "field": "vendorId",
        "code": "REQUIRED",
        "message": "Vendor ID is required"
      },
      {
        "field": "amount",
        "code": "INVALID_RANGE",
        "message": "Amount must be greater than 0"
      }
    ]
  }
}

Resource Not Found (404):

{
  "status": "error",
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Invoice with ID INV-12345 was not found",
    "details": {
      "suggestions": [
        "Verify the invoice ID is correct",
        "Check if you have access to this invoice"
      ]
    }
  }
}

Pilaar 2: Performance Optimization

Authentication & Rate Limiting

OAuth 2.0 Implementation:

components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.company.com/oauth2/authorize
          tokenUrl: https://auth.company.com/oauth2/token
          scopes:
            read:invoices: "Read access to invoices"
            write:invoices: "Write access to invoices"

Advanced Rate Limiting:

import { RateLimiterRedis } from 'rate-limiter-flexible';

const rateLimiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'rl_api',
  points: 100, // requests
  duration: 3600, // per hour
});

app.use(async (req, res, next) => {
  try {
    await rateLimiter.consume(req.ip);
    next();
  } catch (rejRes) {
    res.status(429).json({
      error: {
        code: 'RATE_LIMIT_EXCEEDED',
        message: 'Too many requests',
        retryAfter: Math.round(rejRes.msBeforeNext / 1000)
      }
    });
  }
});

Intelligent Caching

Multi-Layer Cache Strategy:

class CacheManager {
  constructor() {
    this.memoryCache = new NodeCache({ stdTTL: 300 });
    this.redisCache = new Redis(process.env.REDIS_URL);
  }
  
  async get(key, fallback, ttl = 300) {
    // Try memory cache first
    let result = this.memoryCache.get(key);
    if (result) return result;
    
    // Try Redis cache
    const redisResult = await this.redisCache.get(key);
    if (redisResult) {
      result = JSON.parse(redisResult);
      this.memoryCache.set(key, result, ttl);
      return result;
    }
    
    // Execute fallback and cache result
    if (fallback) {
      result = await fallback();
      this.memoryCache.set(key, result, ttl);
      await this.redisCache.setex(key, ttl, JSON.stringify(result));
      return result;
    }
    
    return null;
  }
}

Pilaar 3: Security

SQL Injection Prevention

Dangerous Approach:

// ❌ NEVER do this
const query = `SELECT * FROM products WHERE name LIKE '%${keyword}%'`;

Secure Implementation:

// ✅ Use parameterized queries
const query = 'SELECT id, name FROM products WHERE name LIKE ? AND status = ?';
const params = [`%${keyword}%`, 'active'];
const results = await db.query(query, params);

Input Validation

import { body, validationResult } from 'express-validator';

const invoiceValidation = [
  body('vendorId')
    .matches(/^VND-[0-9]{6}$/)
    .withMessage('Invalid vendor ID format'),
  
  body('amount')
    .isFloat({ min: 0.01, max: 999999.99 })
    .withMessage('Amount must be between 0.01 and 999,999.99'),
  
  body('currency')
    .isIn(['EUR', 'USD', 'GBP'])
    .withMessage('Invalid currency code')
];

app.post('/api/v1/invoices', invoiceValidation, (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: {
        code: 'VALIDATION_FAILED',
        details: errors.array()
      }
    });
  }
  // Process valid request...
});

Security Headers

import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true
  }
}));

Testing Strategy

Functional Testing

import request from 'supertest';

describe('Invoice API', () => {
  test('should create invoice with valid data', async () => {
    const response = await request(app)
      .post('/api/v1/invoices')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        vendorId: 'VND-123456',
        amount: 1250.00,
        currency: 'EUR',
        dueDate: '2024-04-07'
      })
      .expect(201);
    
    expect(response.body.status).toBe('success');
    expect(response.body.data.amount).toBe(1250.00);
  });
  
  test('should reject invalid amount', async () => {
    await request(app)
      .post('/api/v1/invoices')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        vendorId: 'VND-123456',
        amount: -100
      })
      .expect(400);
  });
});

Performance Testing

# artillery-config.yml
config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 10
    - duration: 120
      arrivalRate: 50

scenarios:
  - name: "Invoice operations"
    flow:
      - post:
          url: "/api/v1/auth/login"
          json:
            username: "test@company.com"
            password: "password"
          capture:
            - json: "$.token"
              as: "authToken"
      
      - post:
          url: "/api/v1/invoices"
          headers:
            Authorization: "Bearer {{ authToken }}"
          json:
            vendorId: "VND-123456"
            amount: 1000
            currency: "EUR"

Tool Recommendations

Functional Testing

  • Postman: API testing and documentation
  • Jest + Supertest: JavaScript testing framework
  • REST Assured: Java DSL for API testing
  • Insomnia: Open-source API client

Performance Testing

  • Artillery: Modern load testing
  • k6: Developer-centric performance testing
  • JMeter: Comprehensive load testing platform
  • Gatling: High-performance testing framework

Security Testing

  • OWASP ZAP: Open-source security scanner
  • Burp Suite: Professional security testing
  • Postman Security: Built-in security testing
  • Snyk: Dependency vulnerability scanning

Conclusie

Het creëren van een excellente API vereist een holistische aanpak die functionaliteit, performance en security in balans brengt. Door consistent design, proper error handling, intelligent caching, rate limiting, en comprehensive security measures te implementeren, bouw je APIs die niet alleen technisch excellent zijn, maar ook developer-friendly en business-ready.

Key Takeaways

Functionele Excellence: Consistente design patterns, comprehensive error handling, proper versioning Performance Optimization: Multi-layer caching, intelligent rate limiting, advanced authentication Security First: Input validation, SQL injection prevention, security headers, automated testing

Een excellente API is nooit “klaar” - het is een living system dat evolueert met je business needs. Blijf investeren in continuous improvement, community feedback en emerging best practices om je API ecosystem future-proof te houden.

by Ralph Van Der Horst

arrow right
back to blog

share this article

Relevant articles

Automatische Cursuscreatie Met Google Sheets, Apps Script en AI

Automatische Cursuscreatie Met Google Sheets, Apps Script en AI

Chat GPT en Testdatacreatie

19 mrt. 2024

Chat GPT en Testdatacreatie

Prestatietests met de installatiehandleiding voor webdriverio

26 feb. 2024

Prestatietests met de installatiehandleiding voor webdriverio