← All writing

ADL vs Strapi

Strapi Interprets Its Schema Too — Until the Content Needs Behavior

TL;DR: Strapi deserves credit most comparisons skip: its content types live as schema files that the runtime interprets into a working API, with no generated controllers and no scaffold to maintain. That’s the same architectural bet ADL makes, and it’s why Strapi is the fastest respectable path from nothing to a content API. The fork comes when content needs behavior. Strapi ships exactly one lifecycle (draft and publish) and answers every other rule with JavaScript: controllers, services, policies, lifecycle hooks. ADL’s position is that the rules are part of the domain, so they belong in the declaration, next to the fields they govern.


The steelman

Strapi earned its place as the default self-hosted headless CMS honestly.

Stand up a project, open the Content-Type Builder, click together an Article with a title, a body, a relation to Author, and Strapi writes a schema file and immediately serves /api/articles: filtering, sorting, pagination, relation population, REST and GraphQL both. Attach roles and per-action permissions in the admin. Give your editors the admin panel (a genuinely good one, with a media library that handles uploads and image processing out of the box), and they’re publishing the same afternoon. Open source, self-hosted on your terms, backed by a plugin marketplace and the entire Node ecosystem.

And notice the architecture under the convenience, because it’s the interesting part. When you build a content type, Strapi does not generate route handlers for you to own. It writes a description (schema.json, attributes and relations and options), and the runtime reads that description and serves the API. Change the schema, the API changes. Within its territory, Strapi is an interpreter, and every Strapi deployment is another working proof that description-executed-directly is a production-grade architecture, not a prototype trick.

This series has now said that about json-server, PostgREST, and JDL. The pattern should be getting suspicious: the industry keeps independently rediscovering that the declaration should be the software — and keeps stopping at the same fence.

The fence: content vs. behavior

Strapi’s vocabulary describes content: what fields exist, what types they are, how entries relate, who may hit which action. Walk one step past structure into behavior and watch where each requirement lands.

Lifecycle. Strapi ships exactly one state machine, hard-coded: draft and publish. It’s a good machine — for publishing. Now model a grant application: draft → submitted → under_review → approved | returned, where only the applicant submits, only a reviewer approves, returned requires a comment, and approval stamps approvedAt. In Strapi, none of that is declarable. You add a status enumeration field, which any authorized update can teleport to any value, and enforce the legal moves in lifecycle-hook code (beforeUpdate) you write, test, and maintain per type. In ADL, that machine is a state_machine: block: transitions, role guards, required fields, effects. A direct write to status either executes a legal transition’s guards or is rejected. The one state machine a CMS bakes in is the tell: lifecycles were always part of the domain. A CMS just picks one for you.

Validation. Field-level basics (required, min/max, unique, regex) are declarable in the schema, and that’s where it ends. Cross-field, cross-record, and state-conditional rules are lifecycle-hook code again: “an article in published state must have an excerpt,” “the reviewer may not be the author.” And errors surface however your hook throws them, not as a uniform contract. ADL declares these rules next to the fields, collects every violation, and returns them at once under the same error envelope every resource uses.

Permissions. Strapi’s model is role × action × content type: solid coarse-grained coverage, managed in the admin. Field-level rules (“HR and the owner read salary; only HR writes it; everyone else never sees the field”) and expression-conditional rules are policy and controller code. ADL declares them per operation and per field, down to expressions, with restricted fields stripped from responses.

Derived values. Computed and aggregated fields (an order total that recalculates when line items change) are custom controllers or services. In ADL they’re computed: declarations that ignore client-supplied values.

Each individual answer is reasonable, and Strapi’s extension points are well-designed as code extension points. But sum the aggregate: the domain’s actual rules (the part an auditor or a new hire most needs to read) live scattered through lifecycles.js, policies/, custom controllers, and services, in imperative JavaScript, while the schema files serenely describe only the shapes. The declaration didn’t fail; it fenced itself at structure, and everything past the fence became code. Same wound as the last three essays, one layer up the stack.

There’s a second, quieter cost: the Content-Type Builder only works in development. Schema changes ship as file edits through your deploy pipeline, and Strapi syncs the database to match. That works, but renames and refactors are on you to migrate safely, with no declared migration history to lean on. ADL treats schema change as part of the interpreted contract, with migration history and rollback in the runtime.

If the boundary needs a picture: ISDG publishes a complete CMS as a single ADL file (cms.domain.yaml, in the seeds), where the content types, the editorial roles, the validation, and the publishing state machine are all the same declaration. The CMS turns out to be a domain you can declare. That’s the whole argument in one artifact.

The honest concessions

The admin panel is product; Kalos renders nothing. Strapi hands non-technical editors a polished UI (content entry, media library, roles) on day one. That interface is most of why teams choose a CMS, and Kalos has no answer to it: the runtime serves APIs and schema, and any editorial UI is yours to build against them. For a marketing site or a newsroom where editors live in the tool daily, Strapi is the better choice and it isn’t close.

The ecosystem is JavaScript. Strapi customization is ordinary Node code your team already writes, drawing on npm without ceremony. ADL’s escape hatch is sandboxed Lua, deliberately constrained and without a package ecosystem, because the sandbox is the point. That trade buys governability and costs familiarity, and pretending it costs nothing would be marketing.

Closer

Strapi interprets your schema and generates nothing — right up until the content needs a rule, and then the rule becomes JavaScript; ADL is the refusal to let the rules leave the declaration.