Chapter 17 · Volume II — The ADL Domain Language

API Configuration

17.1 Overview

The api: block on a resource customizes how the resource is exposed through the REST transport. It controls the URL path, which operations are enabled, and what custom endpoints are available.

A resource with no api: block uses all defaults: auto-generated path, all five CRUD operations enabled, no custom operations.

resources:
  Post:
    api:
      path: /articles
      disable_delete: true
      customOperations:
        - name: publish
          method: POST
          path: /:id/publish
          permissions: [editor, admin]

17.2 Path Override

The path key overrides the auto-generated URL path for the resource.

api:
  path: /items

Default Path Generation

When path is omitted, the path is derived from the resource name: PascalCase → kebab-case → pluralized.

Resource NameDefault Path
Post/posts
OrderItem/order-items
Category/categories
SiteSettings/site-settings
Person/people

Full URL

The full URL for a resource combines the domain prefix and the resource path:

/api/v1/{domain}/{resource-path}

For a domain named blog with a resource Post using the default path:

/api/v1/blog/posts

With path: /articles:

/api/v1/blog/articles

The domain prefix (/api/v1/{domain}) is configured at the domain level (§6.2) and is not affected by the resource’s path override.


17.3 Operation Disabling

Individual CRUD operations can be disabled. A disabled operation returns 404 as if the route does not exist.

api:
  disable_list: true
  disable_get: false
  disable_create: true
  disable_update: false
  disable_delete: true
KeyDefaultSuppresses
disable_listfalseGET / (list endpoint)
disable_getfalseGET /:id (get endpoint)
disable_createfalsePOST / (create endpoint)
disable_updatefalsePATCH /:id and PUT /:id (both update methods)
disable_deletefalseDELETE /:id (delete endpoint)

An alternative array syntax is also accepted:

api:
  disable: [list, create, delete]

Both forms produce identical behavior. The boolean form is preferred for clarity when disabling one or two operations. The array form is more compact when disabling several.

Disabled Operations and Permissions

Disabling an operation is stronger than setting its permission to [] (nobody). A disabled operation has no route — the URL returns 404. A permission of [] has a route that always returns 403. Disabling is appropriate when the operation is conceptually invalid for the resource (e.g., you cannot create a singleton). Permission restriction is appropriate when the operation exists but is limited to specific users.


17.4 Singleton Pattern

The singleton pattern exposes a resource that has exactly one record. It is implemented by disabling list, create, and delete:

resources:
  SiteSettings:
    api:
      disable_list: true
      disable_create: true
      disable_delete: true

The single record is created by the seeder at boot time. API consumers access it via:

GET /api/v1/cms/site-settings/:id
PATCH /api/v1/cms/site-settings/:id

Singleton Behavior

OperationAvailable?Description
ListNo list endpoint — there is only one record
GetRetrieve the single record by ID
CreateRecord created by seeder only
UpdateModify the single record
DeleteCannot delete the singleton

UI Pattern

A UI for a singleton resource skips the data table entirely and renders the edit form directly, pre-populated with the record’s current values. The record’s ID is obtained from the schema endpoint or hardcoded from the seed data.


17.5 Custom Operations

Custom operations define non-CRUD endpoints on a resource. They are declared in the customOperations array:

api:
  customOperations:
    - name: publish
      method: POST
      path: /:id/publish
      permissions: [editor, admin]
      fields:
        status: published
        publishedAt: "NOW()"

    - name: duplicate
      method: POST
      path: /:id/duplicate
      permissions: [owner, admin]

    - name: bulk-activate
      method: POST
      path: /bulk-activate
      permissions: [admin]

Declaration

KeyTypeRequiredDefaultDescription
namestringyesOperation identifier. Used in action names and route registration.
methodstringno"POST"HTTP method: POST, PUT, PATCH, GET, DELETE.
pathstringno/:id/{name}URL path relative to the resource base.
permissionsstring[]no[admin]Role array for access control.
fieldsobjectno{}Field values to auto-set when the operation executes.

Generated Route

Each custom operation registers a route:

POST /api/v1/orders/orders/:id/publish    → adl.orders.order.publish
POST /api/v1/orders/orders/:id/duplicate  → adl.orders.order.duplicate
POST /api/v1/orders/orders/bulk-activate  → adl.orders.order.bulk-activate

Fields Auto-Set

The fields object defines values that are applied to the record when the operation executes, similar to state machine sets:

