Skip to content

Backend stacks

Four backends, in three languages, all built to the same contract.

Stack Language What it is Tests
express JavaScript Layered REST API — Joi validation, JWT, Winston, Mocha 32
fastify JavaScript Schema-first REST API — JSON Schema validation, JWT, node:test 28
spring-boot Java Feature-packaged REST API — JPA, Flyway, JWT, Lombok, MapStruct 25
fastapi Python Async REST API — SQLAlchemy, Alembic, Pydantic settings, pytest 29

Those test counts are the suites that ship in the generated project and pass before you touch it.

lattice my-api --stack express     --database postgres
lattice my-api --stack fastify     --database mongodb
lattice my-api --stack spring-boot --port 8080
lattice my-api --stack fastapi

The shared contract

The four are not four unrelated templates that happen to be in one repository. Each ships the same two domainsusers and products — with the same route shapes, the same error envelope, and the same shutdown behaviour, so moving between them is a change of language rather than a change of shape.

products exists specifically to keep the first domain honest. A template with one domain proves nothing about whether its layering generalises: everything can be a special case when there is only one case. Products is the second, and porting it to the other three backends is what surfaced three real bugs that were unreachable before, because the code that exposed them did not exist.

The full contract is CONVENTIONS.md.

The error envelope

Every error, in every backend, in one shape:

{ "error": { "code": "VALIDATION_FAILED", "message": "…", "details": [] } }

A client written against one backend can read the errors of all four.

Authentication and authorisation are separate

One guard answers "is this caller signed in", a second answers "is this caller allowed". Every backend has both, in its own idiom:

Stack Authenticated Authorised
express, fastify requireAuth requireRole
spring-boot Spring Security filter chain @PreAuthorize
fastapi current_user dependency require_admin dependency

A template that ships only the first quietly makes every admin-only operation available to any signed-in user — which is exactly the bug Fastify had until 1.3.0, when it had requireAuth and nothing else.

Storage

express and fastify are storage-agnostic and take --database: six options behind one six-method repository interface, so the routes, controllers, services and the test suite are identical whichever you pick.

src/
  api/users/          routes → controller → service     (never touches a driver)
  database/
    index.js          the seam: database.users, connectDatabase(), ping()
    adapters/
      postgres.js     ← exactly one of these survives scaffolding

Everything above src/database/ imports no driver, which is enforceable by grep: no mongoose / pg / mysql2 import exists anywhere outside src/database/adapters/.

spring-boot and fastapi are not storage-agnostic in this sense — they are built on JPA and SQLAlchemy respectively, which are their own answer to the same question — so they do not take --database.

Health probes

Every backend exposes two, served by different layers on purpose:

GET /api/health/liveness    200 while alive, even while draining
GET /api/health/readiness   503 while draining, or if storage is down

Why they disagree, and why that is the correct behaviour under Kubernetes, is in Health probes and shutdown.

What CI proves about these

storages.yml scaffolds express and fastify against all six databases, installs each, and runs the generated suite — with Postgres, MySQL and Mongo as real containers the CLI starts itself. Twelve jobs. templates-java.yml runs mvn test against a scaffolded spring-boot; templates-python.yml runs pytest against a scaffolded fastapi.

None of that tests the scaffolder by testing the scaffolder. It scaffolds real projects and runs their suites, which is the only thing that can catch a template that is subtly wrong.