Give your backend a design system

Give your backend a design system

Why should design have all the fun?

Contents #

The core need #

To be clear, a rigorous API spec that generates all sorts of things is not a new concept (add footnote to prior art here). But its importance, position, and implications are rapidly changing in an AI-native world.

The core need is thus:

Normally you might just copy this information all over the place, or erect a team to manage it…

But in a super-fast-changing and increasingly guardrailed environment, that just isn’t going to work.

Getting carried away… #

So you send your trusty AI coding agent to generate a bunch of schemas for you based on your models, SQL tables, etc.

Buuuttt if you give a mouse a cookie…

He’s probably going to want to generate integration tests, admin / frontend code, client libraries, documentation, AI-evals, and of course make such a schema accessible for all AI agents across your company to access (securely, of course!) for all such cases.

And before long you realize that the schemas are the code — in a very real sense — and should probably be the source of truth.1

This pattern isn’t just for data models: cross-platform test cases, design / branding systems, documentation / strings — hell, even accounting, legal, sales, HR, and others are rapidly converging on concise, canonical, and most importantly, AI-first specs or schemas. This is what tomorrow’s AI-native business will look like.

But we’ll stay focused on data + method schemas for today:

Myriad benefits #

As you go further down this rabbit hole, you realize just how many parts of your system can benefit from this.

So how do we get started?

But first, an example #

Emulating the great Rob Pike3, let’s show a detailed example in place of a lot more words.

# Similar in spirit to JSON Schema, but our own simple format that allows comments and
# addresses our needs (and of course is fully tested!).
title: Files
id: files

# AI/prompt-first description
description: |
  Content-addressed file storage. A file row IS the file: id is the SHA1 hex
  hash of the bytes, bytes live at ...

# For API/MCP routing (can be omitted; defaults to /files from id above)
service_path: /files
# Set to `-` for non-db-backed models
pg_table_name: file
# Defaults to `user`. Properties and methods can also specify role.
role: user

# Always in alpha order
model_properties:
  - id: checksum_verified_at
    type: date_time
    description: When file integrity was last verified (null if never)
  - id: content_draft
    type: text_long
    description: In-progress bytes for streaming files (voice recording). NULL when not streaming.
  - id: content_type
    type: text_mime
    description: Detected MIME type (e.g. image/png, application/pdf)
  - id: created
    type: date_time
    description: UTC timestamp when uploaded
  - id: id
    type: guid
    description: Standard GUID — primary key
  - id: metadata
    type: object_any
    description: Extracted metadata (EXIF, PDF info, provider response, ...)
  - id: original_name
    type: text_short
    description: Original filename as uploaded
  - id: parent_id
    type: guid
    description: Generic parent link (NOT for versioning)
  - id: private
    type: bool
    description: |
      Owner-only file. When true (paired with list_config.privacy_aware),
      even admin list / drill / user_id=all never surfaces this row —
      only the owning user_id sees it. Default false.
    default: false
  - id: size_bytes
    type: int_nonneg
    description: File size in bytes
  # ... more props

# Behavior declared in schema (not just shape) — auto-CRUD honors these
list_config:
  default_limit: 50
  max_limit: 200
  sortable_fields: [created, updated, size_bytes]
  default_sort: created desc
  user_scoped: true         # rows scoped to caller by default
  admin_default_scope: all  # admin list spans all users unless drilled
  privacy_aware: true       # honors per-row `private` flag even for admin

methods:
  # Auto-CRUD --------------------------------------------------------------
  - id: get
    description: Fetch a single file row by id (SHA1 hex)

  - id: list
    description: List the caller's files, newest first
    properties_optional: [group_id, status, source]

  - id: patch
    description: Update mutable metadata. Content is immutable.
    properties_optional:
      [date_effective, description, geo_alt, geo_lat, geo_lng, geo_location,
       metadata, parent_id, status, tags, tags_project, title, transcript]

  - id: delete
    description: Soft-delete (sets status=deleted). Bytes remain on disk.

  # Custom -----------------------------------------------------------------
  - id: classify
    description: |
      Retry Haiku classification for an existing file. Admin / debug —
      classification runs automatically on user uploads.
    http_method: POST
    path: /files/classify
    role: admin
    params:
      - id: id
        type: sha1_hex
        required: true
        description: File id (SHA1 hex)
    errors: [not_found, internal_error]

Notice what’s there besides shape: routing, RBAC, list defaults, privacy semantics, the difference between auto-CRUD and custom methods. That’s the schema doing real behavioral work, not just describing fields.

Hardcore verification #

One of my first thoughts upon becoming obsessed with GPT-3 was that, if nothing else, the extra power from LLM-coding should allow us to build the engineering org/team/stack we’ve always dreamed of but never had the time or budget to build beforehand.

Hardcore verification across all layers is a major part of this, and Claude excels at this kind of work.

In our case, the layout is simple:

Our schemas are used to generate client libraries and tests, and used in various other capacities like generating admin tools with their own suite of tests.

The compounding effect is the real prize: each new test added to shared routing or validation strengthens every service in the system. You’re not testing one feature; you’re testing the platform.

Custom types: a real powerhouse #

Traditional validation params are nice, and work well (enum, min, max, etc), but they can be verbose — several fields — and not always specific enough.

A quick note on admin tools #

With this approach, it’s incredibly easy to generate entire admin/internal tools — including writes. But you might want to consider if you really need that tool.

Only you can say. :)

Appendix #

1. Prior art: schemas as source of truth is not new #

2. What stays as code #

A skeptical reader is rightly asking this from paragraph one. Schemas describe the surface; code still implements the behavior. The split, concretely:

The schema-as-source-of-truth claim is not "no code." It’s "the boilerplate that used to be 80% of a service is now generated, and the 20% that’s actually interesting is what you write."

Use caution when using legacy code generators. In our experience, these are more hassle than they’re worth; LLMs do this with ease given one or two good examples (have the LLM generate everything based on the schema).

3. Other fields and controls #

4. References / further reading #

Additional reading:


Footnotes #

  1. See appendix §2 — "What stays as code." TL;DR: schemas describe the surface; code still implements behavior, side effects, and anything stateful.
  2. Side benefit worth its own post: with auth declared in schema, qualitative AI security review becomes tractable — an agent can audit every method’s role + scoping rules in one pass.
  3. Rob Pike’s "hello, world" / Brian Kernighan lineage — for the "small example beats a thousand words" principle.
  4. Buf — "The real reason to use Protobuf is not performance." buf.build/blog/the-real-reason-to-use-protobuf. The real value isn’t speed; it’s an enforceable, versioned, language-agnostic contract between producers and consumers.
  5. AWS Smithy. smithy.io. Protocol-agnostic IDL, v2.0 (2022); descended from internal "Coral." Every AWS SDK generated from it. Core concept is @trait metadata.
  6. Microsoft TypeSpec (formerly Cadl). typespec.io. Concise TypeScript-flavored DSL that compiles down to OpenAPI, Protobuf, JSON Schema, and experimental clients / servers.