Orders API

Orders API

>

The Forgotten Pattern That Still Works

Start with your data model. Let it shape your API. Generate the CRUD boilerplate automatically. Add your business logic on top. Never let AI touch the core layer.

This is the workflow one developer describes as the foundation of their backend process: “I start modeling my schema and then take it from there, and it has worked for me. I find that it speeds up development, adding a new entity or field is not as painful, and I fully control the core part of the application.”

The key insight here is subtle but critical: the database schema is not the business logic. It’s the foundation the business logic sits on. That distinction changes everything about how you structure your backend.

Approach Source of Truth Strengths Weaknesses
Schema-First DB schema or OpenAPI spec Consistency, auto-generated boilerplate, enforcement Can couple frontend to DB structure
Code-First Application models Flexibility, DDD alignment Drift between code and schema
API-First OpenAPI/Smithy contract Team coordination, parallel dev Spec maintenance overhead
Domain-Driven Design Bounded contexts Business logic fidelity Complex, overkill for CRUD apps

Why Schema-First Works Better Than You Remember

1. The Sync Problem Disappears

When the schema is the source of truth, you never wonder whether your code matches your database. One developer puts it succinctly: “when the schema is the source of truth, u dont have to worry about sync issues between ur db n ur code, its litrally the cleanest way to work imo.”

This isn’t just about convenience. Every hour spent debugging a production incident caused by a migration that didn’t match the application code is an hour that wouldn’t have happened if the schema were the driver.

2. Generated Code Is a Feature, Not a Hack

The critique against schema-first often comes down to “but you’ll generate spaghetti code.” That’s a tooling problem, not a pattern problem. Modern schema-first workflows are sophisticated:

# openapi.yaml,  OpenAPI 3.1
openapi: "3.1.0"
info:
  title: Orders API
  version: "1.0.0"
  description: >
    Contract-first REST API for order management.
    This file is the source of truth, implementation is derived from it.

From this spec, you generate TypeScript types, server stubs, mock servers, and even client SDKs. The generated code never needs to be hand-transcribed. When the spec changes, the generated output changes automatically. As one source notes, “Spec-Driven Development mandates a machine-readable contract (OpenAPI or GraphQL) as the single source of truth before a single line of application code is written.”

3. The AI Boundary Is a Business Decision

Here’s the spicy part: this approach explicitly avoids AI in the core logic layer. That’s not Luddism, it’s pragmatism. When your database schema and CRUD operations are generated from a contract, you don’t need an LLM hallucinating your endpoints. The schema itself is the specification.

AI becomes useful for the product layer on top, the business logic, the complex workflows, the things that actually benefit from pattern recognition. The core data handling stays deterministic.

The Architecture That Makes It Work

The secret to making schema-first work at scale is a layered architecture that separates concerns without losing the schema as the anchor:

[ Database Schema ] → [ Generated CRUD Library ] → [ Product Logic Layer ] → [ Frontend ]
     (Source of Truth)     (Auto-generated, deterministic)     (Hand-crafted, AI-optional)

One developer who has built systems this way describes the approach: “I generate all the code for the CRUD operations, listing and so on, and then use that as a library and add my product logic on top.”

The generated CRUD layer becomes infrastructure. Your actual application logic sits on top of it, completely decoupled from the schema itself. When you add a field to the database, the CRUD layer regenerates, but the product logic only needs to change if it cares about the new field.

The CI Gate That Keeps Everything Honest

This isn’t a “trust me, it works” architecture. It’s enforceably correct. A proper schema-first pipeline includes a CI gate that blocks any PR where the spec and implementation drift:

# .github/workflows/contract.yml
name: Contract Sync Gate

on:
  pull_request:
    paths:
      - "openapi.yaml"
      - "src/**"

jobs:
  contract-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Lint OpenAPI spec
        run: npx spectral lint openapi.yaml --fail-severity warn
      - name: Detect breaking changes
        run: npx openapi-diff base-spec.yaml openapi.yaml --fail-on-incompatible
      - name: Regenerate types and confirm no drift
        run: |
          npx openapi-typescript openapi.yaml --output src/generated/api.d.ts
          git diff --exit-code src/generated/api.d.ts

