Dan Matthew

Program note · Writing · 17 Aug 2026

Composing a full stack app from two separate generators

I wanted a React front end and a Python back end in one repo without teaching either generator about the other. The answer was composition, a core primitive that stitches two generated projects and writes a neutral orchestration.

#packkit #fullstack #composition #docker #architecture

A common thing to want is a front end and a back end together: a React app talking to a Python API, in one repo, with one command to run both. The lazy way to build that into a generator is to teach the React generator about Python, or write a bespoke "react plus fastapi" template. Both are dead ends. The first makes every generator know about every other one. The second means a new template for every pairing, which is a combinatorial mess before you have three of each.

Packkit does it as composition instead. There is a core primitive, composeFullstack, that takes two already-generated projects and stitches them into one, and it does that without knowing what React or FastAPI is.

The one rule: compose from contracts, not from languages

composeFullstack takes a front end project whose deployment contract is static, and a back end project whose contract is service. That is all it requires of them. Not "a React project", not "a FastAPI project", just a static thing and a service thing. Any generator that can produce those two contracts can be composed, which is exactly why the contracts exist.

What it does is deliberately mechanical. It merges the two file trees under apps/web and apps/server, matching the layout convention the JavaScript full stack preset already used. It rewrites the sub-projects' commands and paths to be root-relative, so build in the server becomes cd apps/server && build. And it writes one thing neither generator produced: a root docker-compose.yml that runs the back end from its own Dockerfile on its own port, and serves the front end's built output behind nginx. Every value in that compose file comes from the two deployment contracts, the ports, the output directory, the build. Core never reads a line of React or Python to write it.

The result is a real repo. A React front end with a Python FastAPI back end composes to 46 files. A React front end with a Go back end composes to 45 files, with a real go.mod, handler.go, and server.go under apps/server. Same primitive, different backend language, because the primitive only ever saw two contracts.

The same idea, on three surfaces

Composition lives in core, so every surface gets it for free, in the same shape.

Over the MCP server, there is a compose_fullstack tool. An agent hands it a front end spec and a back end spec, each naming a generator and a preset, and gets back a composed repo. It genuinely runs a JavaScript React app and a Python service through the protocol and stitches them, so an agent can build a full stack app across two languages without either generator knowing the other exists.

In the browser configurator there is a Fullstack mode: pick an app name, a front end, and a back end, and it composes in the browser. That one had a wrinkle worth naming. The browser-safe JavaScript path returns files but not a deployment contract, while the Python and Go browser paths do return one. So the web composer synthesizes the contract for the JavaScript side, static for a React app, service for a Node service, and then calls the exact same composeFullstack. The seam held: the fix was to supply the missing contract, not to special-case the composition.

What it is not

This is composition, not code generation, and the distinction is the honest limit. You do not get a merged framework where the front end imports server functions directly. You get two apps, correctly wired: same-origin /api in development via a proxy, the server serving the built front end in production, and a shared package for the types both sides agree on. That is what most "full stack starter" repos actually are once you look, but I want to be clear that the primitive stitches, it does not fuse.

The other limit is that the neutral docker-compose.yml is a starting orchestration, not a production topology. It runs both halves on one host for local development and a simple deploy. Anything with a real database, a queue, or horizontal scaling is a rewrite you do yourself, and again, it is a rewrite of a plain compose file you own.

Full stack composition is why the language rule in core earned its keep. Because core forgot React and FastAPI, it could stitch them without preferring either, and the day I want a Rust back end under that same React front end, the primitive will not need to change. You can try the pairings in the browser at packkit-web.pages.dev, and the primitive lives in @packkit/core.