JSON, a DSL or components: describing a screen
Server-driven UIOpinion6 min readMiniwall team
Part of the series: Server-driven UI: a field guide for app teams
If your team is small, we'd describe screens as plain, typed JSON with no logic in it, and keep the logic in the app. Move to a small language only when you can name a behavior that must change without a release and can't be answered before the screen is requested. That's an opinion, and the rest of this post is the reasoning, including the cases where it's wrong.
If "server describes the screen, app draws it" is new, start with what server-driven UI is. The wider picture is in the field guide.
The real question is where the logic lives
People argue about three options as if they were rivals. They aren't quite, so here is how we use the words.
- Data. The description says what to draw and nothing else. JSON with a schema is the usual form.
- A DSL. The description can compute: conditions on values, loops, local state, handlers.
- Components. The server picks from a catalog of things the app already knows how to draw, and passes properties.
A component catalog isn't a third choice so much as the vocabulary the other two speak. A JSON screen made of button and stack nodes is components too. So the question that actually separates your options is smaller: may the description compute, or does it only describe?
Plain data: boring on purpose
Data is easy to check. You can validate it against a schema before it goes live, diff two versions, store it, preview it, and roll it back. It can't loop forever, and a bad edit produces a wrong screen, not a hung one.
The cost is repetition and a steady pull to add "just one condition". A designer wants the second price line to disappear for people with a trial. Somebody adds an if field. A quarter later you have a second, worse programming language, and you didn't decide to build one. In our view, that creep hurts more than the format ever does.
What keeps it out is a rule you can state in a sentence: the description holds no logic. If a screen needs a decision, the decision is made somewhere else, and the screen receives the result.
A language: real power, real bill
A DSL fixes repetition and lets a screen react locally, like a pressed state or an expanded row, without a round trip.
Flutter's rfw package is a useful specimen because it sits at the language end and still stops short of code. Its API reference lists constructs for loops, switches, arguments, state and event handlers[2]. Yet its README says remote widget libraries "are declarative and not code, they cannot represent executable closures"[1]. When the code that handles an event is needed, it lives in the app.
If you go this way, we'd budget for the costs people tend to skip:
- Every platform needs an interpreter, and they must agree on every edge case. What does a missing argument do? What does an empty list do? Those answers become a public contract the day an old app meets a new screen.
- You test semantics, not just layout. That's a bigger matrix than "does it draw".
- Authors need to think like programmers, which quietly removes the non-engineers you wanted to let in.
- The format grows features for the next request. Someone has to be allowed to say no.
None of that is a reason to never do it. It's a reason to do it when the need is named, not when it's imaginable. Treat this list as reasoning, not as a report from the trenches.
Components: the catalog is your roadmap
Fixed components give you consistency. A button looks and focuses the same everywhere, accessibility is solved once in the renderer, and the person editing can't break the design system. The rfw README says the package is "best suited for interfaces made out of prebuilt components"[1], and as we read that page it also advises against moving a whole application onto it[1].
The cost is that the server can only arrange what the app already knows. A new component is a new app release for everyone who wants to see it. Your catalog turns into your roadmap, and every entry is a promise to all the old builds still installed.
One judgment call is worth making on purpose: name by meaning or by shape. A pricing_table is easier to author and keeps its look consistent; a pile of stacks, text and images is more flexible and easier to get wrong. Our default would be shapes while you're still learning what the screens need, and meaning-names once the same arrangement has shown up a few times and stopped changing. Meaning-names that arrive too early get stretched with a variant field until they're a language again.
| Plain data | DSL | Fixed components | |
|---|---|---|---|
| Wins when | A small team wants to validate, diff and roll back | Local reactions or heavy repetition are real needs | Consistency and accessibility matter more than flexibility |
| Costs | Repetition, pressure to add conditions | An interpreter per platform, semantics to test | A release for every new component |
| Who can author | Anyone with an editor and a schema | Mostly programmers | Anyone, within the catalog |
| Fails by | Growing a language by accident | Growing a language on purpose, then not maintaining it | A catalog nobody owns |
Swipe sideways to see every column.
A test before you add the condition
Imagine you want the yearly plan first for people who have opened the screen twice. Before it goes into the format, ask where that fact can be answered without the screen. Usually there are two good places. The app knows it and passes a value in. Or the system that decides who sees which screen knows it, and it picks a different screen. If either works, the format doesn't need the condition.
If neither can answer it, and the behavior must change without a release, you may have found a real reason for a DSL. Write that reason down. It's the one you'll need to defend in a year.
When this advice doesn't apply
- Interaction-heavy screens. Drag and drop, gestures tied to animation. By the time data can express it you've built the language anyway, or you should keep the screen native. When not to use remote UI goes through those cases.
- A team that already maintains interpreters. If a platform team owns the runtime and its tests, the DSL bill is mostly paid.
- Content with a fixed shape. If every screen is a headline, an image and a list, you have a CMS problem. Components, nothing more.
Whichever you pick, decide this first
What does an app do with a piece of the description it has never heard of? Skip it, draw a stand-in, refuse the screen? That choice does more to keep you out of trouble than the choice of format, and it's easier to make before the first old build is in the wild. Where server-driven UI helps and hurts covers the surrounding costs.
Next step: write the three conditions you most expect to want, and next to each the place that can answer it without the screen. If every condition has a place, start with data.
Sources
- Miniwall docs, Versioning (components an app does not know) (opens in a new tab)docs.miniwall.app · accessed
- Miniwall docs, Callbacks (onAction, onPurchase) (opens in a new tab)docs.miniwall.app · accessed