This gate catches: breaking changes to the contract, missed type regenerations, spec lint violations, and any hand-edited generated files that don’t match. The CI output for a clean PR looks like:

✔ spectral lint passed (0 errors, 0 warnings)
✔ openapi-diff: no breaking changes detected
✔ openapi-generator validate: spec is valid OAS 3.1
✔ src/generated/api.d.ts matches openapi.yaml

Where Schema-First Gets Dangerous

No architectural pattern is a silver bullet. Schema-first has real failure modes that you need to anticipate.

The Coupling Trap

The most dangerous anti-pattern is exposing your database schema directly to your frontend. As one developer warns, “I would very much not recommend tying your frontend logic to your database schema. That way lies misery.”

The solution is the layered approach mentioned earlier. The frontend talks to the product layer, not the generated CRUD layer. The database schema can change without breaking the frontend if the product layer abstracts the differences.

The Complexity Ceiling

Schema-first shines for CRUD-heavy, data-centric applications. But when business rules become genuinely complex, the schema can’t drive everything. One developer who has gone back and forth on the approach notes: “The problems usually start when business rules get more complex. At that point I prefer the schema to define the data model, but not necessarily drive every layer of the application.”

The rule of thumb: if your application is 80% CRUD with some business logic on top, schema-first is optimal. If it’s 80% complex business logic with some CRUD, you need a different approach.

The Data Modeling Skill Gap

Ruby on Rails popularized the schema-driven approach with ActiveRecord, but it also exposed a critical weakness: data modeling is not a skill that’s emphasized in modern web development education. As one Rails developer points out, the danger is that “data modelling is not a skill that is emphasised in Rails education (or maybe it’s more fair to say in any web framework) and it’s really critical long term to get this right.”

If you can’t model data well, schema-first will amplify your mistakes rather than prevent them.

Schema-First vs. The Modern Landscape

When Spec-Driven Development Is the Right Call

The spec-driven development approach, where a machine-readable contract is the source of truth, works best for:

  • High-velocity teams that need to ship APIs fast and consistently
  • Data-centric applications where the database naturally drives the architecture
  • Microservice environments where service contracts must be maintained across teams
  • Internal tools and admin panels where CRUD operations dominate

When You Should Absolutely Not Do This

  • Event-sourced systems where the current state is derived, not stored
  • High-complexity domains with intricate business rules that can’t be derived from data structure
  • Systems requiring CQRS where read and write models are intentionally different
  • When your data model is unstable and still undergoing major discovery

The Object-Relational Impedance Mismatch

One critique of schema-first is that it forces an ORM-like layer that can drag performance. But modern tooling addresses this. As one developer explains, their approach “doesn’t use an ORM it uses sqlc that is much more efficient.” The generated code is raw SQL via type-safe query builders, not a heavyweight ORM that obscures what’s happening.

Practical Steps to Adopt Schema-First Today

If you want to try this approach without rewriting your entire backend:

Start with a single bounded context. Identify the most CRUD-heavy part of your system and model its schema first. Generate the CRUD layer. See how the product logic sits on top.

Use tools that enforce the contract. Lint with Spectral, generate types with openapi-typescript, break changes with openapi-diff. The tooling is mature enough to make this a CI-enforceable pattern.

Resist the temptation to start with code. The whole point is that the schema comes first. Write the spec. Lint it. Mock it. Then write implementations.

Watch for the warning signs of poor schema design. If you find yourself adding the 47th boolean column to your user permissions table, you’ve got a schema design problem that schema-first won’t fix, it’ll just make it faster to create bad structures.

The Bottom Line

The database schema as source of truth isn’t for every project. But it’s a legitimate pattern that the industry abandoned too quickly in favor of code-first dogma.

The real cost of code-first is the implicit contract between your application and your database that nobody ever documents. When that contract breaks at 3 AM on a Saturday, the schema-first teams are already asleep, because their contract is explicit, machine-validated, and unbreakable.

And yes, you can still use AI. Just not in the core layer. Put it in the product logic where it belongs.

The database outlives the application. Shouldn’t its structure be the thing everything else is built around?

Share:

Related Articles