API-development-gids¶
Gids voor IntraVox-contributors om nieuwe API-endpoints toe te voegen en OpenAPI-documentatie te onderhouden.
Inhoudsopgave¶
- Architectuur-overzicht
- Een nieuw endpoint toevoegen
- OpenAPI-spec bijwerken
- Schema-best-practices
- Validatie-checklist
- Case study: template-endpoints
- Resources
Architectuur-overzicht¶
De IntraVox-API volgt een gelaagd architectuur-patroon:
HTTP-request
↓
Router (appinfo/routes.php)
↓
Controller (lib/Controller/ApiController.php)
↓
Service-laag (lib/Service/PageService.php, enz.)
↓
Data-access (filesystem via Nextcloud-API)
↓
HTTP-response (JSONResponse)
Componenten¶
Router (appinfo/routes.php):
- Mapt URL-patronen naar controller-methoden
- Definieert HTTP-verbs (GET, POST, PUT, DELETE)
- Groepeert gerelateerde endpoints
Controller (lib/Controller/ApiController.php):
- Verwerkt HTTP-zaken (request-parsing, auth-checks)
- Valideert input-parameters
- Delegeert business-logica naar services
- Formatteert responses als JSON
- Vangt exceptions op en geeft passende HTTP-statuscodes terug
Service-laag (lib/Service/):
- Bevat business-logica
- Praat met de Nextcloud-filesystem-API
- Herbruikbaar over controllers, CLI-commando's en background jobs heen
- Gooit exceptions bij fouten (controller vangt ze op)
OpenAPI-spec (openapi.json):
- Single source of truth voor API-contracten
- Documenteert alle endpoints, parameters en responses
- Gebruikt door Swagger UI, Postman, code-generators
- Moet in sync blijven met de daadwerkelijke implementatie
Design-principes¶
- Thin controllers — delegeer naar services, geen business-logica
- Herbruikbare services — gebruikt door API, CLI, cron-jobs
- OpenAPI als contract — de spec definieert wat endpoints horen te doen
- Consistente error-afhandeling — alle errors geven JSON terug met
error- enerrorId-velden - Semantic versioning — breaking changes vereisen een nieuwe API-versie
Een nieuw endpoint toevoegen¶
Volg deze stappen om een nieuw API-endpoint aan IntraVox toe te voegen.
Stap 1: route definiëren¶
Bestand: appinfo/routes.php
Voeg de route toe aan de 'routes'-array:
Naam-conventies:
- Route-naam:
api#{camelCase}(bv.api#createPageFromTemplate) - URL-pad:
/api/{kebab-case}(bv./api/pages/from-template) - HTTP-verb: GET (lees), POST (aanmaken), PUT (bijwerken), DELETE (verwijderen)
Voorbeelden:
// Resources oplijsten
['name' => 'api#listResources', 'url' => '/api/resources', 'verb' => 'GET']
// Enkele resource ophalen
['name' => 'api#getResource', 'url' => '/api/resources/{id}', 'verb' => 'GET']
// Resource aanmaken
['name' => 'api#createResource', 'url' => '/api/resources', 'verb' => 'POST']
// Resource bijwerken
['name' => 'api#updateResource', 'url' => '/api/resources/{id}', 'verb' => 'PUT']
// Resource verwijderen
['name' => 'api#deleteResource', 'url' => '/api/resources/{id}', 'verb' => 'DELETE']
Stap 2: controller-methode implementeren¶
Bestand: lib/Controller/ApiController.php
Voeg een publieke methode toe die de route-naam matched:
/**
* Maak een nieuwe resource aan
*
* @NoAdminRequired
* @param string $param Verplichte parameter
* @param string|null $optionalParam Optionele parameter
* @return JSONResponse
*/
public function myNewEndpoint(string $param, ?string $optionalParam = null): JSONResponse {
try {
// 1. Input valideren
if (empty($param)) {
return new JSONResponse([
'error' => 'Parameter "param" is verplicht'
], 400);
}
// 2. Permissies checken (indien nodig)
if (!$this->permissionService->canWrite($this->userId, '/some/path')) {
return new JSONResponse([
'error' => 'Toegang geweigerd'
], 403);
}
// 3. Delegeren naar service
$result = $this->pageService->doSomething($param, $optionalParam);
// 4. Success-response teruggeven
return new JSONResponse([
'success' => true,
'data' => $result
], 200);
} catch (\InvalidArgumentException $e) {
// 5. Validatie-errors afhandelen
return new JSONResponse([
'error' => $e->getMessage()
], 400);
} catch (\Exception $e) {
// 6. Onverwachte errors afhandelen
$errorId = bin2hex(random_bytes(4));
$this->logger->error('myNewEndpoint failed', [
'errorId' => $errorId,
'exception' => $e->getMessage(),
'param' => $param,
'userId' => $this->userId
]);
return new JSONResponse([
'error' => 'Interne server-fout',
'errorId' => $errorId
], 500);
}
}
Annotaties:
@NoAdminRequired— sta niet-admin-gebruikers toe (default: alleen admin)@NoCSRFRequired— sla CSRF-check over (alleen voor read-only GET-endpoints)@PublicPage— sta anonieme toegang toe (voorzichtig gebruiken)
Response-conventies:
- Succes:
{'success': true, 'data': ...}met 200 (of 201 bij aanmaak) - Client-error:
{'error': 'message'}met 400/403/404 - Server-error:
{'error': 'message', 'errorId': 'abc123'}met 500
Stap 3: service-logica implementeren¶
Bestand: lib/Service/PageService.php (of nieuwe service maken)
Voeg een business-logica-methode toe:
/**
* Doe iets met een resource
*
* @param string $param Parameter-beschrijving
* @param string|null $optionalParam Optionele parameter
* @return array Resultaat-data
* @throws \InvalidArgumentException als validatie faalt
* @throws \Exception als operatie faalt
*/
public function doSomething(string $param, ?string $optionalParam = null): array {
// 1. Business-regels valideren
if (strlen($param) < 3) {
throw new \InvalidArgumentException('Parameter moet minstens 3 tekens zijn');
}
// 2. Data ophalen (filesystem, database)
$folder = $this->rootFolder->getUserFolder($this->userId);
$file = $folder->get('/IntraVox/' . $param);
// 3. Business-logica uitvoeren
$content = $file->getContent();
$processed = $this->processContent($content);
// 4. Resultaat teruggeven
return [
'id' => $param,
'content' => $processed,
'optional' => $optionalParam
];
}
/**
* Helper-methode voor verwerking
*
* @param string $content
* @return string
*/
private function processContent(string $content): string {
// Verwerkings-logica hier
return strtoupper($content);
}
Service-verantwoordelijkheden:
- Business-logica en validatie
- Data-access (bestanden, database)
- Data-transformatie
- Gooi exceptions bij errors (controller vangt op)
- Geen HTTP-zaken (geen JSONResponse, geen statuscodes)
Stap 4: OpenAPI-spec bijwerken¶
Bestand: openapi.json
Voeg endpoint-documentatie toe. Zie sectie OpenAPI-spec bijwerken.
Stap 5: testen¶
Zie sectie Validatie-checklist.
OpenAPI-spec bijwerken¶
Path-entry toevoegen¶
Locatie: openapi.json → paths-object
"/api/my-resource": {
"post": {
"operationId": "my-new-endpoint",
"summary": "Maak een nieuwe resource aan",
"description": "Maakt een nieuwe resource aan met de gegeven parameters. Vereist schrijfrechten op de doel-map.",
"tags": ["Resources"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResourceCreateRequest"
},
"example": {
"param": "my-value",
"optionalParam": "optional-value"
}
}
}
},
"responses": {
"200": {
"description": "Resource succesvol aangemaakt",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResourceResponse"
}
}
}
},
"400": {
"description": "Ongeldige parameters",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": { "type": "string" }
}
},
"example": {
"error": "Parameter \"param\" is verplicht"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
}
}
}
}
Schema-definities toevoegen¶
Locatie: openapi.json → components → schemas
"ResourceCreateRequest": {
"type": "object",
"required": ["param"],
"properties": {
"param": {
"type": "string",
"description": "Verplichte parameter (minimum 3 tekens)",
"minLength": 3
},
"optionalParam": {
"type": "string",
"nullable": true,
"description": "Optionele parameter voor aanvullende configuratie"
}
}
},
"ResourceResponse": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Resource-identifier"
},
"content": {
"type": "string",
"description": "Verwerkte content"
},
"optional": {
"type": "string",
"nullable": true,
"description": "Optionele parameter-waarde"
}
}
}
}
}
Tag toevoegen (bij nieuwe categorie)¶
Locatie: openapi.json → tags-array
{
"name": "Resources",
"description": "Resource-management-endpoints voor aanmaken, lezen, bijwerken en verwijderen van resources"
}
Schema-best-practices¶
Naam-conventies¶
| Element | Conventie | Voorbeeld | Waarom |
|---|---|---|---|
| operationId | kebab-case | create-page-from-template |
URL-friendly, leesbaar |
| Schema-naam | PascalCase | TemplateCreateFromRequest |
Matched met class-naming |
| Tag-naam | PascalCase, enkelvoud | Templates (niet Template) |
Consistentie |
| Property-naam | camelCase | pageTitle, parentPath |
JavaScript-conventie |
Schema-naam-patterns:
- Request-schema's:
{Resource}{Action}Request TemplateCreateFromRequest— pagina maken vanuit templateTemplateSaveRequest— pagina opslaan als templatePageUpdateRequest— pagina bijwerken- Response-schema's:
{Resource}of{Resource}Response Page— volledig pagina-objectTemplatePreview— template-lijst-item (lichtgewicht)ErrorResponse— error-details- Suffixen:
Request— input-data voor POST/PUTResponse— output-data (optioneel als gelijk aan resource)Preview— lichtgewicht-versie voor lijsten
Verplichte velden¶
Specificeer altijd een required-array voor object-schema's:
{
"type": "object",
"required": ["field1", "field2"],
"properties": {
"field1": { "type": "string" },
"field2": { "type": "integer" },
"field3": { "type": "string", "nullable": true }
}
}
Regels:
- Lijst alleen écht verplichte velden (validatie faalt zonder ze)
- Optionele velden: weglaten uit
required-array - Nullable velden: voeg
"nullable": truetoe - Markeer nooit alle velden als verplicht als sommige optioneel zijn
Beschrijvingen¶
Schrijf duidelijke, actionable beschrijvingen:
Slecht:
Goed:
{
"templateId": {
"type": "string",
"description": "Template-map-naam (bv. 'department', 'event'). Gebruik GET /api/templates om beschikbare IDs op te lijsten."
}
}
Richtlijnen:
- Leg formaat/validatie uit (bv. "ISO-8601-datum-string", "UUID v4")
- Geef voorbeelden in de beschrijving
- Link naar gerelateerde endpoints indien behulpzaam
- Verduidelijk beperkingen (min/max-lengte, regex-patronen, enum-waarden)
- Leg business-betekenis uit, niet alleen technisch type
Validatie-beperkingen¶
Gebruik OpenAPI-validatie-keywords:
{
"pageTitle": {
"type": "string",
"description": "Pagina-titel",
"minLength": 1,
"maxLength": 255
},
"columnCount": {
"type": "integer",
"description": "Aantal kolommen",
"minimum": 1,
"maximum": 5
},
"complexity": {
"type": "string",
"description": "Template-complexiteit-niveau",
"enum": ["simple", "medium", "advanced"]
},
"email": {
"type": "string",
"description": "Gebruikers-e-mailadres",
"format": "email"
},
"url": {
"type": "string",
"description": "Externe URL",
"format": "uri"
}
}
Beschikbare beperkingen:
minLength,maxLength— string-lengteminimum,maximum— getallen-rangeenum— beperkte set waardenpattern— regex-validatieformat— standaard-formaten (email, uri, date-time, uuid)
Voorbeelden¶
Lever realistische, copy-paste-ready voorbeelden:
Enkel voorbeeld:
Meerdere voorbeelden (beter):
"examples": {
"department": {
"summary": "HR-afdeling-pagina maken",
"value": {
"templateId": "department",
"pageTitle": "HR-afdeling",
"parentPath": "/teams"
}
},
"event": {
"summary": "Bedrijfs-event-pagina maken (geen parent)",
"value": {
"templateId": "event",
"pageTitle": "Jaarcongres 2026"
}
}
}
Voordelen:
- Swagger UI toont dropdown met voorbeelden
- Code-generators gebruiken voorbeelden voor tests
- Ontwikkelaars kunnen werkende waarden copy-pasten
- Documenteert verschillende use-cases
Error-responses¶
Gebruik herbruikbare error-response-schema's:
Herbruikbare errors (al gedefinieerd in IntraVox):
"responses": {
"400": { "$ref": "#/components/responses/BadRequest" },
"401": { "$ref": "#/components/responses/Unauthorized" },
"403": { "$ref": "#/components/responses/Forbidden" },
"404": { "$ref": "#/components/responses/NotFound" }
}
Custom error met voorbeeld:
"400": {
"description": "Ongeldige template-ID of ontbrekende verplichte parameters",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"description": "Mensvriendelijke foutmelding"
}
}
},
"examples": {
"missing-param": {
"summary": "Verplichte parameter ontbreekt",
"value": {
"error": "Verplichte parameter ontbreekt: templateId"
}
},
"invalid-template": {
"summary": "Ongeldige template-ID",
"value": {
"error": "Template niet gevonden: invalid-id"
}
}
}
}
}
}
Validatie-checklist¶
Voltooi deze checklist voordat je je API-wijzigingen merget:
1. OpenAPI-spec-validatie¶
Installeer validator:
Valideer spec:
Verwachte output:
Veelvoorkomende errors en fixes:
| Error | Oorzaak | Fix |
|---|---|---|
| Missing required field | Schema mist required-array |
Voeg verplichte velden-lijst toe |
| Invalid $ref | Pad bestaat niet | Check schema-naam-spelling |
| Duplicate operationId | Twee endpoints met dezelfde ID | Gebruik unieke kebab-case-IDs |
| Missing response schema | Response heeft geen schema | Voeg schema toe of gebruik bestaande |
2. Handmatig testen (cURL)¶
Test succes-case:
curl -X POST \
-u "user:app-password" \
-H "Content-Type: application/json" \
-d '{"param":"valid-value"}' \
https://localhost/apps/intravox/api/my-resource
Test error-cases:
# Ontbrekende parameter
curl -X POST \
-u "user:app-password" \
-H "Content-Type: application/json" \
-d '{}' \
https://localhost/apps/intravox/api/my-resource
# Ongeldige parameter
curl -X POST \
-u "user:app-password" \
-H "Content-Type: application/json" \
-d '{"param":"ab"}' \
https://localhost/apps/intravox/api/my-resource
# Ongeauthoriseerd (verkeerd wachtwoord)
curl -X POST \
-u "user:wrong-password" \
-H "Content-Type: application/json" \
-d '{"param":"valid"}' \
https://localhost/apps/intravox/api/my-resource
Verifieer:
- ✅ Response matched met OpenAPI-schema
- ✅ HTTP-statuscodes correct (200, 201, 400, 401, 403, 404, 500)
- ✅ Foutmeldingen zijn behulpzaam (niet alleen "Bad Request")
- ✅ Authenticatie werkt (401 bij verkeerde credentials)
- ✅ Autorisatie werkt (403 bij geen permissie)
3. Postman-import-test¶
- Open Postman
- Import → Link →
https://raw.githubusercontent.com/nextcloud/IntraVox/main/openapi.json - Vind je nieuwe endpoint in de collection
- Stuur request met geldige data
- Verifieer dat response matched met schema
- Probeer ongeldige data (zou 400 moeten geven)
4. Code-generatie-test¶
Genereer TypeScript-client:
npx @openapitools/openapi-generator-cli generate \
-i openapi.json \
-g typescript-fetch \
-o /tmp/intravox-test-client
Check voor:
- ✅ Geen generatie-errors
- ✅ Method-namen kloppen (check
/tmp/intravox-test-client/apis/) - ✅ Type-definities accuraat
- ✅ Voorbeelden opgenomen in docs
Inspecteer gegenereerde bestanden:
# Lijst gegenereerde API-methodes
ls /tmp/intravox-test-client/apis/
# Check je nieuwe endpoint-methode
grep -A 10 "myNewEndpoint" /tmp/intravox-test-client/apis/*.ts
5. Documentatie-review¶
Self-review-checklist:
-
operationIdvolgt kebab-case-conventie - Summary is bondig (<70 tekens)
- Description legt uit wat, waarom en wanneer te gebruiken
- Alle parameters hebben beschrijvingen
- Voorbeelden zijn realistisch en copy-paste-ready
- Error-responses gedocumenteerd met voorbeelden
- Schema-namen volgen PascalCase
- Required-velden-array is accuraat
- Tag bestaat en is passend
- Geen typo's in beschrijvingen
Peer-review:
- Vraag een collega om de OpenAPI-docs zonder context te lezen
- Kunnen ze begrijpen wat het endpoint doet?
- Zijn voorbeelden duidelijk genoeg om direct te gebruiken?
Case study: template-endpoints¶
De template-endpoints toegevoegd in v0.9.17 demonstreren best-practices. Laten we ze analyseren.
Route-definitie¶
Bestand: appinfo/routes.php:127-132
// Templates oplijsten
['name' => 'api#listTemplates', 'url' => '/api/templates', 'verb' => 'GET'],
// Template-details ophalen
['name' => 'api#getTemplate', 'url' => '/api/templates/{id}', 'verb' => 'GET'],
// Pagina maken vanuit template
['name' => 'api#createPageFromTemplate', 'url' => '/api/pages/from-template', 'verb' => 'POST'],
// Pagina opslaan als template
['name' => 'api#saveAsTemplate', 'url' => '/api/templates', 'verb' => 'POST'],
// Template verwijderen
['name' => 'api#deleteTemplate', 'url' => '/api/templates/{id}', 'verb' => 'DELETE'],
Design-beslissingen:
- ✅ RESTful URL-structuur (
/api/templatesvoor de collection) - ✅ Consistente naamgeving (
listTemplates,getTemplate, enz.) - ✅ Speciale case:
/api/pages/from-template(maakt pagina's, geen templates) - ✅ Standaard-HTTP-verbs (GET lijst/details, POST aanmaken, DELETE verwijderen)
Controller-implementatie¶
Bestand: lib/Controller/ApiController.php:3156-3183
Voorbeeld: createPageFromTemplate()
/**
* Pagina maken vanuit template
*
* @NoAdminRequired
* @param string $templateId Template-ID
* @param string $pageTitle Pagina-titel
* @param string|null $parentPath Parent-map-pad
* @return JSONResponse
*/
public function createPageFromTemplate(
string $templateId,
string $pageTitle,
?string $parentPath = null
): JSONResponse {
try {
$page = $this->pageService->createPageFromTemplate(
$templateId,
$pageTitle,
$parentPath,
$this->userId
);
return new JSONResponse([
'success' => true,
'page' => $page
], 201); // 201 Created voor nieuwe resource
} catch (\Exception $e) {
return new JSONResponse([
'error' => $e->getMessage()
], 400);
}
}
Kern-patterns:
- ✅ Thin controller (delegeert direct naar service)
- ✅ Type-hints voor alle parameters
- ✅ Nullable parameter met default-waarde (
?string $parentPath = null) - ✅ 201-status voor resource-aanmaak (niet alleen 200)
- ✅ Generieke exception-handling (service gooit specifieke errors)
- ✅ Huidige user-ID geïnjecteerd (
$this->userId)
OpenAPI-schema's¶
TemplatePreview (voor list-endpoint):
{
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Template-identifier (map-naam)"
},
"name": {
"type": "string",
"description": "Template-weergavenaam"
},
"columnCount": {
"type": "integer",
"minimum": 1,
"maximum": 5
},
"complexity": {
"type": "string",
"enum": ["simple", "medium", "advanced"]
}
}
}
TemplateCreateFromRequest (voor create-endpoint):
{
"type": "object",
"required": ["templateId", "pageTitle"],
"properties": {
"templateId": {
"type": "string",
"description": "Template-map-naam (bv. 'department', 'event')"
},
"pageTitle": {
"type": "string",
"description": "Titel voor de nieuwe pagina"
},
"parentPath": {
"type": "string",
"nullable": true,
"description": "Optioneel parent-map-pad (bv. '/teams')"
}
}
}
Design-beslissingen:
- ✅ Aparte schema's voor verschillende use-cases (list vs create)
- ✅ Enums voor validatie (
complexity) - ✅ Min/max-beperkingen (
columnCount1-5) - ✅ Expliciete nullability (
parentPath) - ✅ Beschrijvende beschrijvingen met voorbeelden
- ✅ Duidelijke verplichte velden (
templateId,pageTitle)
Voorbeelden¶
Meerdere voorbeelden voor verschillende use-cases:
"examples": {
"department": {
"summary": "HR-afdeling-pagina maken",
"value": {
"templateId": "department",
"pageTitle": "HR-afdeling",
"parentPath": "/teams"
}
},
"event": {
"summary": "Bedrijfs-event-pagina maken",
"value": {
"templateId": "event",
"pageTitle": "Jaarcongres 2026"
}
}
}
Waarom meerdere voorbeelden:
- ✅ Toont verschillende templates
- ✅ Demonstreert optionele parameter (met/zonder
parentPath) - ✅ Helpt ontwikkelaars het juiste template te kiezen
Commits¶
Zie CHANGELOG v0.9.17:
- Template-endpoint-implementatie (controller + service)
- OpenAPI-spec-update (schema's + endpoints)
- Versie-bump naar 0.9.17
Resources¶
OpenAPI-specificatie:
- Officiële spec: https://spec.openapis.org/oas/v3.1.0
- Voorbeelden: https://github.com/OAI/OpenAPI-Specification/tree/main/examples
Tools:
- Swagger Editor: https://editor.swagger.io (online validator)
- OpenAPI Generator: https://openapi-generator.tech (client-generatie)
- swagger-cli: https://github.com/APIDevTools/swagger-cli (validatie)
Nextcloud-development:
- App-development: https://docs.nextcloud.com/server/latest/developer_manual/
- Coding-standards: https://docs.nextcloud.com/server/latest/developer_manual/basics/coding_standard.html
- API-guidelines: https://docs.nextcloud.com/server/latest/developer_manual/basics/controllers.html
IntraVox-docs:
- Quickstart: template-api-quickstart.md
- OpenAPI-tooling: openapi-tooling.md
- API-referentie: api-reference.md
Leer-resources:
- OpenAPI-tutorial: https://oai.github.io/Documentation/
- RESTful-API-design: https://restfulapi.net/
- JSON-schema: https://json-schema.org/learn/