Most full-stack applications start with a burst of rapid development. Features land quickly, the team feels productive, and stakeholders are happy. But after a year or two, that same codebase often becomes a source of friction: subtle coupling between frontend and backend, tangled state management, and deployment pipelines that break without warning. This pattern is so common that many teams accept it as inevitable. But it doesn't have to be.
We call a different philosophy the Cobble Approach — a set of architectural and process decisions that prioritize long-term maintainability from day one. It's not about choosing the 'perfect' framework or following every best practice. It's about making pragmatic trade-offs that keep your application adaptable, testable, and comprehensible as it grows. This guide is for developers and tech leads who want to build full-stack applications that remain a pleasure to work on, not a burden.
We'll walk through the core ideas, how they work in practice, common edge cases, and honest limitations. Along the way, we'll share composite scenarios drawn from real-world projects to illustrate what works and what breaks. By the end, you'll have a clear framework for evaluating your own stack and making decisions that pay off over years, not just the next sprint.
Why Long-Term Maintainability Matters Now
The hidden cost of fast shipping
Every team feels pressure to deliver quickly. But the shortcuts taken to hit a deadline — skipping tests, hardcoding configuration, ignoring type safety — accumulate as technical debt. Unlike financial debt, technical debt doesn't have a fixed interest rate; it compounds unpredictably. A seemingly small decision to inline a value instead of using an environment variable can, months later, cause a production incident when a colleague deploys to a new region.
In full-stack applications, this debt multiplies because the frontend and backend are tightly coupled. A change in the API response format might require updates in multiple frontend components, each with its own state management logic. Without clear boundaries, what should be a one-line fix turns into a day of detective work.
The sustainability lens
Maintainability isn't just about code quality; it's about team sustainability. When a codebase is hard to change, developers become hesitant to refactor. They work around problems, adding layers of complexity. Burnout rises, turnover increases, and the institutional knowledge walks out the door. The Cobble Approach treats maintainability as an ethical concern: building systems that respect the time and energy of future developers, including your future self.
We've seen projects where the original team spent 80% of their time on new features in the first year, but only 20% in the second year — the rest went to fixing bugs and understanding the architecture. A maintainable codebase flips that ratio, letting you invest effort where it matters: solving user problems.
Core Idea: Clear Boundaries and Explicit Contracts
What we mean by 'Cobble'
The name comes from the idea of cobblestones — individually shaped, but fitted together to form a durable surface. In a full-stack application, each layer (frontend, API, database, background jobs) should be a well-defined piece with explicit interfaces. The connections between them are the mortar: standardized, predictable, and testable.
This isn't new — it's the principle of separation of concerns. But in practice, many frameworks encourage blurring lines. For example, Next.js API routes live next to page components, making it easy to accidentally leak database queries into the frontend logic. Similarly, server-side rendering frameworks often mix data fetching with presentation in ways that are convenient initially but hard to untangle later.
The three contracts
We advocate for three explicit contracts in any full-stack application:
- API contract — The shape of all data exchanged between client and server, ideally defined with a schema language (like OpenAPI or GraphQL SDL). This contract is the single source of truth for what the backend provides and what the frontend expects.
- State contract — How the frontend manages local, server, and UI state. Using a consistent pattern (e.g., React Query for server state, zustand for local state) prevents the chaos of ad-hoc state scattered across components.
- Deployment contract — The environment variables, secrets, and infrastructure assumptions that both layers depend on. Documenting these explicitly prevents 'works on my machine' surprises.
These contracts act as a shared language. When a new developer joins, they can read the API contract to understand what data flows where, rather than tracing through dozens of files. When a breaking change is needed, the contract makes it visible immediately — you can't accidentally change a field without updating the schema.
How It Works Under the Hood
Layered architecture with dependency rules
The Cobble Approach enforces a strict dependency direction: frontend code can depend on the API contract, but not directly on the backend implementation. Similarly, backend code can depend on the API contract, but not on frontend specifics. This is achieved through tooling and conventions, not just good intentions.
Practically, this means:
- The API contract (e.g., an OpenAPI spec) lives in a shared repository or package that both frontend and backend import as a dependency. Any change to the contract requires a version bump and explicit update in both layers.
- Frontend code never imports backend models or database schemas. Instead, it uses generated client libraries from the API contract. This eliminates the common anti-pattern of sharing TypeScript types between frontend and backend by direct file references, which creates hidden coupling.
- Backend code never assumes a specific frontend implementation. It returns data in the agreed format, without knowledge of how that data will be rendered.
Automated contract testing
To keep the contracts honest, we recommend automated tests that verify compliance. For REST APIs, this means contract tests (using tools like Pact or Dredd) that run in CI. For GraphQL, schema validation and persisted operations serve a similar purpose. These tests catch mismatches before they reach production — a safety net that's especially valuable when multiple teams own different parts of the stack.
One team we worked with had a monorepo where the frontend and backend shared a types directory. A developer changed a backend model to add a new field, but forgot to update the corresponding frontend type. The application compiled fine because TypeScript didn't catch the mismatch — the types were the same file. A contract test would have flagged the discrepancy immediately. After adopting the Cobble Approach with a separate API contract, similar issues became rare.
Worked Example: Building a Simple E-Commerce Feature
Scenario: Product listing with filters
Let's walk through a concrete example. Imagine you're building a product listing page with category filters, search, and pagination. In a typical full-stack framework, you might write a single file that fetches data on the server and passes it to a React component. That works, but it mixes concerns: the data fetching logic is tied to the rendering logic.
With the Cobble Approach, you'd start by defining the API contract:
GET /api/products?category={category}&search={query}&page={page}&limit={limit}- Response:
{ products: Product[], total: number, page: number }
This contract is documented in an OpenAPI spec and stored in a shared package. Both frontend and backend import it. The backend implements the endpoint, returning data exactly as specified. The frontend uses a generated client to call the endpoint, with types derived from the spec.
State management on the frontend
On the frontend, we use a library like React Query to manage server state. The filters and pagination are stored in URL search params (persistent and shareable), while the fetched data is cached and invalidated based on the query key. This separation means the UI components don't need to know about fetching logic — they just consume data from the cache.
When a user changes a filter, the URL updates, which triggers a new query. React Query handles deduplication and background refetching. If the backend changes the response format, the API contract test fails in CI, alerting the team before they merge.
Deployment contract
Both frontend and backend read environment variables from a shared configuration file (e.g., .env.shared) that lists required variables with descriptions. The CI pipeline validates that all variables are set before deploying. This prevents the classic issue where a new feature works locally but fails in staging because a missing environment variable wasn't documented.
In practice, this example shows how the Cobble Approach adds a small upfront cost (defining contracts) that pays off when the application grows. Adding a new filter becomes a matter of updating the contract, implementing the backend change, and adjusting the frontend query — all with clear boundaries.
Edge Cases and Exceptions
When contracts become too rigid
Strict contracts can slow down rapid prototyping. In the early stages of a project, when requirements are fluid, maintaining a formal API contract may feel like overhead. The Cobble Approach acknowledges this: it's okay to skip contracts during initial exploration, but you should introduce them before the first production release. The key is recognizing the inflection point — usually when a second developer joins the project or when the first external integration is planned.
Shared types vs. shared contracts
A common objection is that sharing TypeScript types directly between frontend and backend is simpler than maintaining a separate contract. This works for small teams with a single codebase, but it creates coupling: changing a backend model requires updating the frontend types, and there's no explicit boundary to enforce compatibility. The Cobble Approach recommends using a contract as the single source of truth, even if you generate TypeScript types from it. This adds a step but prevents hidden dependencies.
Monorepo considerations
In a monorepo, it's tempting to bypass contracts because both layers live in the same repository. But that's exactly when boundaries are most important. Without them, developers can easily import backend modules from frontend code, creating a tangled dependency graph. We've seen monorepos where the frontend directly calls database functions — a practice that works in development but makes it impossible to deploy the frontend independently. The Cobble Approach applies regardless of repository structure; the contracts are logical, not physical.
When the frontend needs real-time data
WebSockets and server-sent events introduce a different kind of contract — one that's event-based rather than request-response. The same principles apply: define the event payloads explicitly, version them, and test that both sides adhere to the schema. Tools like AsyncAPI can document these event-driven contracts, providing the same safety net as OpenAPI for REST.
Limits of the Approach
Not a silver bullet
The Cobble Approach is not a framework or a library; it's a set of principles. It won't automatically make your codebase maintainable if the team doesn't buy in. Without discipline, contracts become outdated, tests are skipped, and the architecture drifts back toward chaos. The approach requires ongoing investment: updating contracts when APIs change, running contract tests in CI, and resisting the temptation to take shortcuts.
Overhead for small projects
For a simple CRUD app with a single developer, the overhead of maintaining a separate API contract may not be worth it. The cost of generating clients and running contract tests can outweigh the benefits when the codebase is small and changes are infrequent. In those cases, a simpler approach — like sharing types in a monorepo — may be more pragmatic. The Cobble Approach shines when the application grows beyond a few endpoints or when multiple developers are involved.
Tooling maturity
Some contract testing tools have a learning curve and may not integrate smoothly with every framework. For example, Pact works well for HTTP APIs but less so for GraphQL subscriptions. Teams may need to invest time in setting up the toolchain and training developers. This is a real cost that should be factored into the decision.
False sense of security
Contracts catch mismatches in data shape, but they don't guarantee correct behavior. An API might return the right fields but with wrong values, or a frontend might send invalid data that passes schema validation but causes a business logic error. Contract tests are a safety net, not a substitute for integration and end-to-end tests. Teams should layer multiple testing strategies, with contracts as one component.
Reader FAQ
How do I convince my team to adopt this approach?
Start with a pilot project or a new feature, not a rewrite. Show concrete benefits: a contract test catching a breaking change before deployment, or a new developer getting up to speed faster by reading the API spec. Metrics like reduced regression bugs or faster onboarding times can help make the case.
Does this work with serverless and edge functions?
Yes, the principles are framework-agnostic. Serverless functions are just another backend layer; they should adhere to the same API contract. Edge functions that run close to the user can be treated as part of the frontend layer, with their own state contract. The key is maintaining explicit boundaries, even when the infrastructure blurs them.
What if our API contract changes frequently?
That's a signal that the contract might be too detailed or that the design is unstable. Consider using a more flexible contract format (like GraphQL with deprecation fields) or versioning your API. Frequent breaking changes suggest the team hasn't settled on a stable domain model. It may be worth investing in domain-driven design to identify core entities before finalizing contracts.
Can we use this with a third-party API?
Absolutely. Treat the third-party API as an external contract. Wrap it with your own backend layer that translates between the external contract and your internal one. This isolates your application from changes in the third-party API and gives you a place to add caching, error handling, or transformation logic.
Next steps: pick one small feature in your current project and try defining an explicit API contract for it. Run a contract test in CI. See how it feels. If the upfront effort saves you even one debugging session, you'll see the value. From there, expand gradually — the Cobble Approach is about building a durable foundation, one cobblestone at a time.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!