API Design
This document consolidates conventions for Arda’s REST API design: URL naming, required headers, error responses, filtering and pagination, endpoint usage patterns, known limitations, and hard-won gotchas.
URL Naming
Section titled “URL Naming”Network Domains
Section titled “Network Domains”Arda’s platform uses four network domains:
| Domain | Purpose |
|---|---|
app.arda.cards | End-user-oriented applications |
io.arda.cards / api.arda.cards | APIs exposing platform functionality |
auth.arda.cards | OAuth2 authorization endpoints |
Purpose Subdomains
Section titled “Purpose Subdomains”Each Purpose defines subdomains:
<purpose>.<infrastructure>.app.arda.cards<purpose>.<infrastructure>.io.arda.cards<purpose>.<infrastructure>.api.arda.cards<purpose>.<infrastructure>.auth.arda.cardsProduction Entry Points
Section titled “Production Entry Points”The main production system uses canonical short hostnames:
live.app.arda.cardslive.io.arda.cardslive.api.arda.cardslive.auth.arda.cardsAPI Route Pattern
Section titled “API Route Pattern”API endpoint routes follow:
https://<purpose>.<infrastructure>.api.arda.cards/<major-version>/<endpoint-name>/<specific-route>Where:
<major-version>is the SemVer major version of the API definition (not the module or component version)<endpoint-name>is the functional name of the endpoint, following REST naming conventions, in hyphenated lower-kebab-case plural form (e.g.,lookup-suppliers,kanban-card)<specific-route>is the route as understood in OpenAPI specification
Route Naming Consistency
Section titled “Route Naming Consistency”Existing routes use hyphenated, plural naming: lookup-suppliers, lookup-units. New routes must follow the same pattern.
Required Headers
Section titled “Required Headers”Every API call requires:
Authorization: Bearer <token>X-Author: <author-uuid>X-Tenant-Id: <tenant-uuid>X-Request-ID: <fresh-uuid-per-request> # Must be valid UUID formatContent-Type: application/jsonCritical notes:
- Missing
X-Request-IDcauses a misleading400error about “Invalid UUID format” - Missing
X-Tenant-Idalso causes400 - Generate a fresh UUID for each request (e.g.,
$(uuidgen)in bash)
Error Responses
Section titled “Error Responses”HTTP Status Codes
Section titled “HTTP Status Codes”All error responses use standard HTTP status codes:
Client Errors (4xx):
400 Bad Request— Invalid request payload, malformed JSON, missing required parameters401 Unauthorized— Missing or invalid authentication token403 Forbidden— Authenticated user lacks permission404 Not Found— Requested resource does not exist405 Method Not Allowed409 Conflict— Conflict in current resource state (e.g., edit conflict, update without draft)422 Unprocessable Entity429 Too Many Requests
Server Errors (5xx):
500 Internal Server Error— Unexpected server error501 Not Implemented502 Bad Gateway503 Service Unavailable504 Gateway Timeout
Error Response JSON Format
Section titled “Error Response JSON Format”All error responses conform to the ErrorResponse data class:
@Serializabledata class ErrorResponse( override val responseMessage: String, override val code: Int, // HTTP Status Code override val details: JsonElement? = null) : Throwable(...), HttpResponseThe details field can contain:
- Simple Cause: JSON representation of a cause
ErrorResponse - Contextual Information (
SingleDetails):data class SingleDetails(val error: ErrorResponse,override val context: String?): ErrorDetails - Composite Errors (
CompositeDetails):data class CompositeDetails(override val context: String?,val errors: List<ErrorResponse>): ErrorDetails
Example composite error JSON:
{ "responseMessage": "Multiple errors occurred", "code": 500, "details": { "context": "Validating user input", "errors": [ { "responseMessage": "username is Invalid: cannot be empty", "code": 400, "details": null }, { "responseMessage": "email is Invalid: must be a valid email format", "code": 400, "details": null } ] }}Ktor StatusPages Integration
Section titled “Ktor StatusPages Integration”The system uses Ktor’s StatusPages plugin for centralized exception handling:
install(StatusPages) { exception<Throwable> { call, ktorExc -> val toProcess = if(ktorExc.message == ktorExc.cause?.message) ktorExc.cause else ktorExc val response = toProcess.toErrorResponse() app.log.warn("Error: {}", call.request.path(), toProcess) call.respond(status=response.httpCode, message=response) }}Any unhandled Throwable is caught and normalized through toErrorResponse(). Throwing AppError subtypes is the preferred pattern:
get("/my-resource/{id}") { val id = call.parameters["id"] ?: throw AppError.ArgumentValidation("id", "ID path parameter is missing") val resource = fetchResource(id) ?: throw AppError.NotFound("MyResource", context = { "Resource ID: $id" }) call.respond(resource)}For functions returning Result<T>, use Result.getOrThrow() to propagate to StatusPages.
Data Types at API Boundaries
Section titled “Data Types at API Boundaries”Strong Types Preferred
Section titled “Strong Types Preferred”Prefer strong types (UUID/EntityId) over String at API boundaries to reduce parsing boilerplate.
Money vs Quantity
Section titled “Money vs Quantity”The Money type uses value (not amount) for the numeric field:
// CORRECT"unitCost": { "value": 0.15, "currency": "USD" }
// WRONG — returns 400"unitCost": { "amount": 0.15, "currency": "USD" }The Quantity type uses amount and unit:
"quantityPerOrder": { "amount": 100, "unit": "EA" }Strict Integer Types for Quantities
Section titled “Strict Integer Types for Quantities”The API is strict about types for Quantity objects. Float values fail:
{"amount": 1.0, "unit": "each"} // Returns 400{"amount": 1, "unit": "each"} // CorrectField Naming Consistency
Section titled “Field Naming Consistency”Watch for camelCase variations — defaultSupplyEid (lowercase ‘id’) vs supplyEId.
Serialization
Section titled “Serialization”For kotlinx.serializable data classes with UUID fields, the @Contextual annotation is required.
Filtering and Query Endpoints
Section titled “Filtering and Query Endpoints”Query Route Pattern
Section titled “Query Route Pattern”The /query sub-path supports filtering, sorting, and pagination:
- Root path:
/<version>/<resource>(e.g.,/v1/items) - Query sub-path:
/<version>/<resource>/query(e.g.,/v1/items/query)
Initiating a Query
Section titled “Initiating a Query”POST /<version>/<resource>/query with a JSON Query object body:
{ "filter": { "EQ": { "locator": "status", "value": "ACTIVE" } }, "sort": [ { "field": "name", "direction": "ASC" } ], "paginate": { "index": 0, "size": 20 }}Response is a PageResult:
@Serializabledata class PageResult<P: EntityPayload, M : PayloadMetadata>( val thisPage: String, val nextPage: String, val previousPage: String?, val results: List<EntityRecord<P, M>>)Page Navigation
Section titled “Page Navigation”GET /<version>/<resource>/query/{page} — retrieve a specific page using a token from nextPage or previousPage. Returns the same PageResult structure.
If {page} cannot be decoded to a proper Query, returns an Argument Validation error.
Query Behavior
Section titled “Query Behavior”- The
/queryendpoints return only non-retired, visible records by default - An empty result does not mean no data exists — verify with direct
GET /entity-idcalls - The
filterparameter is optional; omitting it returns all records - Query parameter defaults:
effectiveAsOfandrecordedAsOfdefault toTimeCoordinates.now()when absent
See Query DSL for filter syntax.
Locator Naming
Section titled “Locator Naming”Universes configured with EntityServiceConfiguration accept locators as JSON field paths (camelCase, e.g., identity.email, cardQuantity.amount) in addition to raw database column names (snake_case, e.g., identity_email, card_quantity_amount). The structured translator resolves both forms, so API clients can use whichever style matches their context. See Query DSL: EntityServiceConfiguration.
CRUD Operations
Section titled “CRUD Operations”Item Update Workflow (Draft → Publish)
Section titled “Item Update Workflow (Draft → Publish)”Updating an item requires a strict Draft → Publish workflow due to the bitemporal model:
- Get/Create Draft:
GET /v1/item/item/{eId}/draft - Publish:
PUT /v1/item/item/{eId}with the Item payload as the body
Calling PUT /item/{eId} without a draft existing returns 400 Cannot update... without a draft.
Delete Semantics
Section titled “Delete Semantics”Delete is a logical retirement, not physical removal — data is preserved to maintain bitemporal history. The response includes "retired": true.
Items with kanban card “successors” in the bitemporal timeline cannot be deleted. The API returns: "Record[ITEM] cannot be deleted because it has a successor". Retire items instead, or delete all associated cards first.
Lookup Endpoints
Section titled “Lookup Endpoints”The /lookup-* endpoints (suppliers, facilities, units, types, subtypes, usecases, departments) are typeahead/autocomplete endpoints. They require a name query parameter and return matches, not full lists. Use the /query endpoints for full data listing.
API Response Structure
Section titled “API Response Structure”All API responses wrap data in a payload property. For queries:
payload.datais the results arraypayload.totalis the count
The locator field on items is a nested object {facility, department, location, subLocation}, not a flat string.
Known Limitations
Section titled “Known Limitations”Query Limitations
Section titled “Query Limitations”- Query Object Complexity: Clients are responsible for constructing the JSON
Queryobject; errors produce runtime failures, not schema validation errors. - Page Token Security: Current page tokens are strings (serialized
Query); no integrity checking is implemented. - Performance: Query performance depends on
Universeimplementation and database indexing. No projection support — fullEntityRecordis always returned. - No Full-Text Search: The filter mechanism is for structured data only.
- No Aggregations: No standard mechanism for
COUNT,SUM,AVGvia query API. - GET Pagination: Filters only apply on the initial
POSTrequest;GET /query/{page}does not accept a filter body.
Kanban Card Locators
Section titled “Kanban Card Locators”Universes with EntityServiceConfiguration accept both camelCase JSON field paths and snake_case column names as locators. For universes not yet configured with a structured translator, only raw column names are accepted — test before relying on camelCase paths.
Orphaned References
Section titled “Orphaned References”Orphaned card references (cards that reference deleted items) can cause 500 errors on the /details endpoint.
Browser Refresh After Mutations
Section titled “Browser Refresh After Mutations”The frontend does not automatically poll for data changes. After API mutations, manual browser refresh is required to see changes in the UI.
Copyright: © Arda Systems 2025-2026, All rights reserved