BFF Pattern with Spring Boot and Specmatic: Contract-First Backend
Your frontend team is blocked. They need user data to build the dashboard, but the backend API doesn’t exist yet. Backend is still debating database schemas. Everyone waits.
Two weeks later, both teams finish. Integration day arrives. The frontend expected userId, backend returns user_id. The frontend expected an array, backend returns paginated results. Another week of fixes.
The BFF (Backend for Frontend) pattern with contract-first development eliminates this. Define the API contract upfront. Frontend and backend develop in parallel against the same spec. Integration becomes boring—because the contract guarantees compatibility.
+-----------------------------------------------------------+
| PARALLEL DEVELOPMENT |
| |
| OpenAPI Spec (shared contract) |
| | | |
| v v |
| +-------------+ +-------------+ |
| | Frontend | | Backend | |
| | develops | | implements | |
| | against | | against | |
| | Specmatic | | spec | |
| +-------------+ +-------------+ |
| | | |
| +------ Integration -+ |
| (boring, no surprises) |
+-----------------------------------------------------------+
+-----------------------------------------------------------+
| TESTING RESPONSIBILITY |
| |
| Specmatic -----------------> validates SHAPE |
| Spring Boot tests ----------> validates LOGIC |
| |
| Don't mix them. |
+-----------------------------------------------------------+
The key insight: Specmatic validates shape (field names, types, structure). Spring Boot tests validate logic (filtering, sorting, business rules). Keep them separate.
What’s a BFF?
Frontend (React) ──▶ BFF (Spring Boot) ──▶ Backend Services
The BFF translates between frontend needs and backend services.
Does:
- Aggregate multiple backend calls
- Transform data shapes
- Handle auth at the edge
Doesn’t:
- Business logic (backend’s job)
- Data persistence (backend’s job)
Keep it thin.
The Key Insight
Specmatic validates shape. Spring Boot tests validate logic.
| Tool | Tests |
|---|---|
| Specmatic | Field names, types, response structure |
| Spring Boot | Filtering, sorting, aggregations, business rules |
Don’t ask Specmatic to do logic. Don’t skip integration tests because you have contracts.
The Proxy Switch
One env var switches between mock and real:
const MOCK_MODE = process.env.MOCK_MODE === "true";
const target = MOCK_MODE ? "http://localhost:9002" : "http://localhost:8080";
app.use("/api", createProxyMiddleware({ target, changeOrigin: true }));
MOCK_MODE=true npm run dev # Frontend → Specmatic
MOCK_MODE=false npm run dev # Frontend → Spring Boot
Frontend code doesn’t change. Zero conditional logic.
Spring Boot Controller
@RestController
@RequestMapping("/users")
public class UsersBffController {
@PostMapping("/query")
public QueryResponse queryUsers(@RequestBody QueryRequest request) {
List<User> users = userService.findAll(request.getFilters());
return QueryResponse.builder()
.data(users)
.pagination(buildPagination(request, users))
.build();
}
}
The controller matches the OpenAPI spec. Specmatic validates this.
Contract Test
@SpringBootTest(webEnvironment = RANDOM_PORT)
class ContractTest {
@LocalServerPort
private int port;
@Test
void contractsAreValid() {
Results results = Specmatic.test(
"specs/users-api.yaml",
"http://localhost:" + port
);
assertThat(results.success()).isTrue();
}
}
Specmatic throws every contract example at your running app. Mismatches fail the test.
What Specmatic Can’t Do
Static responses only.
| Want | Reality | Solution |
|---|---|---|
| Filter by status | Same response | Client-side filter in dev |
| Sort by name | Same response | Client-side sort in dev |
| Dynamic pagination | Same response | Client-side slice in dev |
The contract proves shape. The backend implements logic.
// Works in both modes
const { data } = await response.json();
// Client-side filtering (dev only)
const filtered = IS_DEV ? data.filter(u => u.status === selectedStatus) : data; // Backend already filtered
What to Avoid
Dynamic filters in contracts:
// DON'T - Specmatic matches exact bodies
{ "filters": [{ "field": "status", "value": "active" }] }
Different filter = no match = 404.
Computed values:
// DON'T - Specmatic can't compute
{ "metrics": { "totalCost": 15234.5, "avgLatency": 245.3 } }
These need real data. Test with integration tests.
Stateful operations:
// DON'T - Specmatic has no state
POST /users → creates user-123
GET /users/user-123 → needs its own contract
Layer Your Tests
Contract Tests (Specmatic)
└─▶ Shape: field names, types, structure
↓
Integration Tests (Spring Boot)
└─▶ Logic: filtering, sorting, aggregations
↓
E2E Tests
└─▶ Full flow: deployment, infra
Contract tests on every PR. Integration tests on every build. E2E nightly.
The Workflow
# 1. Write spec
vim specs/users-api.yaml
# 2. Create contract examples
vim contracts/users/query-default.json
# 3. Validate contracts
specmatic test --spec-file specs/users-api.yaml --examples-dir contracts/
# 4. Start stub for frontend
specmatic stub --spec-file specs/users-api.yaml --data contracts/ --port 9002
# 5. Frontend develops against stub
MOCK_MODE=true npm run dev
# 6. Backend implements
./mvnw spring-boot:run
# 7. Switch to real backend
MOCK_MODE=false npm run dev
# 8. Validate backend matches spec
specmatic test --spec-file specs/users-api.yaml --host localhost --port 8080
Both teams work in parallel. Contract catches mismatches. Integration is boring.
Results
Three BFF services, six months:
- Frontend unblocked day one
- Backend implements confidently (spec is the target)
- Integration issues rare (shape caught early, logic caught by tests)
- Spec always current (used daily)
Trade-offs
| Do | Don’t |
|---|---|
| One contract per shape | Contracts per filter combo |
| Client-side filtering in dev | Expect Specmatic to filter |
| Static test data | Dynamic/computed values |
| Shape tests + logic tests | One or the other |
Next
This covered backend contract-first. Next: 3-Tier Design Token Architecture—same separation of concerns, applied to CSS.
Specmatic validates shape. Spring Boot validates logic. Keep them separate.