{"slug": "javascript-still-can-t-ship-a-full-stack-module", "title": "JavaScript still can't ship a full-stack module", "summary": "A developer argues that JavaScript still lacks the ability to ship full-stack modules, unlike ecosystems like Rails and Laravel. They propose that pre-built, well-tested 'full-stack modules' would benefit both human developers and LLMs by reducing complexity and improving security. The developer, working on the Wasp framework, explores how other frameworks achieve this and defines full-stack modules as deep modules with thin interfaces.", "body_md": "Imagine if there were a way for us to somehow ship a *full-stack* package that you could plug into your app. Client code, backend code, webhooks, and database models all wired up and ready to go. I know I'd love that. Just install one complete module and the whole thing just works.\n\nTake payments, for example. Normally, we install the Stripe SDK or some nice payments UI library, but it always requires manual wiring. And that wiring bit, figuring out how to integrate libraries written by different people, is one of the most annoying parts of the job.\n\n*Different ways you can write a full-stack feature.*\n\nYeah yeah, I know what you are thinking. LLMs are writing most of the code these days, who cares? **Ok, but it's already been established: what's good for humans is good for LLMs. They like to be lazy too.**\n\nOpus, GPT, and the rest of the models out there adore \"legos\" (pre-built pieces of functionality): they speed up the scaffolding process and result in more secure apps. You shouldn't trust your agent with hand-rolled components, especially with parts that need to be vetted (auth, payments...). When you trust the agent with that, you pay for more tokens and have no idea where the security holes are.\n\nBut imagine those agents had access to well-tested, full-stack implementations of auth, payments, file upload, admin dashboard, etc. Ideally, these packaged legos would hide most of the complexity from us and offer our agents control knobs for the most important configuration. For example, the agents would only be dealing with 2 env variables and maybe 2 or 3 lines of config for tested, reliable, and secure Google auth.\n\nMatt Pocock talks about how LLMs struggle when a feature is split across too many layers and too much ad hoc glue. He focuses on the concept of \"deep modules\" from [A Philosophy of Software Design](https://www.amazon.com/Philosophy-Software-Design-John-Ousterhout/dp/1732102201) as one of the methods that help agents work in your codebase. We can define deep modules as a standalone part of our app that exposes a simple way to use it (a *thin interface*), but hides all the internal complexities of the actual implementation.\n\nThe legos I described are nothing more than deep modules themselves, having a thin interface for the agent to interact with while the complexity is tucked away.\n\nWhile developing [Wasp](https://dev.to/), a JS full-stack framework, we keep researching other ecosystems (Rails, Laravel, Django, etc.) and finding ways how they figured out developer productivity. We kept finding these reusable legos, so we gave them a name: \"full-stack modules\". Let's define what we mean by that exactly.\n\nWe usually talk about applications in two ways:\n\n*Technical layers vs vertical slices of an application.*\n\nIn Rails and Laravel, you can install packages that ship a slice like this. Add the package and payments are covered. You are not assembling a model, a backend, and a UI by hand. You can install one package which was developed against the stable shape of these frameworks.\n\nThat is a real advantage developers in Rails and Laravel ecosystems have, and it matters even more in the AI age. You ship faster if you don't spend days figuring out payments. You are also safer if you lean on a battle-tested package instead of something you wrote (or generated) under time pressure. The same logic applies to LLMs. We rarely review every line they produce. So, the less custom glue we ask them to write, the better.\n\nTo better understand the common patterns, I wanted to \"feel\" how full-stack modules work elsewhere. I can read the docs only so much, so I prefer playing around with code, trying to make some changes and see what kind of pushback the API will give me.\n\nI gathered all the frameworks/libraries/tools that sounded interesting to me and looked like they had something to teach me. Then I sketched an app that would somehow use a full-stack module.\n\nTo make it easy to compare, I created a static HTML prototype and tried to flesh out the requirements:\n\nIn all of the frameworks, we want to see the same webshop (same design, same capabilities).\n\n*Demo of the payments module in Django*\n\nOur webshop is made out of auth, payments, and products modules. In this exercise, we told the agent that the \"payments\" module *must be implemented* as a full-stack module. The idea was for the module to be reusable, and it can't make any assumptions about where it will be used.\n\nWe took all these requirements and built the same app in 11 different frameworks. See the list here: [https://fsm-research.static.miho.dev](https://fsm-research.static.miho.dev/)\n\n*How the host app uses the payments module in different frameworks.*\n\nPlaying around with the same webshop across different frameworks helped us understand how full-stack modules can be implemented. Different frameworks had different levels of support, which made the experience more or less enjoyable.\n\nFor example:\n\n`Startup`\n\nclass which has the ability to register services, database migrations, and add client and server routes as soon as the payments module is registered.`payments`\n\nengine, based on the Rails folder conventions, you get views, models, controllers, etc. automatically registered.`web`\n\napp and then again in the `api`\n\napp. I had to manually define queries and re-export pages to get them registered.Some of that is due to the design of the frameworks (e.g. serverless-first assumptions), some of it was due to wanting to allow more flexibility (e.g. database implementation is not owned by the framework).\n\nFor fun, we built a (very subjective) tier list of frameworks we tried based on their level of support for what we call full-stack modules: [https://fsm-research.static.miho.dev/fsm-tier-list](https://fsm-research.static.miho.dev/fsm-tier-list)\n\n*Tier list of support for full-stack modules in various frameworks. JS frameworks are highlighted in yellow.*\n\nHm, if full-stack modules are so great, why aren't they a JS standard? I think it's useful to first define what full-stack modules need to be viable in an ecosystem.\n\nFull-stack modules need:\n\nJavaScript ecosystem has no shortage of great libraries, and in great part that's due to how easy it is to ship code with `npm`\n\n. I'd say that sorts out points 1 and 3: it's easy to ship and install libraries.\n\nBut, it's impossible to ship \"glue\" code, the code that connects, e.g. the Stripe SDK on the backend, the payment button, and your database storage layer. I mean, how could it work if there are so many different standards for every little thing in JS?\n\n*A full-stack module shipped as an npm package.*\n\nThere is no standard full-stack layer you can rely on when shipping a library, so you can't really ship full-stack libraries.\n\nOne of the core reasons is that many popular frameworks are frontend-first, and nobody dares to own the full-stack concept: client, server, and the database. You need to opt into owning (and accept the maintenance burden) everything if you want to provide a consistent development surface.\n\nWhen you have some stable surface, your libraries can actually make assumptions and be \"braver\", e.g. they can assume how the routing works or how to expose webhooks in your app.\n\nSo what are we doing about it? We are building a truly full-stack framework. We control the frontend, the backend, and the database layer. That puts us in a good position to ship a full-stack module system on a predictable stack, and later grow it into a registry.\n\nThe core feature of Wasp is its spec file, a TypeScript config where you describe your app's features like client routing, server queries and actions, async jobs, etc. You still write React and Node.js to implement your features, while Wasp's spec file is where you write the \"glue\" code.\n\n*You define all the wiring code in one place: the Wasp Spec file.*\n\nThe cool thing about the \"glue\" code being written in a TypeScript spec file means that we can also package it alongside the React and Node.js code. We then arrive at a full-stack module.\n\nOne package which ships:\n\nI can hear you saying \"This is all very exciting, but let's see some code\", so, okay, okay, let's take a quick look at some of the technical details. Beware, this is all very experimental, but the concept is solid.\n\nLet's see how we can extract a full-stack feature from your Wasp app, so you can reuse it in the next project or even across all your company's internal tooling.\n\nHere's an example Wasp spec file:\n\n``` js\n// main.wasp.ts\n// Regular Wasp app: the app owns the payments wiring.\n\nimport { action, api, app, page, query, route } from \"@wasp.sh/spec\";\n\nimport { LandingPage } from \"./src/landing-page/LandingPage\" with { type: \"ref\" };\nimport { PricingPage } from \"./src/payment/PricingPage\" with { type: \"ref\" };\nimport { CheckoutResultPage } from \"./src/payment/CheckoutResultPage\" with { type: \"ref\" };\nimport { generateCheckoutSession, getCustomerPortalUrl } from \"./src/payment/operations\" with { type: \"ref\" };\nimport { paymentsMiddlewareConfigFn, paymentsWebhook } from \"./src/payment/webhook\" with { type: \"ref\" };\n\nexport default app({\n  // ...\n\n  spec: [\n    route(\"LandingPageRoute\", \"/\", page(LandingPage), { prerender: true }),\n\n    // Payments \"glue\" code\n    route(\"PricingPageRoute\", \"/pricing\", page(PricingPage), {\n      prerender: true,\n    }),\n    route(\n      \"CheckoutResultRoute\",\n      \"/checkout\",\n      page(CheckoutResultPage, { authRequired: true }),\n    ),\n    query(getCustomerPortalUrl, { entities: [\"User\"] }),\n    action(generateCheckoutSession, { entities: [\"User\"] }),\n    api(\"POST\", \"/payments-webhook\", paymentsWebhook, {\n      entities: [\"User\"],\n      middlewareConfigFn: paymentsMiddlewareConfigFn,\n    }),\n\n    // ...\n  ],\n});\n```\n\nWe see that to implement payments we need:\n\n`/pricing`\n\nand `/checkout`\n\n`getCustomerPortalUrl`\n\nand `generateCheckoutSession`\n\n`/payments-webhook`\n\nFor each of those pieces, we also have runtime code in files like `PricingPage.tsx`\n\nand `operations.ts`\n\n.\n\nSo if we wanted to share this as an installable full-stack feature, we need to package both the runtime and the spec code. This way, Wasp knows how to wire the feature once installed.\n\nLet's say we shipped `stripePayments`\n\nas an `npm`\n\npackage. This is how using the full-stack module would look:\n\n``` js\n// main.wasp.ts\n// Wasp app with a full-stack payments module.\n\nimport { app, page, route } from \"@wasp.sh/spec\";\nimport { stripePayments } from \"@acme/stripe-payments/spec\";\n\nimport { LandingPage } from \"./src/landing-page/LandingPage\" with { type: \"ref\" };\n\nexport default app({\n  // ...\n\n  spec: [\n    route(\"LandingPageRoute\", \"/\", page(LandingPage), { prerender: true }),\n\n    stripePayments(\"Payments\", {\n      entities: { User: \"User\" },\n\n      plans: {\n        hobby: {\n          kind: \"subscription\",\n          priceIdEnvVar: \"PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID\",\n        },\n        pro: {\n          kind: \"subscription\",\n          priceIdEnvVar: \"PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID\",\n        },\n        credits10: {\n          kind: \"credits\",\n          amount: 10,\n          priceIdEnvVar: \"PAYMENTS_CREDITS_10_PLAN_ID\",\n        },\n      },\n    }),\n\n    // ...\n  ],\n});\n```\n\nIn our example, you:\n\n`stripePayments`\n\nfrom `@acme/stripe-payments/spec`\n\n,One extra fun fact: since the spec file is written in TypeScript, you can even fetch the info about your payment plans from a CMS or an API.\n\n*Installing a full-stack module in Wasp.*\n\nFull-stack modules will not fit every use case. So there should be escape hatches:\n\nAt Wasp, we've built and maintain the most popular open-source SaaS boilerplate starter, [Open SaaS](https://opensaas.sh/).\n\nIt's already packed with features:\n\nBut we're reluctant to add new ones, because it's already common for users to delete many features from the template to customize it (which is pretty meh from a DX perspective).\n\n*Our open-source and free SaaS starter.*\n\nLet's imagine what full-stack modules would mean for starters like these. Instead of shipping a starter with *all the features*, you could maybe start with a nice design system and tell users to install *features* they need. A user building an LLM-powered site with subscriptions would only install the payments and LLM full-stack modules. Some other user might install only the admin panel and S3 file uploads full-stack modules, etc.\n\nAnd those are only the modules we thought of so far. Imagine when the ecosystem has hundreds of community built modules, this starter becomes the most powerful starter in the world. And of course, the same modules could be used by any Wasp app out there.\n\n*A concept for a future full-stack module registry.*\n\nWe've seen in other ecosystems how powerful these legos/full-stack modules are. Developers and LLMs that use them get more done plus have higher confidence in their apps.\n\nWe think it's possible to bring the same idea to the JS ecosystem.\n\nThe pieces we need to make it happen:\n\nAnd that's exactly what we want to build with Wasp. [Follow us on X](https://x.com/WaspLang) or join our [Discord](https://discord.gg/rzdnErX) to follow along.", "url": "https://wpnews.pro/news/javascript-still-can-t-ship-a-full-stack-module", "canonical_source": "https://dev.to/wasp/javascript-still-cant-ship-a-full-stack-module-ijn", "published_at": "2026-06-29 12:29:05+00:00", "updated_at": "2026-06-29 12:48:56.368199+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Wasp", "Stripe", "Matt Pocock", "Rails", "Laravel", "Django", "OpenAI", "Anthropic"], "alternates": {"html": "https://wpnews.pro/news/javascript-still-can-t-ship-a-full-stack-module", "markdown": "https://wpnews.pro/news/javascript-still-can-t-ship-a-full-stack-module.md", "text": "https://wpnews.pro/news/javascript-still-can-t-ship-a-full-stack-module.txt", "jsonld": "https://wpnews.pro/news/javascript-still-can-t-ship-a-full-stack-module.jsonld"}}