fields:
  status: published
  publishedAt: "NOW()"

NOW() is replaced with the current UTC timestamp. Other supported expressions match state machine sets behavior (§12.5).

Request Body

Custom operation endpoints accept a JSON request body for additional field values (subject to field-level permissions). The fields auto-set values override any client-provided values for the same fields.

Implementation

Custom operations with fields are handled by the ADL engine’s built-in action handler — no Lua script is needed. For operations that require complex logic beyond field updates, the operation’s name serves as the hook identifier, and a Lua script can subscribe to the corresponding bus action:

adl.{domain}.{resource}.{operationName}

17.6 Operation Overrides

Standard CRUD operations can have their HTTP method or URL path overridden:

api:
  operation_overrides:
    update:
      method: PUT
      path: /:id/edit

    delete:
      method: POST
      path: /:id/remove

Supported Operations

OperationDefault MethodDefault PathOverridable
listGET/method, path
getGET/:idmethod, path
createPOST/method, path
updatePATCH + PUT/:idmethod, path
deleteDELETE/:idmethod, path

When method is overridden on update, both PATCH and PUT are replaced with the specified method. When path is overridden, the new path is relative to the resource base.

Use Cases

  • Legacy API compatibility: Override paths and methods to match an existing API contract that the ADL domain is replacing.
  • Convention alignment: Some teams prefer PUT for all updates instead of PATCH.
  • URL structure: Override delete to POST /:id/remove for systems that don’t support DELETE method.

Operation overrides are rare. Most domains use the default methods and paths. Overrides exist for backward compatibility, not as a preferred pattern.


17.7 Nested Resource Paths

A resource MAY declare that it is accessed through a parent resource’s URL:

resources:
  Comment:
    api:
      under: Post
      path: /posts/:postId/comments

Behavior

When under is declared, the resource’s routes are nested under the parent’s path:

GET    /api/v1/blog/posts/:postId/comments          → list comments for post
GET    /api/v1/blog/posts/:postId/comments/:id       → get comment
POST   /api/v1/blog/posts/:postId/comments           → create comment (postId auto-filled)
PATCH  /api/v1/blog/posts/:postId/comments/:id       → update comment
DELETE /api/v1/blog/posts/:postId/comments/:id       → delete comment

The postId path parameter is automatically used as the FK value on create — the client does not need to include it in the request body.

Direct Access

A resource with under MAY also expose direct routes at its own path. This allows both nested and direct access:

resources:
  Comment:
    api:
      under: Post
      directAccess: [get]

This generates:

GET /api/v1/blog/comments/:id              → direct get by comment ID
GET /api/v1/blog/posts/:postId/comments    → nested list under post

Requirements

  • The resource MUST have a belongsTo relationship to the parent resource specified in under.
  • The FK field on the resource MUST match the parent’s ID path parameter.
  • Nested routes do NOT affect the resource’s table structure — it is the same database table regardless of how it is accessed via URL.

17.8 Domain-Level API Configuration

The domain itself has an api: block that configures the URL prefix:

domain:
  name: blog
  version: "1.0.0"

  api:
    prefix: /api/v1/blog
    aliases:
      - /api/blog
      - /blog
KeyTypeDefaultDescription
prefixstring/api/v1/{domain.name}Base URL prefix for all resource routes.
aliasesstring[][]Alternative prefixes that also route to the domain’s resources.

When prefix is omitted, the default is /api/v1/{name} where {name} is the domain’s name in lowercase.

Aliases register duplicate routes at alternative paths. This is useful for migration scenarios where clients use different URL conventions:

/api/v1/blog/posts        → primary prefix
/api/blog/posts            → alias
/blog/posts                → alias

All three URLs resolve to the same action handler.


17.9 Schema Endpoint

The schema introspection endpoint returns the full API configuration:

{
  "api": {
    "path": "/articles",
    "disable_list": false,
    "disable_get": false,
    "disable_create": false,
    "disable_update": false,
    "disable_delete": true,
    "customOperations": [
      {
        "name": "publish",
        "method": "POST",
        "path": "/:id/publish",
        "permissions": ["editor", "admin"]
      }
    ],
    "operation_overrides": {}
  }
}

A UI generator uses this data to determine which CRUD buttons to render, which custom action buttons to add, and what the endpoint URLs are.


End of Chapter 17.

Next: Chapter 18 — Content & Media Types