<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Wasp Blog</title>
        <link>https://wasp.sh/blog</link>
        <description>Wasp Blog</description>
        <lastBuildDate>Mon, 13 Jul 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[Why design matters for a web framework: a 7-year evolution]]></title>
            <link>https://wasp.sh/blog/2026/07/13/why-design-matters-for-a-web-framework</link>
            <guid>https://wasp.sh/blog/2026/07/13/why-design-matters-for-a-web-framework</guid>
            <pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Wasp is a full-stack web framework. After five years of treating design as second-tier, we rebranded it as a technical spec: blueprints, monospace, three colors. Here's why design ended up mattering more for us than for our users.]]></description>
            <content:encoded><![CDATA[<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="The Wasp mark across four labeled eras on a timeline: 2019 STIC (an actual stick), 2019 'wasp' (the rename, three triangles), 2020 Alpha (the =} arrives, in a circle), and 2026 today (bulked up + locked in a square)" src="https://wasp.sh/img/wasp-rebrand/rebrand-logo-progression-real.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p><a href="https://wasp.sh/" target="_blank" rel="noopener noreferrer" class="">Wasp</a> is a full-stack web framework for React and Node.js (imagine Rails for TypeScript, with but with frontend included and wired up out-of-the-box), and we've been building it for a little over five years (as a full-time thing). For almost all of that time, we saw design and "branding" as something we shouldn't spend too much time on.</p>
<p>A framework is mostly code, we thought: you drive it from a CLI and your editor, there is little visual to look at, and these days an agent reads the docs for you half the time anyway. The most loved frameworks kind of prove the point, with Django and Rails famous and famously plain.</p>
<p>Turns out we were wrong, though not in the way you'd expect. What made design start to matter wasn't users or growth - it was us, the people building Wasp and the community around it.</p>
<p>This is the story of how we figured that out, and of the rebrand it led to, which shipped as part of Launch Week #12 - the same week Wasp went <a class="" href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec">fully TypeScript-native</a>.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="five-years-of-not-having-a-design">Five years of not having a design<a href="https://wasp.sh/blog/2026/07/13/why-design-matters-for-a-web-framework#five-years-of-not-having-a-design" class="hash-link" aria-label="Direct link to Five years of not having a design" title="Direct link to Five years of not having a design" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;flex-direction:column;align-items:stretch"><figure style="margin:0;position:relative"><div style="display:grid;grid-template-columns:repeat(2, 1fr);gap:8px"><img src="https://wasp.sh/img/wasp-rebrand/alpha-landing-atf.webp" alt="Wasp's first proper landing page from the Alpha launch in March 2021" class="no-default-zoom" style="width:100%;height:auto;display:block;cursor:zoom-in;border:1px solid var(--wasp-g5)"><img src="https://wasp.sh/img/wasp-rebrand/current-wasp-sh.webp" alt="Today's wasp.sh hero after the 2026 rebrand" class="no-default-zoom" style="width:100%;height:auto;display:block;cursor:zoom-in;border:1px solid var(--wasp-g5)"></div><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.6em;text-align:center">Same product, five years apart. Very different vibes.</figcaption></figure></div>
<p>Wasp has always had a logo, colors and fonts (ok, the fonts were whatever shipped with Docusaurus). So when I say we didn't have a design, I mean we never sat down to think about it as a whole. There was no central story, no backbone holding everything together, just a bunch of pages that happened to be ours.</p>
<p>Here's how it typically went down: with every big release (v0, Alpha, Beta), I'd sit down and convince Martin (my twin brother and co-founder) to let me "rebrand" Wasp. And every single time, I'd do almost the exact same thing: look around for some inspiration (thanks, Supabase), then patch, okay, <em>steal</em>, a few sections together into something that looked decent enough, and ship it.</p>
<p>And early on, that was the right call. When you're still proving the product deserves to exist, obsessing over a design system is a great way to burn weeks you don't really have. Plenty of founders do the opposite. They polish pixels on something nobody wants yet (and we witnessed that many times) and we were determined not to be them.</p>
<p>But design debt compounds, just like code debt does. Over five years, Wasp itself grew up <em>a lot</em>, and it grew much faster than its design did, until at some point it just overgrew it. The framework got serious, but the brand stayed improvised. You can kind of watch it happen across our old landing pages:</p>
<div class="figure-container" tabindex="0" style="display:flex;flex-direction:column;align-items:stretch;outline:none"><figure style="margin:0;position:relative"><div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:8px"><div style="font-family:IBM Plex Mono, monospace;font-weight:600;font-size:15px">stic (before it was called wasp), 2019</div><div style="font-family:IBM Plex Mono, monospace;font-size:12px;letter-spacing:0.08em;opacity:0.6">01<!-- --> /<!-- --> <!-- -->04</div></div><div style="position:relative;overflow:hidden"><div style="display:flex;transform:translateX(-0%);transition:transform 0.35s ease"><div style="flex:0 0 100%;display:flex;justify-content:center;align-items:center"><img src="https://wasp.sh/img/wasp-rebrand/stic-landing-page.webp" alt="The stic-era landing page from 2019: minimal black-and-white Bulma styling, 'STIC' navbar, and the tagline 'Specification To Implementation Compiler'" class="no-default-zoom" style="max-width:100%;display:block;cursor:zoom-in;border:1px solid var(--wasp-g5);box-sizing:border-box"></div><div style="flex:0 0 100%;display:flex;justify-content:center;align-items:center"><img src="https://wasp.sh/img/wasp-rebrand/alpha-landing-atf.webp" alt="The Alpha-era Wasp landing page from March 2021: '.wasp (alpha)' navbar, hero 'A simple language for developing full-stack web apps with less code', code preview on the right with React, Node.js and Prisma logos" class="no-default-zoom" style="max-width:100%;display:block;cursor:zoom-in;border:1px solid var(--wasp-g5);box-sizing:border-box"></div><div style="flex:0 0 100%;display:flex;justify-content:center;align-items:center"><img src="https://wasp.sh/img/wasp-rebrand/pre-rebrand-landing-atf.webp" alt="The pre-rebrand Wasp landing page from October 2025: a Launch Week #11 announcement bar at the top, yellow gradient hero with 'Develop full-stack web apps faster', and a code preview on the right" class="no-default-zoom" style="max-width:100%;display:block;cursor:zoom-in;border:1px solid var(--wasp-g5);box-sizing:border-box"></div><div style="flex:0 0 100%;display:flex;justify-content:center;align-items:center"><img src="https://wasp.sh/img/wasp-rebrand/current-wasp-sh.webp" alt="Today's wasp.sh hero after the 2026 rebrand: blueprint paper background, big monospace headline 'A truly full-stack TypeScript framework that just works.', main.wasp.ts code preview on the right" class="no-default-zoom" style="max-width:100%;display:block;cursor:zoom-in;border:1px solid var(--wasp-g5);box-sizing:border-box"></div></div><button disabled="" aria-label="Previous" style="position:absolute;top:50%;left:8px;transform:translateY(-50%);width:40px;height:40px;border:1.5px solid var(--wasp-k);background:var(--wasp-y);color:var(--wasp-k);cursor:not-allowed;opacity:0.35;display:flex;align-items:center;justify-content:center;font-family:monospace;font-weight:700;font-size:20px;padding:0;z-index:5">‹</button><button aria-label="Next" style="position:absolute;top:50%;right:8px;transform:translateY(-50%);width:40px;height:40px;border:1.5px solid var(--wasp-k);background:var(--wasp-y);color:var(--wasp-k);cursor:pointer;opacity:1;display:flex;align-items:center;justify-content:center;font-family:monospace;font-weight:700;font-size:20px;padding:0;z-index:5">›</button></div><div style="display:flex;justify-content:center;flex-wrap:wrap;gap:8px;margin-top:16px"><button type="button" aria-label="Go to slide 1: stic (before it was called wasp), 2019" aria-current="true" title="stic (before it was called wasp), 2019" style="width:150px;height:94px;padding:0;border:2.5px solid var(--wasp-y);outline:1px solid var(--wasp-k);background:transparent;cursor:pointer;overflow:hidden;transition:border-color 0.15s, transform 0.15s;transform:scale(1.05);box-sizing:border-box"><img src="https://wasp.sh/img/wasp-rebrand/stic-landing-page.webp" alt="" class="no-default-zoom" draggable="false" style="width:100%;height:100%;object-fit:cover;object-position:top center;display:block;pointer-events:none"></button><button type="button" aria-label="Go to slide 2: Alpha, March 2021" aria-current="false" title="Alpha, March 2021" style="width:150px;height:94px;padding:0;border:1.5px solid var(--wasp-g5);outline:none;background:transparent;cursor:pointer;overflow:hidden;transition:border-color 0.15s, transform 0.15s;transform:none;box-sizing:border-box"><img src="https://wasp.sh/img/wasp-rebrand/alpha-landing-atf.webp" alt="" class="no-default-zoom" draggable="false" style="width:100%;height:100%;object-fit:cover;object-position:top center;display:block;pointer-events:none"></button><button type="button" aria-label="Go to slide 3: Pre-rebrand, October 2025" aria-current="false" title="Pre-rebrand, October 2025" style="width:150px;height:94px;padding:0;border:1.5px solid var(--wasp-g5);outline:none;background:transparent;cursor:pointer;overflow:hidden;transition:border-color 0.15s, transform 0.15s;transform:none;box-sizing:border-box"><img src="https://wasp.sh/img/wasp-rebrand/pre-rebrand-landing-atf.webp" alt="" class="no-default-zoom" draggable="false" style="width:100%;height:100%;object-fit:cover;object-position:top center;display:block;pointer-events:none"></button><button type="button" aria-label="Go to slide 4: Today, 2026" aria-current="false" title="Today, 2026" style="width:150px;height:94px;padding:0;border:1.5px solid var(--wasp-g5);outline:none;background:transparent;cursor:pointer;overflow:hidden;transition:border-color 0.15s, transform 0.15s;transform:none;box-sizing:border-box"><img src="https://wasp.sh/img/wasp-rebrand/current-wasp-sh.webp" alt="" class="no-default-zoom" draggable="false" style="width:100%;height:100%;object-fit:cover;object-position:top center;display:block;pointer-events:none"></button></div></figure></div>
<p>And once our design felt broken, there was little motivation to keep it right, so it just kept getting worse. At one point we needed to show a roadmap on the site, so Martin just screenshotted our GitHub project board and pasted it straight in. Low-res, rounded corners not quite matching, with some random background bleeding out around the edges. It stayed up like that for an embarrassingly long time:</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="A screenshot of Wasp's GitHub project board pasted straight into the landing page, low-resolution and visibly mismatched with the surrounding light theme" src="https://wasp.sh/img/wasp-rebrand/martin-gh-roadmap-on-landing.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Our roadmap section. Yes, that is just a screenshot of our GitHub project. Not even properly cropped.</figcaption></figure></div>
<p>Or take the "How does it work?" section right below it. The diagram was originally drawn as a transparent SVG, which broke once the site got a dark mode, so someone slapped a white background behind it to "fix" it. Now it looked fine on the dark version, and noticeably worse everywhere else. Add the orange highlight pills next to the copy, styled in a completely different visual language than anything else on the page, and the whole thing kind of looked like three different sites stitched together:</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="An older 'How does it work?' section on wasp.sh: the diagram uses the old yellow-circle logo, a soft yellow gradient behind it, and highlight pills styled inconsistently with the rest of the page" src="https://wasp.sh/img/wasp-rebrand/broken-how-it-works.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Our old "How it works" section, broken in a few different ways.</figcaption></figure></div>
<p>At some point we just started to feel bad about how unpolished it looked. We couldn't really tell you what users made of it (probably not much), but we could feel it ourselves: Wasp had grown into a serious product that people use for <a class="" href="https://wasp.sh/blog/2026/06/17/made-with-wasp-vol-2">serious things</a>, and it was about time it started looking like one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="looking-unpolished-made-us-feel-unpolished">Looking unpolished made us feel unpolished<a href="https://wasp.sh/blog/2026/07/13/why-design-matters-for-a-web-framework#looking-unpolished-made-us-feel-unpolished" class="hash-link" aria-label="Direct link to Looking unpolished made us feel unpolished" title="Direct link to Looking unpolished made us feel unpolished" translate="no">​</a></h2>
<p>There was also another reason for finally taking design seriously, harder to pin down, but looking back it's pretty obvious.</p>
<p>Looking unpolished slowly does something to you. Every time we shipped a great feature next to a website that looked like a slightly abandoned student project, it sent a quiet little signal we never really meant to send: that maybe we don't actually care all that much. And that signal doesn't only go out to users; it also comes back in. A product that looks like nobody sweated the details slowly starts to <em>feel</em> like one to the people building it, even when, internally, the opposite is completely true.</p>
<p>You don't really notice it on any given day, but over five years it compounds. You hesitate a bit before posting the screenshot. You add a caveat before the demo (<em>"ignore how it looks for a second, just look at what it does"</em>). You quietly stop wanting to share Wasp with someone whose opinion you actually care about, because they might judge the wrapper instead of the thing inside. And it gets hard to pour your full energy into something you're a tiny bit embarrassed by.</p>
<p>So in a real sense, a good part of this rebrand wasn't for users at all. It was for us. We wanted to be able to look at every surface Wasp touches and not flinch, and to feel like the way Wasp looks finally matches how much we actually care about it. Looking back, that's not really vanity but basic hygiene, and we should've done it sooner.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-product-changed-so-the-skin-had-to">The product changed, so the skin had to<a href="https://wasp.sh/blog/2026/07/13/why-design-matters-for-a-web-framework#the-product-changed-so-the-skin-had-to" class="hash-link" aria-label="Direct link to The product changed, so the skin had to" title="Direct link to The product changed, so the skin had to" translate="no">​</a></h2>
<p>When we <a class="" href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake">made the call to go all-in on TypeScript</a>, we knew it wasn't a facelift. It changed what Wasp fundamentally <em>is</em>: not a custom language you have to learn, but a framework that meets you where you already are.</p>
<p>That forced an uncomfortable question: if someone landed on wasp.sh today, would they actually see that? Honestly, no. The old look still said "quirky side project with a custom language", while the new Wasp is something else: a serious, full-stack TypeScript framework you'd reach for to build a real product. The caterpillar was getting its wings (ok, wasp, but you get the idea), and it couldn't really keep wearing the old skin.</p>
<p>So instead of asking ourselves "what looks nice?", we ended up asking a much better question: <strong>what is Wasp, actually?</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wasp-is-a-spec-so-the-brand-is-a-blueprint">Wasp is a spec. So the brand is a blueprint.<a href="https://wasp.sh/blog/2026/07/13/why-design-matters-for-a-web-framework#wasp-is-a-spec-so-the-brand-is-a-blueprint" class="hash-link" aria-label="Direct link to Wasp is a spec. So the brand is a blueprint." title="Direct link to Wasp is a spec. So the brand is a blueprint." translate="no">​</a></h2>
<p>The core idea behind Wasp has always been the same, DSL or no DSL: you declare <em>what</em> your app is (routes, pages, auth, jobs, the whole skeleton), and Wasp handles all the plumbing under the hood. The high-level spec you define in TypeScript is essentially an engineering drawing of your application, and the framework just builds it.</p>
<p>(Quick side note: "Wasp" itself stands for <strong>W</strong>eb <strong>A</strong>pplication <strong>SP</strong>ecification - which is basically how we've always thought about it.)</p>
<p>But before we landed there, we explored a <em>lot</em> of other directions. Some internal attempts (mostly me prompting Nano Banana), and we also briefly worked with a few designers who came back with their own takes. AI turned out to be genuinely useful in this phase: you can iterate on entire landing-page mockups in a few minutes and quickly see what does and what doesn't feel like Wasp, and build intuition for what you like. Here's one of mine:</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="An early Wasp landing-page mockup prompted by Matija through Nano Banana: a blueprint-paper hero 'Your application's schematic, engineered by wasp', a 'Functional Monolith' system diagram, and a 'Patterns, The Sharp Path' sidebar exploring squared =} logo variants" src="https://wasp.sh/img/wasp-rebrand/matija-nano-banana-mockup.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">One of my early Nano Banana runs. Already leaning hard into the schematic / blueprint angle that ended up shaping the final brand.</figcaption></figure></div>
<p>And here's a small sample of what the designers sent over:</p>
<div class="figure-container" style="display:flex;flex-direction:column;align-items:stretch"><figure style="margin:0;position:relative"><div style="display:grid;grid-template-columns:repeat(2, 1fr);gap:8px"><img src="https://wasp.sh/img/wasp-rebrand/concept-1.webp" alt="Designer concept 1: monospace hero on Blueprint Paper with mosaic feature cards" class="no-default-zoom" style="width:100%;height:auto;display:block;cursor:zoom-in;border:1px solid var(--wasp-g5)"><img src="https://wasp.sh/img/wasp-rebrand/concept-2.webp" alt="Designer concept 2: dense grid layout with thick black borders and yellow accents" class="no-default-zoom" style="width:100%;height:auto;display:block;cursor:zoom-in;border:1px solid var(--wasp-g5)"><img src="https://wasp.sh/img/wasp-rebrand/concept-3.webp" alt="Designer concept 3: pill-shaped CTAs over a dark feature grid, italic 'faster' wordmark" class="no-default-zoom" style="width:100%;height:auto;display:block;cursor:zoom-in;border:1px solid var(--wasp-g5)"><img src="https://wasp.sh/img/wasp-rebrand/concept-4.webp" alt="Designer concept 4: large yellow =} mark in a cell-grid composition with monospace table-like blocks" class="no-default-zoom" style="width:100%;height:auto;display:block;cursor:zoom-in;border:1px solid var(--wasp-g5)"></div><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.6em;text-align:center">(Click to zoom): A few of the directions designers came back with. Honestly, none of them really clicked for us, probably because the process happened too much in isolation from us.</figcaption></figure></div>
<p>After cycling through enough of these, the visual direction we wanted got pretty obvious. Wasp's brand should look like what it is: <strong>a technical spec</strong>. Blueprints, schematics, annotated diagrams: the kind of stuff engineers draw before they go and build.</p>
<p>That single idea ended up driving basically everything in the new identity:</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="The new Wasp brand system: monospace type, the squared =} mark, Wasp Yellow / Ink Black / Blueprint Paper colors, and schematic illustrations" src="https://wasp.sh/img/wasp-rebrand/rebrand-system.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">The new Wasp brand system.</figcaption></figure></div>
<p><strong>Monospace first.</strong> Our primary typeface is JetBrains Mono, backed by IBM Plex Mono. Monospace is the native typography of our world and we just really like how it looks. We were deliberating about using monospace literally everywhere, but in the end decided to settle on titles and subtitles, while the body text in the docs and the blog is IMB Plex Sans - easier on the eyes, but still feels close to the monospace aesthetic.</p>
<p><strong>Three colors, and that's it.</strong> Wasp Yellow (#F5C842), Ink Black (#111), and a warm paper tone we call Blueprint Paper (#F7F5F0). Yellow and black because, well, wasp. Paper because real engineering drawings live on paper, not on gradients.</p>
<p><strong>Sharp corners, visible structure.</strong> 90° angles, 2px strokes, no rounded-blob softness anywhere. Our illustrations aren't there to decorate the page. They're schematics: boxes, connectors, annotations, the architecture of your app drawn like a circuit diagram.</p>
<p><strong>The mark.</strong> The <code>=}</code> symbol, set in a solid square. It's pulled straight from code, works at any size from a favicon to a billboard, and if you squint a bit it still reads as a wasp's wings and antennae. It didn't arrive fully formed, though. The project actually started life as <em>stick</em> (yes, there was a literal photo of a wooden stick), then <em>stic</em> (Specification to Implementation Compiler), and finally <em>wasp</em>. The <code>=}</code> itself only showed up around the Alpha launch in 2020, sitting inside a yellow circle. The 2026 rebrand was basically the moment we finally squared it off and stopped fiddling with it:</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="The Wasp mark across four eras: STIC (2019, a stick), wasp (2019, three triangles), Alpha (2020, the =} in a yellow circle), and today (2026, the =} bulked up and locked in a square)" src="https://wasp.sh/img/wasp-rebrand/rebrand-logo-progression-real.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">The Wasp mark, from a stick to a square.</figcaption></figure></div>
<p><strong>Light, on purpose.</strong> One thing we very deliberately kept: Wasp stays light theme-first. It would have been the easy, default move to go dark-mode-everything and look like every other devtool out there. But people have always told us that Wasp's look feels <em>happy</em> (warm and approachable in a category that mostly reads as cold and serious), and we didn't want to just throw that away to become another Linear clone. The world already has plenty of near-black landing pages with a purple gradient on top. Blueprint paper is bright, optimistic, and it's ours.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="constraints-are-the-whole-point---both-in-design-and-code">Constraints are the whole point - both in design and code<a href="https://wasp.sh/blog/2026/07/13/why-design-matters-for-a-web-framework#constraints-are-the-whole-point---both-in-design-and-code" class="hash-link" aria-label="Direct link to Constraints are the whole point - both in design and code" title="Direct link to Constraints are the whole point - both in design and code" translate="no">​</a></h2>
<p>Here's the part we honestly didn't expect: the best thing about the new brand isn't really how it looks, but how <em>fast</em> it now makes us move.</p>
<p>Two fonts, three colors, one illustration style, and only sharp corners. It sounds restrictive on paper, and it is, but in the best possible way. Before, no one really knew what to reach for, so everyone would just do their own thing each time. Now there's almost nothing to decide. A launch banner, a chart for Twitter, a docs page, a sticker - they all kind of assemble themselves from the same small kit. It's actually the same philosophy as Wasp itself: strong defaults and clear structure don't limit you - they free you up to focus on the parts that actually matter.</p>
<p>We even wrote ourselves a proper brandbook in the end, partly because we genuinely needed one, and partly because after five years of winging it, it just felt really, really good to finally have one.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="The Wasp brandbook color-system page: five vertical stripes showing Wasp Yellow (#F5C842), Ink Black (#111111), Paper (#F7F5F0), Gray (#777777), and Tint (#FFF3CC), with the squared =} mark set on the yellow column" src="https://wasp.sh/img/wasp-rebrand/rebrand-brandbook.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">The color-system page from our brandbook.</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="we-shipped-it-like-a-rolling-release">We shipped it like a rolling release<a href="https://wasp.sh/blog/2026/07/13/why-design-matters-for-a-web-framework#we-shipped-it-like-a-rolling-release" class="hash-link" aria-label="Direct link to We shipped it like a rolling release" title="Direct link to We shipped it like a rolling release" translate="no">​</a></h2>
<p>We didn't flip a switch one morning and unveil a finished new site. We rolled it out the same way we ship software: piece by piece, swapping new parts in while the old ones were still live.</p>
<p>So for a while, wasp.sh was a bit of a Frankenstein (honestly, it kind of still is). A shiny new hero sitting on top of an old footer, new buttons next to old cards, fonts not quite agreeing with each other across the page. It looked pretty rough, but the old site already looked rough, so a half-migrated version was still a step up, not a regression. The bar, luckily for us, was very forgiving.</p>
<p>And honestly, the awkwardness actually helped. Having a half-finished site sitting out in the open was a little embarrassing, which was kind of the whole point. Nobody really wants a Frankenstein page on the internet with their name on it, so we kept sprinting to fix the next piece before too many people saw it in that state. The embarrassment basically became a deadline we couldn't ignore.</p>
<p>And it was the only realistic way we were ever going to get it done. If we'd waited to perfect every pixel and ship it all at once, we'd genuinely still be waiting today. Wasp moves too fast for a big-bang redesign. By the time it was "done", half of it would already be stale. Shipping it in pieces is what actually kept us moving.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="designing-it-changed-what-we-think-wasp-is">Designing it changed what we think Wasp is<a href="https://wasp.sh/blog/2026/07/13/why-design-matters-for-a-web-framework#designing-it-changed-what-we-think-wasp-is" class="hash-link" aria-label="Direct link to Designing it changed what we think Wasp is" title="Direct link to Designing it changed what we think Wasp is" translate="no">​</a></h2>
<p>Another thing we found really valuable about this whole process had nothing to do with how anything looked.</p>
<p>To draw a thing, you first have to know what the thing actually <em>is</em>. So every design decision kept bouncing back at us as a product question: who is this really for, what do we stand for, what do we actually want to say. Before we knew it, the rebrand had quietly turned into a rewrite. We reworked our words, our positioning, our whole story, and then the new words changed the design, and the new design changed the words again. The two kept reshaping and reinforcing each other.</p>
<p>That loop is exactly why we're glad we didn't just hand this whole thing off to someone external. An agency can absolutely make you <em>look</em> good. What it can't really do is the part that turned out to matter most: the way designing the brand forces you to understand, and sometimes redraw, the product itself. That kind of thing only really happens when the people building the product are also the ones drawing it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-do-you-think">What do you think?<a href="https://wasp.sh/blog/2026/07/13/why-design-matters-for-a-web-framework#what-do-you-think" class="hash-link" aria-label="Direct link to What do you think?" title="Direct link to What do you think?" translate="no">​</a></h2>
<p>The new look is now live on <a href="https://wasp.sh/" target="_blank" rel="noopener noreferrer" class="">wasp.sh</a>, across our docs, and everywhere else Wasp shows up. Like everything we ship, it'll keep evolving, and we'd love your honest take: the good, the bad, and the "did it really have to be monospace?".</p>
<p>Come tell us on <a href="https://discord.gg/rzdnErX" target="_blank" rel="noopener noreferrer" class="">Discord</a>, <a href="https://twitter.com/WaspLang" target="_blank" rel="noopener noreferrer" class="">Twitter/X</a>, or <a href="https://github.com/wasp-lang/wasp" target="_blank" rel="noopener noreferrer" class="">GitHub</a>. 🐝</p>]]></content:encoded>
            <author>matija@wasp-lang.dev (Matija Sosic)</author>
            <category>wasp</category>
            <category>design</category>
            <category>branding</category>
            <category>rebrand</category>
            <category>web dev</category>
        </item>
        <item>
            <title><![CDATA[Roll your own file-based router in under 50 lines of code]]></title>
            <link>https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router</link>
            <guid>https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router</guid>
            <pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Wasp's spec is just a TypeScript program, so file-based routing isn't a framework feature you wait for — it's under 50 lines of code you own. Here's how to build one with the exact conventions you want.]]></description>
            <content:encoded><![CDATA[<p>Nowadays, web frameworks come with ✨ magic ✨. Some more than others, but all of them have some. The magic is usually in the form of conventions: do things their way and the framework helps you out; wander off the happy path and you're on your own.</p>
<!-- -->
<p>One of the main tricks they pull is quietly turning your folder structure into a config file. Create <code>app/about-us/page.tsx</code> in a Next.js app and <code>https://localhost:3000/about-us</code> magically starts working. Rename the file, and the URL changes with it. The framework is reading your file tree and turning it into a routing table.</p>
<p>That is called <strong>file-based routing</strong>, and it's usually magic your framework either hands you or it doesn't; and when it does, you rarely get to customize it, the conventions are baked in. Want a folder name to carry special meaning, or a <code>logged-in.tsx</code>/<code>logged-out.tsx</code> split per route? Your only option is to post a feature request and wait, or go without.</p>
<p>In Wasp, we really like explicitness, so we've resisted shipping a file-based router. That's changing now, but of course, we're not just adding the magic conventions and moving on. We're giving you a way to choose and design your own magic.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wait-whats-wasp">Wait, what's Wasp?<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#wait-whats-wasp" class="hash-link" aria-label="Direct link to Wait, what's Wasp?" title="Direct link to Wait, what's Wasp?" translate="no">​</a></h2>
<p>Wasp is a batteries-included full-stack web framework for TypeScript. It covers your frontend, backend, and database as one cohesive whole.</p>
<p>And one of our core ideas is that the entry point to all this full-stack goodness is through an explicit <strong>Spec</strong>. Instead of hiding your app's structure behind implicit conventions, you declare its features (like routes, authentication, queries, cron jobs, etc) directly in code. Wasp then compiles and stitches these pieces together:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> app</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> route</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> page </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// `type: ref` means we just want to point to these files, not execute them now:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">MainPage</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/pages/MainPage"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">UserPage</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/pages/LoginPage"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">app</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// Some example configurations, we'll skip them in the future:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"my-app"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  wasp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> version</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"^0.24.0"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  auth</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> userEntity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"User"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> google</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// The spec is where you declare your app's features:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  spec</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"MainRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">MainPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"LoginRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/login"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">UserPage</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> authRequired</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>We chose this approach because explicit code is easier to reason about than hidden magic. This transparency makes the codebase easier to navigate for your team, and it gives AI code assistants a clear, structured map of your application to work with.</p>
<p>That explicitness is the whole philosophy, and it's exactly why we've deliberately never shipped file-based routing. We just didn't want to bake that pile of implicit conventions into the framework.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-spec-is-just-a-program">The Spec is just a program<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#the-spec-is-just-a-program" class="hash-link" aria-label="Direct link to The Spec is just a program" title="Direct link to The Spec is just a program" translate="no">​</a></h2>
<p>But here's the part that makes it exciting. We recently <a class="" href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec">updated our spec</a> so that it lives in a <code>main.wasp.ts</code> file. And instead of being a static JSON file that the framework reads, it is just <strong>a standard Node.js program</strong>, written in TypeScript. You can pull in libraries from npm, read from disk, call an API, use environment variables, anything! The only rule is that you export an <code>app</code> at the end. That's all we need.</p>
<p>So the Spec is explicit, but it's also <em>programmable</em>. And nothing says those <code>route</code> and <code>page</code> calls have to be typed out by hand.</p>
<p>That's the entire trick. You can write a function that walks your <code>src/</code> folder and returns the same <code>route</code> and <code>page</code> objects you'd otherwise write manually. File-based routing stops being internal magic, and becomes a few lines of ordinary code that live in your repo, that you can read, and more importantly, customize.</p>
<p>So even though we never shipped file-based routing, "we didn't ship it" doesn't mean "you can't have it". You add it yourself, with exactly the conventions you want. You get the best of both worlds, and nobody has to argue about defaults. Sorry if you liked the arguing.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-simplest-possible-router">The simplest possible router<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#the-simplest-possible-router" class="hash-link" aria-label="Direct link to The simplest possible router" title="Direct link to The simplest possible router" translate="no">​</a></h2>
<p>Let's build that. Because the route list is just an array of objects, the simplest possible "router" is one we write out by hand:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// The `ref` helper is the dynamic version of `with { type: "ref" }` imports, so we'll use it to build programmatically.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> app</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> page</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> route</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> ref </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">app</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  spec</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"RootRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">ref</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> importDefault</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"RootPage"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/page.tsx"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"AboutUsRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"/about-us"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token function" style="color:#d73a49">ref</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> importDefault</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"AboutUsPage"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/about-us/page.tsx"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>That's a functioning router already, but the unsatisfying part is that we had to type it out ourselves. So let's <em>generate</em> it instead, by looking at the project files.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-step-up-generate-the-routes-from-the-filesystem">The step up: generate the routes from the filesystem<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#the-step-up-generate-the-routes-from-the-filesystem" class="hash-link" aria-label="Direct link to The step up: generate the routes from the filesystem" title="Direct link to The step up: generate the routes from the filesystem" translate="no">​</a></h2>
<p>We'll pick a very simple convention: <strong>a <code>page.tsx</code> inside a folder makes it a route.</strong> So <code>src/about-us/page.tsx</code> serves <code>https://my-app.com/about-us</code>, and <code>src/page.tsx</code> serves <code>https://my-app.com/</code>.</p>
<p>Here's the whole thing:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> page</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> ref</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> route </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> pascalCase </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"es-toolkit"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> globSync </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"node:fs"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports operator" style="color:#393A34">*</span><span class="token imports"> </span><span class="token imports keyword module" style="color:#00009f">as</span><span class="token imports"> path</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"node:path"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token function-variable function" style="color:#d73a49">fileBasedRoutes</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">baseDir</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">globSync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"**/page.tsx"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> cwd</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> baseDir </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">sort</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Make the list stable between runs</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">filePath</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> absoluteFilePath </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> path</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">resolve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">baseDir</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> filePath</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> urlRoute </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> filePath</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">split</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">path</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sep</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">slice</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Removes the "page.tsx" part</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">join</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> routeName </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">pascalCase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">urlRoute</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Root"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">routeName</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">Route</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token string" style="color:#e3116c">"/"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> urlRoute</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token function" style="color:#d73a49">ref</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            importDefault</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">routeName</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">Page</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> absoluteFilePath</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>That's the entire idea. Find every <code>page.tsx</code>, turn its folder path into a URL path and a name, and produce the <code>route()</code>s. For the route name we can even <a href="https://es-toolkit.dev/reference/string/pascalCase.html" target="_blank" rel="noopener noreferrer" class="">lean on an npm package</a> to get a clean name; it's just an ordinary npm dependency, like in any Node project.</p>
<p>There's nothing clever going on here, it's plain filesystem glue. An inexperienced dev could write this without much trouble. It never reaches into Wasp's internals, and it only calls the same public <code>route</code>, <code>page</code>, and <code>ref</code> functions you'd use by hand.</p>
<p>Wiring it into your app is one line:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// main.wasp.ts</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> app </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> fileBasedRoutes </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./lib/file-based-routes.wasp"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">app</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  spec</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">fileBasedRoutes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"src"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// Any other Specs your app uses (job(), query(), action(), ...)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p><code>fileBasedRoutes</code> returns an array of route objects, and we drop it straight into <code>spec</code>. Voilà, you have file-based routing!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="everything-wasp-already-does-still-works">Everything Wasp already does still works<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#everything-wasp-already-does-still-works" class="hash-link" aria-label="Direct link to Everything Wasp already does still works" title="Direct link to Everything Wasp already does still works" translate="no">​</a></h2>
<p>Because we're producing normal <code>route</code> and <code>page</code> spec objects, the rest of Wasp neither knows nor cares that they were generated. So all the features you'd expect keep working, including <a class="" href="https://wasp.sh/docs/advanced/links">type-safe links</a>:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">Link</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"wasp/client/router"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">MainPage</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">h1</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text">Main page</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">h1</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#00009f">Link</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">to</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">/about-us</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text">About us</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag class-name" style="color:#00009f">Link</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>The <code>&lt;Link&gt;</code> component is fully typechecked with the routes your function generated. Generate a route from a folder, and you will get autocomplete and type errors for it everywhere. Nice.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="now-make-it-yours-route-groups">Now make it yours: route groups<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#now-make-it-yours-route-groups" class="hash-link" aria-label="Direct link to Now make it yours: route groups" title="Direct link to Now make it yours: route groups" translate="no">​</a></h2>
<p>Here's where it gets fun. This is <em>your</em> code, so bend it to your project. Say you want <strong>route groups:</strong> folders that organize your files without showing up in the URL. The usual convention is to wrap them in parentheses, like <code>(marketing)</code>.</p>
<p>It's a one-line filter:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">ROUTE_GROUP_REGEX</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">^</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\(</span><span class="token regex regex-source language-regex char-set class-name" style="color:#36acaa">.</span><span class="token regex regex-source language-regex quantifier number" style="color:#36acaa">*</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\)</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">$</span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Wrapped in parentheses</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> urlRoute </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> filePath</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">split</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">path</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sep</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">slice</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Removes the "page.tsx" part</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">part</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token constant" style="color:#36acaa">ROUTE_GROUP_REGEX</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">part</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Remove any route groups</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">join</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>Now <code>src/(marketing)/about-us/page.tsx</code> still serves <code>/about-us</code>, but you can keep your marketing pages tidily grouped.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-convention-for-prerender-and-auth">A convention for prerender and auth<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#a-convention-for-prerender-and-auth" class="hash-link" aria-label="Direct link to A convention for prerender and auth" title="Direct link to A convention for prerender and auth" translate="no">​</a></h2>
<p>Now let's say that in your project, there are two settings you're reaching for constantly: marking a page as auth-required, or marking it for prerendering. Let's invent a convention for both, reusing the route-group syntax we just added: an <code>(auth)</code> group makes everything inside require auth, and a <code>(prerender)</code> group marks pages for prerendering.</p>
<p>We detect those folders and thread the result into the spec objects:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> isPrerender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> filePath</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">includes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"(prerender)"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> isAuth </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> filePath</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">includes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"(auth)"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">routeName</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">Route</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token string" style="color:#e3116c">"/"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> urlRoute</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">ref</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      importDefault</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">routeName</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">Page</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> absoluteFilePath</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> authRequired</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> isAuth </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> prerender</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> isPrerender </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>These are <em>your</em> conventions. Don't like parentheses for this? Use a <code>.auth.tsx</code> suffix, a <code>"use auth"</code> directive, or a config file next to the page; whatever reads best for your team. The spec doesn't dictate the shape, you do!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dynamic-and-optional-segments">Dynamic and optional segments<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#dynamic-and-optional-segments" class="hash-link" aria-label="Direct link to Dynamic and optional segments" title="Direct link to Dynamic and optional segments" translate="no">​</a></h2>
<p>Wasp routing also supports <a class="" href="https://wasp.sh/docs/advanced/routing#dynamic-segments">dynamic</a> (<code>:id</code>) and <a class="" href="https://wasp.sh/docs/advanced/routing#optional-segments">optional</a> (<code>:id?</code>) segments. Written out in the spec by hand, they look like this:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ProductRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/products/:productId"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">ProductPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ArticleRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/articles/:slug?"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">ArticlePage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><br></div></code></pre></div></div>
<p>The <code>:</code> character is not allowed in file paths, so let's copy Next.js's bracket syntax for this: <code>[productId]</code> becomes <code>:productId</code>, and <code>[[productId]]</code> becomes <code>:productId?</code>:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">DYNAMIC_SEGMENT_REGEX</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">^</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\[</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">(</span><span class="token regex regex-source language-regex char-set class-name" style="color:#36acaa">.</span><span class="token regex regex-source language-regex quantifier number" style="color:#36acaa">*</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">)</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\]</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">$</span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Wrapped in square brackets</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">OPTIONAL_SEGMENT_REGEX</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">^</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\[</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\[</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">(</span><span class="token regex regex-source language-regex char-set class-name" style="color:#36acaa">.</span><span class="token regex regex-source language-regex quantifier number" style="color:#36acaa">*</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">)</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\]</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\]</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">$</span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Wrapped in two square brackets</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> urlRoute </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> filePath</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">split</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">path</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sep</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">slice</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Removes the "page.tsx" part</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">part</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token constant" style="color:#36acaa">ROUTE_GROUP_REGEX</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">part</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Remove any route groups</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">part</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> part</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">replace</span><span class="token punctuation" style="color:#393A34">(</span><span class="token constant" style="color:#36acaa">OPTIONAL_SEGMENT_REGEX</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">":$1?"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Convert optional segments</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">part</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> part</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">replace</span><span class="token punctuation" style="color:#393A34">(</span><span class="token constant" style="color:#36acaa">DYNAMIC_SEGMENT_REGEX</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">":$1"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Convert dynamic segments</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">join</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>Or you can invent your own syntax. The point is that it's just code, so you do you.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="45-lines-later">45 lines later<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#45-lines-later" class="hash-link" aria-label="Direct link to 45 lines later" title="Direct link to 45 lines later" translate="no">​</a></h2>
<p>We're done!</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> page</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> ref</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> route </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> pascalCase </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"es-toolkit"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> globSync </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"node:fs"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports operator" style="color:#393A34">*</span><span class="token imports"> </span><span class="token imports keyword module" style="color:#00009f">as</span><span class="token imports"> path</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"node:path"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">ROUTE_GROUP_REGEX</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">^</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\(</span><span class="token regex regex-source language-regex char-set class-name" style="color:#36acaa">.</span><span class="token regex regex-source language-regex quantifier number" style="color:#36acaa">*</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\)</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">$</span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Wrapped in parentheses</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">DYNAMIC_SEGMENT_REGEX</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">^</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\[</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">(</span><span class="token regex regex-source language-regex char-set class-name" style="color:#36acaa">.</span><span class="token regex regex-source language-regex quantifier number" style="color:#36acaa">*</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">)</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\]</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">$</span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Wrapped in square brackets</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">OPTIONAL_SEGMENT_REGEX</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">^</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\[</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\[</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">(</span><span class="token regex regex-source language-regex char-set class-name" style="color:#36acaa">.</span><span class="token regex regex-source language-regex quantifier number" style="color:#36acaa">*</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">)</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\]</span><span class="token regex regex-source language-regex special-escape escape" style="color:#36acaa">\]</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">$</span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Wrapped in two square brackets</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token function-variable function" style="color:#d73a49">fileBasedRoutes</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">baseDir</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">globSync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"**/page.tsx"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> cwd</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> baseDir </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">sort</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Make the list stable between runs</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">filePath</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> absoluteFilePath </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> path</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">resolve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">baseDir</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> filePath</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> isPrerender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> filePath</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">includes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"(prerender)"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> isAuth </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> filePath</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">includes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"(auth)"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> urlRoute </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> filePath</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">split</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">path</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sep</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">slice</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Removes the "page.tsx" part</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">part</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token constant" style="color:#36acaa">ROUTE_GROUP_REGEX</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">part</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Remove any route groups</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">part</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> part</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">replace</span><span class="token punctuation" style="color:#393A34">(</span><span class="token constant" style="color:#36acaa">OPTIONAL_SEGMENT_REGEX</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">":$1?"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Convert optional segments</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">part</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> part</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">replace</span><span class="token punctuation" style="color:#393A34">(</span><span class="token constant" style="color:#36acaa">DYNAMIC_SEGMENT_REGEX</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">":$1"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Convert dynamic segments</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">join</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> routeName </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">pascalCase</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">urlRoute</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Root"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">routeName</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">Route</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token string" style="color:#e3116c">"/"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> urlRoute</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token function" style="color:#d73a49">ref</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            importDefault</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">routeName</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">Page</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> absoluteFilePath</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> authRequired</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> isAuth </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> prerender</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> isPrerender </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>That's it. You can start using these conventions as you see fit. For example, with the folder layout below:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">src/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── (prerender)/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│   └── (marketing)/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│       ├── page.tsx</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│       └── about-us/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│           └── page.tsx</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── products/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│   └── [productId]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│       └── page.tsx</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">└── (auth)/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    └── dashboard/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        └── page.tsx</span><br></div></code></pre></div></div>
<p>...our function produces this spec:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">app</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  spec</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">fileBasedRoutes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"src"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// the above call ⬆️ will turn into the following specs ⬇️</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"RootPage"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">RootPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> prerender</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"AboutUsPage"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/about-us"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">AboutUsPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> prerender</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ProductIdPage"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/products/:productId"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">ProductIdPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"DashboardPage"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/dashboard"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">DashboardPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> authRequired</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>Add more files in that <code>(auth)</code>, and those pages become auth-required; name a folder <code>[[slug]]</code> and you get an optional segment. We've covered the most useful bits of Wasp's routing and exposed them through an entirely new API surface, and all of it lives in under 50 lines you can read end to end in a minute.</p>
<p>And the same trick isn't limited to routes. Because the spec is just data your program returns, you can generate any of it the same way: <code>api()</code>, <code>query()</code>, <code>job()</code>, or <code>action()</code>, all from your own file layout. With other frameworks you might need weird hacks or an escape hatch to do this, but in Wasp there's nothing to escape from.</p>
<p>This is the whole reason we keep going on about the spec being a first-class, programmable thing. It means that file-based routing didn't have to become a Wasp feature with a dozen options and an opt-out. It just became a small file you can control. <a class="" href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec">Meta-meta-framework, anyone?</a></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="roll-your-own">Roll your own<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#roll-your-own" class="hash-link" aria-label="Direct link to Roll your own" title="Direct link to Roll your own" translate="no">​</a></h2>
<p>Want a more full-featured file-based router than the version above? We are building one you can use or learn from: <a href="https://github.com/wasp-lang/file-based-routing" target="_blank" rel="noopener noreferrer" class="">wasp-lang/file-based-routing</a>. We're still working on it, but it has quite a few more features than the one here. Other people might create their own libraries, and you can use those too!</p>
<p>But the whole point of this post is that you don't have to use anyone's library. Run <code>wasp new</code>, copy our starting example, and build the conventions that fit <em>your</em> project.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-future">The future<a href="https://wasp.sh/blog/2026/07/01/roll-your-own-file-based-router#the-future" class="hash-link" aria-label="Direct link to The future" title="Direct link to The future" translate="no">​</a></h2>
<p>The same "spec is just a program" idea doesn't stop at routing. We're actively working on <a class="" href="https://wasp.sh/blog/2026/06/22/javascript-still-cant-ship-a-full-stack-module">full-stack modules</a>: the idea that you'll be able to pull libraries that don't only contain backend or frontend code, but full-stack code, defined as a Spec. File-based routing might be a part of those specs in the future! But it all goes towards giving you the magic of conventions without losing control and explicitness when you want it.</p>
<p>If you come up with something good, we'd genuinely love to see it, come share it on <a href="https://discord.gg/rzdnErX" target="_blank" rel="noopener noreferrer" class="">our Discord</a>!</p>]]></content:encoded>
            <category>wasp</category>
            <category>typescript</category>
            <category>routing</category>
            <category>file-based routing</category>
            <category>full-stack</category>
        </item>
        <item>
            <title><![CDATA[JavaScript still can't ship a full-stack module]]></title>
            <link>https://wasp.sh/blog/2026/06/22/javascript-still-cant-ship-a-full-stack-module</link>
            <guid>https://wasp.sh/blog/2026/06/22/javascript-still-cant-ship-a-full-stack-module</guid>
            <pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Why JavaScript lacks true full-stack modules, what Rails and Laravel get right, and how Wasp can bring that pattern to JS.]]></description>
            <content:encoded><![CDATA[<p>Imagine if there were a way for us to somehow ship a <em>full-stack</em> 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.</p>
<!-- -->
<p>Take 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.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Different ways to write a full-stack feature" src="https://wasp.sh/img/javascript-still-cant-ship-a-full-stack-module/banner.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Different ways you can write a full-stack feature.</figcaption></figure></div>
<p>Yeah yeah, I know what you are thinking. LLMs are writing most of the code these days, who cares? <strong>Ok, but it's already been established: what's good for humans is good for LLMs. They like to be lazy too.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="agents-love-working-with-legos">Agents love working with legos<a href="https://wasp.sh/blog/2026/06/22/javascript-still-cant-ship-a-full-stack-module#agents-love-working-with-legos" class="hash-link" aria-label="Direct link to Agents love working with legos" title="Direct link to Agents love working with legos" translate="no">​</a></h2>
<p>Opus, 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.</p>
<p>But 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.</p>
<p>Matt 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 href="https://www.amazon.com/Philosophy-Software-Design-John-Ousterhout/dp/1732102201" target="_blank" rel="noopener noreferrer" class="">A Philosophy of Software Design</a> 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 <em>thin interface</em>), but hides all the internal complexities of the actual implementation.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Matt Pocock video about preparing codebases for AI" src="https://wasp.sh/img/javascript-still-cant-ship-a-full-stack-module/matt-pocock-deep-modules.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"><a href="https://youtu.be/uC44zFz7JSM?si=uike3KKy_fbc01TU" target="_blank" rel="noreferrer noopener" class="">“Your codebase is NOT ready for AI (here's how to fix it)”</a></figcaption></figure></div>
<p>The 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="legos-have-a-name-full-stack-modules">Legos have a name: full-stack modules<a href="https://wasp.sh/blog/2026/06/22/javascript-still-cant-ship-a-full-stack-module#legos-have-a-name-full-stack-modules" class="hash-link" aria-label="Direct link to Legos have a name: full-stack modules" title="Direct link to Legos have a name: full-stack modules" translate="no">​</a></h2>
<p>While developing <a class="" href="https://wasp.sh/">Wasp</a>, 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.</p>
<p>We usually talk about applications in two ways:</p>
<ol>
<li class="">One is by technical layers: backend server, frontend client, database.</li>
<li class="">The other is by vertical slices: a complete feature that runs through the whole stack. Payments is our standard example: backend logic, the button the user clicks, and the database model that stores the state. A full-stack module is one of these vertical slices.</li>
</ol>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Technical layers compared with vertical slices in an application" src="https://wasp.sh/img/javascript-still-cant-ship-a-full-stack-module/technical-layers-vs-vertical-slices.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Technical layers vs vertical slices of an application.</figcaption></figure></div>
<p>In 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.</p>
<p>That 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="researching-full-stack-modules-in-11-frameworks">Researching full-stack modules in 11 frameworks<a href="https://wasp.sh/blog/2026/06/22/javascript-still-cant-ship-a-full-stack-module#researching-full-stack-modules-in-11-frameworks" class="hash-link" aria-label="Direct link to Researching full-stack modules in 11 frameworks" title="Direct link to Researching full-stack modules in 11 frameworks" translate="no">​</a></h2>
<p>To 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.</p>
<p>I 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.</p>
<p>To make it easy to compare, I created a static HTML prototype and tried to flesh out the requirements:</p>
<ul>
<li class="">users can sign up / log in</li>
<li class="">they can browse products</li>
<li class="">they can buy products with Stripe</li>
<li class="">they can see their purchased items</li>
</ul>
<p>In all of the frameworks, we want to see the same webshop (same design, same capabilities).</p>
<div style="display:flex;justify-content:center;margin:1rem 0"><figure style="width:100%;margin:0"><video width="100%" controls="" aria-label="Demo of the payments module in Django"><source src="/img/javascript-still-cant-ship-a-full-stack-module/fsm.mp4" type="video/mp4"></video><figcaption class="image-caption" style="font-style:italic;opacity:0.6;font-size:0.9rem">Demo of the payments module in Django</figcaption></figure></div>
<p>Our webshop is made out of auth, payments, and products modules. In this exercise, we told the agent that the "payments" module <em>must be implemented</em> 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.</p>
<p>We took all these requirements and built the same app in 11 different frameworks. See the list here: <a href="https://fsm-research.static.miho.dev/" target="_blank" rel="noopener noreferrer" class="">https://fsm-research.static.miho.dev</a></p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="How host apps use a payments module in different frameworks" src="https://wasp.sh/img/javascript-still-cant-ship-a-full-stack-module/payments-module-frameworks.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">How the host app uses the payments module in different frameworks.</figcaption></figure></div>
<p>Playing 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.</p>
<p>For example:</p>
<ol>
<li class="">In some frameworks, sharing a full-stack feature is much more polished.<!-- -->
<ol>
<li class="">For example, in Orchard Core, you define a <code>Startup</code> class which has the ability to register services, database migrations, and add client and server routes as soon as the payments module is registered.</li>
<li class="">In Rails, once you install the <code>payments</code> engine, based on the Rails folder conventions, you get views, models, controllers, etc. automatically registered.</li>
</ol>
</li>
<li class="">But in some, the experience of sharing a module was more hands-on.<!-- -->
<ol>
<li class="">My experience with RedwoodJS was a bit less smooth. I had to install the package twice, once in the <code>web</code> app and then again in the <code>api</code> app. I had to manually define queries and re-export pages to get them registered.</li>
</ol>
</li>
</ol>
<p>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).</p>
<p>For 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: <a href="https://fsm-research.static.miho.dev/fsm-tier-list" target="_blank" rel="noopener noreferrer" class="">https://fsm-research.static.miho.dev/fsm-tier-list</a></p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Tier list showing support for full-stack modules across frameworks" src="https://wasp.sh/img/javascript-still-cant-ship-a-full-stack-module/full-stack-modules-tier-list.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Tier list of support for full-stack modules in various frameworks. JS frameworks are highlighted in yellow.</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-there-are-no-full-stack-modules-in-js">Why there are no full-stack modules in JS<a href="https://wasp.sh/blog/2026/06/22/javascript-still-cant-ship-a-full-stack-module#why-there-are-no-full-stack-modules-in-js" class="hash-link" aria-label="Direct link to Why there are no full-stack modules in JS" title="Direct link to Why there are no full-stack modules in JS" translate="no">​</a></h2>
<p>Hm, 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.</p>
<p>Full-stack modules need:</p>
<ol>
<li class="">A way to ship runtime code (React components, Node.js server code, etc.)</li>
<li class="">A way to ship glue (code that describes how pieces fit together)</li>
<li class="">A way to distribute an installable unit</li>
</ol>
<p>JavaScript ecosystem has no shortage of great libraries, and in great part that's due to how easy it is to ship code with <code>npm</code>. I'd say that sorts out points 1 and 3: it's easy to ship and install libraries.</p>
<p>But, 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?</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="A full-stack module shipped as an npm package" src="https://wasp.sh/img/javascript-still-cant-ship-a-full-stack-module/npm-full-stack-module.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">A full-stack module shipped as an npm package.</figcaption></figure></div>
<p>There is no standard full-stack layer you can rely on when shipping a library, so you can't really ship full-stack libraries.</p>
<p>One 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.</p>
<p>When 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wasp-is-doing-something-about-it">Wasp is doing something about it<a href="https://wasp.sh/blog/2026/06/22/javascript-still-cant-ship-a-full-stack-module#wasp-is-doing-something-about-it" class="hash-link" aria-label="Direct link to Wasp is doing something about it" title="Direct link to Wasp is doing something about it" translate="no">​</a></h2>
<p>So 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.</p>
<p>The 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.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="The Wasp Spec file defines application wiring code in one place" src="https://wasp.sh/img/javascript-still-cant-ship-a-full-stack-module/wasp-spec-file.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">You define all the wiring code in one place: the Wasp Spec file.</figcaption></figure></div>
<p>The 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.</p>
<p>One package which ships:</p>
<ul>
<li class="">React and Node.js code,</li>
<li class="">and the spec file that explains how to wire it all together.</li>
</ul>
<p>I 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.</p>
<p>Let'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.</p>
<p>Here's an example Wasp spec file:</p>
<div class="language-typescript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-typescript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// main.wasp.ts</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Regular Wasp app: the app owns the payments wiring.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> action</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> api</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> app</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> page</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> route </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> LandingPage </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/landing-page/LandingPage"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> PricingPage </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/payment/PricingPage"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> CheckoutResultPage </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/payment/CheckoutResultPage"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> generateCheckoutSession</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> getCustomerPortalUrl </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/payment/operations"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> paymentsMiddlewareConfigFn</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> paymentsWebhook </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/payment/webhook"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">app</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  spec</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"LandingPageRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">LandingPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> prerender</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// Payments "glue" code</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"PricingPageRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/pricing"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">PricingPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      prerender</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"CheckoutResultRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"/checkout"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">CheckoutResultPage</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> authRequired</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">query</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">getCustomerPortalUrl</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"User"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">action</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">generateCheckoutSession</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"User"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">api</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"POST"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/payments-webhook"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> paymentsWebhook</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"User"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      middlewareConfigFn</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> paymentsMiddlewareConfigFn</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>We see that to implement payments we need:</p>
<ul>
<li class="">two client pages: <code>/pricing</code> and <code>/checkout</code></li>
<li class="">two operations: <code>getCustomerPortalUrl</code> and <code>generateCheckoutSession</code></li>
<li class="">and a webhook endpoint at <code>/payments-webhook</code></li>
</ul>
<p>For each of those pieces, we also have runtime code in files like <code>PricingPage.tsx</code> and <code>operations.ts</code>.</p>
<p>So 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.</p>
<p>Let's say we shipped <code>stripePayments</code> as an <code>npm</code> package. This is how using the full-stack module would look:</p>
<div class="language-typescript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-typescript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// main.wasp.ts</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Wasp app with a full-stack payments module.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> app</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> page</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> route </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> stripePayments </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@acme/stripe-payments/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> LandingPage </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/landing-page/LandingPage"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">app</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  spec</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"LandingPageRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">LandingPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> prerender</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">stripePayments</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Payments"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> User</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"User"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      plans</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        hobby</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          kind</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"subscription"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          priceIdEnvVar</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        pro</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          kind</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"subscription"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          priceIdEnvVar</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        credits10</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          kind</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"credits"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          amount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          priceIdEnvVar</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"PAYMENTS_CREDITS_10_PLAN_ID"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>In our example, you:</p>
<ul>
<li class="">import <code>stripePayments</code> from <code>@acme/stripe-payments/spec</code>,</li>
<li class="">configure it with your app's payment plans,</li>
<li class="">get the client pages, operations, and webhook endpoints set up automatically!</li>
</ul>
<p>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.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Installing a full-stack module in a Wasp application" src="https://wasp.sh/img/javascript-still-cant-ship-a-full-stack-module/installing-wasp-full-stack-module.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Installing a full-stack module in Wasp.</figcaption></figure></div>
<p>Full-stack modules will not fit every use case. So there should be escape hatches:</p>
<ol>
<li class="">Shipping FSMs as npm packages gives authors flexibility to expose lower-level primitives. Users can rewire the high-level design without rewriting the implementation.</li>
<li class="">And of course, you can take the FSM code as a reference implementation and tweak it, reimplement pieces of it, etc.</li>
<li class="">Or even better, fork it with your tweaks and maintain an internal version specific to your organisation that you can use across your internal apps.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="open-saas-could-become-open-anything">Open SaaS could become Open Anything<a href="https://wasp.sh/blog/2026/06/22/javascript-still-cant-ship-a-full-stack-module#open-saas-could-become-open-anything" class="hash-link" aria-label="Direct link to Open SaaS could become Open Anything" title="Direct link to Open SaaS could become Open Anything" translate="no">​</a></h2>
<p>At Wasp, we've built and maintain the most popular open-source SaaS boilerplate starter, <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a>.</p>
<p>It's already packed with features:</p>
<ul>
<li class="">payments</li>
<li class="">auth methods</li>
<li class="">file upload</li>
<li class="">an LLM wrapper example</li>
<li class="">etc.</li>
</ul>
<p>But 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).</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Open SaaS, Wasp's open-source SaaS starter" src="https://wasp.sh/img/javascript-still-cant-ship-a-full-stack-module/open-saas.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Our open-source and free SaaS starter.</figcaption></figure></div>
<p>Let's imagine what full-stack modules would mean for starters like these. Instead of shipping a starter with <em>all the features</em>, you could maybe start with a nice design system and tell users to install <em>features</em> 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.</p>
<p>And 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.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Concept design for a full-stack module registry" src="https://wasp.sh/img/javascript-still-cant-ship-a-full-stack-module/module-registry.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">A concept for a future full-stack module registry.</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://wasp.sh/blog/2026/06/22/javascript-still-cant-ship-a-full-stack-module#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>We'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.</p>
<p>We think it's possible to bring the same idea to the JS ecosystem.</p>
<p>The pieces we need to make it happen:</p>
<ul>
<li class="">a stable full-stack surface to work against,</li>
<li class="">a way to ship together React components, Node.js code, and the "glue" code.</li>
</ul>
<p>And that's exactly what we want to build with Wasp. <a href="https://x.com/WaspLang" target="_blank" rel="noopener noreferrer" class="">Follow us on X</a> or join our <a href="https://discord.gg/rzdnErX" target="_blank" rel="noopener noreferrer" class="">Discord</a> to follow along.</p>]]></content:encoded>
            <author>miho@wasp-lang.dev (Mihovil Ilakovac)</author>
            <category>wasp</category>
            <category>javascript</category>
            <category>full-stack</category>
            <category>ai</category>
            <category>typescript</category>
        </item>
        <item>
            <title><![CDATA[Made with Wasp, Vol. 2 - from video editing to daycare lesson planning]]></title>
            <link>https://wasp.sh/blog/2026/06/17/made-with-wasp-vol-2</link>
            <guid>https://wasp.sh/blog/2026/06/17/made-with-wasp-vol-2</guid>
            <pubDate>Wed, 17 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Community Day of Launch Week #12: four real, paid SaaS apps shipping on Wasp right now, across creator tools, UK property tech, e-commerce, and early-childhood education.]]></description>
            <content:encoded><![CDATA[<div class="figure-container"><div class="app-showcase-grid" style="background:#F5C842;padding:14px;display:grid;grid-template-columns:1fr 1fr;gap:12px;border:3px solid #111;box-sizing:border-box"><a href="https://cutcue.io/" target="_blank" rel="noreferrer noopener" style="display:block"><img alt="CutCue" src="https://wasp.sh/img/made-with-wasp-vol2/app-cutcue.webp" style="display:block;width:100%;height:auto;border:3px solid #111;box-sizing:border-box"></a><a href="https://onbrick.co.uk/" target="_blank" rel="noreferrer noopener" style="display:block"><img alt="Brick" src="https://wasp.sh/img/made-with-wasp-vol2/app-brick.webp" style="display:block;width:100%;height:auto;border:3px solid #111;box-sizing:border-box"></a><a href="https://chatbuster.com/" target="_blank" rel="noreferrer noopener" style="display:block"><img alt="ChatBuster" src="https://wasp.sh/img/made-with-wasp-vol2/app-chatbuster.webp" style="display:block;width:100%;height:auto;border:3px solid #111;box-sizing:border-box"></a><a href="https://planrelief.io/" target="_blank" rel="noreferrer noopener" style="display:block"><img alt="PlanRelief" src="https://wasp.sh/img/made-with-wasp-vol2/app-planrelief.webp" style="display:block;width:100%;height:auto;border:3px solid #111;box-sizing:border-box"></a></div><style>@media (width<=640px){.app-showcase-grid{grid-template-columns:1fr!important}}</style></div>
<p>Welcome to Day 3 of <a class="" href="https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec">Launch Week #12</a>, a.k.a. Community Day. This is the one where we step out of the way and let the Waspeteers do the talking. 🐝</p>
<p>We get one question more than any other: <em>"okay, but what do people actually build with Wasp?"</em> Today's the answer.</p>
<p>Four apps, four verticals that have basically nothing to do with each other (video tooling, UK property tech, e-commerce, and early-childhood education), all paid, all in production, all built by people from the Wasp community. It's been a minute since <a class="" href="https://wasp.sh/blog/2023/06/28/what-can-you-build-with-wasp">Vol. 1 back in 2023</a>, and the bar has moved. Let's get into it.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cutcue---the-ai-editing-intern-for-streamers-wasp--python-workers"><a href="https://cutcue.io/" target="_blank" rel="noopener noreferrer" class="">CutCue</a> - the AI editing intern for streamers (<em><strong>Wasp + Python workers</strong></em>)<a href="https://wasp.sh/blog/2026/06/17/made-with-wasp-vol-2#cutcue---the-ai-editing-intern-for-streamers-wasp--python-workers" class="hash-link" aria-label="Direct link to cutcue---the-ai-editing-intern-for-streamers-wasp--python-workers" title="Direct link to cutcue---the-ai-editing-intern-for-streamers-wasp--python-workers" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><div style="background:#F5C842;padding:14px;border:3px solid #111;box-sizing:border-box;display:inline-block"><img style="display:block;border:3px solid #111;box-sizing:border-box" alt="CutCue landing page - 'Stop scrubbing. Start cutting.' AI tool that auto-generates timestamped markers from audio and video" src="https://wasp.sh/img/made-with-wasp-vol2/app-cutcue.webp"></div><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p><strong>Try it out:</strong> <a href="https://cutcue.io/" target="_blank" rel="noopener noreferrer" class="">cutcue.io</a>  ·  <strong>Pricing:</strong> €29 to €189/mo</p>
<p>Imagine you just streamed for six hours on Twitch and now you have to pull the three best clips for YouTube. Normally that's an evening of scrubbing the VOD with a coffee. Drop the recording into CutCue and you get back a timeline already marked up with <strong>highlights, chapter breaks, brand mentions, moments that could lose you ad revenue (e.g. swearing), and chat peaks</strong>, automatically.</p>
<div style="display:flex;justify-content:center;margin:1rem 0"><figure style="width:100%;margin:0"><video width="100%" controls="" aria-label="Short demo video of CutCue interpreting a Twitch VOD and marking up the timeline"><source src="/img/made-with-wasp-vol2/cutcue-demo-video.mp4" type="video/mp4"></video><figcaption class="image-caption" style="font-style:italic;opacity:0.6;font-size:0.9rem">CutCue interpreting a VOD in real time.</figcaption></figure></div>
<p>The cool bit: the markers don't sit in some web dashboard you have to copy-paste from. CutCue exports straight into <strong>Premiere Pro, DaVinci Resolve, Vegas, Audacity, and Reaper</strong>, so they show up exactly where your timeline expects them. You open your editor, and the AI's homework is already on the canvas.</p>
<p>Under the hood: a <strong>Wasp app orchestrating four to five Dockerized Python workers</strong> (audio extraction, chapter detection, fingerprinting, queue management), self-hosted on Hetzner, built stateless from day one so it can scale horizontally when it needs to. The kicker: shipped by a backend-heavy engineer who'd never written React before this project.</p>
<blockquote>
<p>"I described it usually as a baseline or a toolkit, the foundation for my project, which takes over the important stuff like session management, user payment, all the things which usually take a long time to build yourself and make stable and secure."</p>
<p><em>- the CutCue founder</em></p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="brick---a-website-builder-for-uk-property-operators"><a href="https://onbrick.co.uk/" target="_blank" rel="noopener noreferrer" class="">Brick</a> - a website builder for UK property operators<a href="https://wasp.sh/blog/2026/06/17/made-with-wasp-vol-2#brick---a-website-builder-for-uk-property-operators" class="hash-link" aria-label="Direct link to brick---a-website-builder-for-uk-property-operators" title="Direct link to brick---a-website-builder-for-uk-property-operators" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><div style="background:#F5C842;padding:14px;border:3px solid #111;box-sizing:border-box;display:inline-block"><img style="display:block;border:3px solid #111;box-sizing:border-box" alt="Brick landing page - 'The property site you've been putting off. Live in five minutes.' Website builder for UK property operators" src="https://wasp.sh/img/made-with-wasp-vol2/app-brick.webp"></div><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p><strong>Try it out:</strong> <a href="https://onbrick.co.uk/" target="_blank" rel="noopener noreferrer" class="">onbrick.co.uk</a>  ·  <strong>Pricing:</strong> £45 to £119/mo</p>
<p>Picture this: you're a UK landlord with three properties, and a prospective tenant just asked for your website. You... don't have one. Brick is the fastest way out of that conversation.</p>
<p>You tell it your business name and what kind of property you run (rent-to-rent, serviced accommodation, HMO, deal sourcing, development), and it spins up a full website in about five minutes: <strong>AI-generated copy</strong> tuned to your niche, a <strong>chatbot that captures leads</strong> while you're at a viewing, <strong>GDPR pages, SSL, daily backups</strong>, the lot. Each property niche gets its own template, so the sites don't all look the same.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Brick's visual editor: section forms on the left ('About Section' with badge text, section title, and paragraph fields), live website preview on the right showing a polished property-management hero" src="https://wasp.sh/img/made-with-wasp-vol2/brick-editor.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Brick features a full-fledged no-code website editor! Really cool.</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="chatbuster---the-ai-chat-that-closes-the-sale-live-on-shopify-and-wordpress"><a href="https://chatbuster.com/" target="_blank" rel="noopener noreferrer" class="">ChatBuster</a> - the AI chat that closes the sale, live on Shopify and WordPress<a href="https://wasp.sh/blog/2026/06/17/made-with-wasp-vol-2#chatbuster---the-ai-chat-that-closes-the-sale-live-on-shopify-and-wordpress" class="hash-link" aria-label="Direct link to chatbuster---the-ai-chat-that-closes-the-sale-live-on-shopify-and-wordpress" title="Direct link to chatbuster---the-ai-chat-that-closes-the-sale-live-on-shopify-and-wordpress" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><div style="background:#F5C842;padding:14px;border:3px solid #111;box-sizing:border-box;display:inline-block"><img style="display:block;border:3px solid #111;box-sizing:border-box" alt="ChatBuster landing page - 'The chat that closes the sale.' AI chat trained on your Shopify or WooCommerce store" src="https://wasp.sh/img/made-with-wasp-vol2/app-chatbuster.webp"></div><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p><strong>Try it out:</strong> <a href="https://chatbuster.com/" target="_blank" rel="noopener noreferrer" class="">chatbuster.com</a>  ·  <strong>Pricing:</strong> from $40/mo  ·  <strong>Where:</strong> Shopify App Store + WordPress.org</p>
<p>If you've ever shopped on a Shopify store and pinged the chat widget at midnight to ask <em>"do you ship to Canada?"</em>, ChatBuster is the thing on the other end. Except now it's AI, trained on <strong>the merchant's actual catalog, theme, and policies</strong>, so it answers with the right number, the right policy, and the right SKU.</p>
<p>Under the hood: <strong>live catalog sync</strong> (SKUs, prices, stock update as the merchant updates), <strong>proactive prompts</strong> you can trigger on scroll or schedule, and <strong>order tracking</strong> through the store's API. A merchant installs it from the <strong>Shopify App Store</strong> or <strong>WordPress.org</strong> (the WP plugin went live in May 2026), flips one toggle, and it's working five minutes later. No developer required.</p>
<p>And it's not theoretical. Real merchants are already leaving five-star reviews:</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Two five-star reviews from real Shopify merchants using ChatBuster, both praising how easy it was to set up" src="https://wasp.sh/img/made-with-wasp-vol2/chatbuster-reviews.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Five-star reviews from Shopify merchants.</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="planrelief---ai-lesson-plans-for-daycare-and-early-years-educators"><a href="https://planrelief.io/" target="_blank" rel="noopener noreferrer" class="">PlanRelief</a> - AI lesson plans for daycare and early-years educators<a href="https://wasp.sh/blog/2026/06/17/made-with-wasp-vol-2#planrelief---ai-lesson-plans-for-daycare-and-early-years-educators" class="hash-link" aria-label="Direct link to planrelief---ai-lesson-plans-for-daycare-and-early-years-educators" title="Direct link to planrelief---ai-lesson-plans-for-daycare-and-early-years-educators" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><div style="background:#F5C842;padding:14px;border:3px solid #111;box-sizing:border-box;display:inline-block"><img style="display:block;border:3px solid #111;box-sizing:border-box" alt="PlanRelief landing page - 'Generate lesson plans. Instantly.' AI-powered lesson plans for educators of ages 0 to 12" src="https://wasp.sh/img/made-with-wasp-vol2/app-planrelief.webp"></div><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p><strong>Try it out:</strong> <a href="https://planrelief.io/" target="_blank" rel="noopener noreferrer" class="">planrelief.io</a>  ·  <strong>Pricing:</strong> free to try  ·  <strong>For:</strong> educators of ages 0 to 12</p>
<p>Imagine you're a daycare teacher and you spent the afternoon watching kids in the play corner. You jot down <em>"Mia experimented with mixing colours."</em> PlanRelief takes that one-line observation, plus a theme you pick ("rainbows and seasons"), and generates <strong>a full lesson plan</strong>: learning objectives, materials list, assessment strategies, the activity flow, ready to hand to a supervisor.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="PlanRelief 'Pick a Lesson Plan' menu with two toddler-friendly activities: 'Big Button App Board Play' and 'Sort, Stick, and Swap App Icons,' generated from the topic 'learn building web apps'" src="https://wasp.sh/img/made-with-wasp-vol2/planrelief-lesson-options.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">I typed 'learn building web apps' as the topic. The AI took it seriously and came back with toddler-grade activities involving big buttons and Velcro icon boards. Respect.</figcaption></figure></div>
<p>The pitch line is exactly what you'd want it to be: <em>"paperwork done in minutes, not hours."</em> Free to try, no credit card.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="build-anything-with-wasp-ship-it-and-get-paying-customers">Build anything with Wasp, ship it, and get paying customers<a href="https://wasp.sh/blog/2026/06/17/made-with-wasp-vol-2#build-anything-with-wasp-ship-it-and-get-paying-customers" class="hash-link" aria-label="Direct link to Build anything with Wasp, ship it, and get paying customers" title="Direct link to Build anything with Wasp, ship it, and get paying customers" translate="no">​</a></h2>
<p>Four apps, zero overlap. Creator tools, property tech, e-commerce, classroom. All paid SaaS. All running in production. All built on the same framework you can install with one command.</p>
<p>That's the whole point of Community Day, really: when we say Wasp is a real fullstack framework, we mean <em>real</em>. Apps in real verticals, with real customers paying real money. The lineup looks very different from <a class="" href="https://wasp.sh/blog/2023/06/28/what-can-you-build-with-wasp">Vol. 1</a>, and it'll look very different again the next time we do this.</p>
<p>If you've built something on Wasp that should be in Vol. 3, come say hi on <a href="https://discord.gg/rzdnErX" target="_blank" rel="noopener noreferrer" class="">Discord</a> or <a href="https://twitter.com/WaspLang" target="_blank" rel="noopener noreferrer" class="">X/Twitter</a>. We'd love to feature you next time.</p>
<p>And if you haven't started yet, you're one command away:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> i </span><span class="token parameter variable" style="color:#36acaa">-g</span><span class="token plain"> @wasp.sh/wasp-cli@latest</span><br></div></code></pre></div></div>
<p>See you on Day 4. 🐝</p>]]></content:encoded>
            <author>matija@wasp-lang.dev (Matija Sosic)</author>
            <category>launch-week</category>
            <category>showcase</category>
            <category>community</category>
            <category>wasp</category>
            <category>saas</category>
            <category>web dev</category>
        </item>
        <item>
            <title><![CDATA[Wasp now lets you write your full-stack logic as a spec in TypeScript]]></title>
            <link>https://wasp.sh/blog/2026/06/15/wasp-typescript-spec</link>
            <guid>https://wasp.sh/blog/2026/06/15/wasp-typescript-spec</guid>
            <pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Wasp drops its custom DSL and replaces it with a TypeScript-native way to define your full-stack app: the TypeScript Spec. Here's what it is, how it works, and what it unlocks.]]></description>
            <content:encoded><![CDATA[<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp's new TypeScript Spec: a main.wasp.ts file defining the whole app — title, wasp version, auth, and a spec list with route/page/query calls — all in TypeScript" src="https://wasp.sh/img/wasp-typescript-spec/banner.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p><a class="" href="https://wasp.sh/">Wasp</a> is an opinionated, <strong>batteries-included</strong> full-stack JS/TS web app framework, built around the concept of an explicit spec for defining full-stack features of your app.</p>
<p>Today is a big day: we dropped our custom spec language and replaced it with a new, TypeScript-native way to define your full-stack app at a high level: the <a class="" href="https://wasp.sh/docs/general/spec">TypeScript Spec</a>!</p>
<!-- -->
<p>Read on for the why and the what, or if you want to try it out right now, go to <a class="" href="https://wasp.sh/docs/quick-start">the quick start guide</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="quick-recap-of-the-last-5-years">Quick recap of the last 5 years<a href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec#quick-recap-of-the-last-5-years" class="hash-link" aria-label="Direct link to Quick recap of the last 5 years" title="Direct link to Quick recap of the last 5 years" translate="no">​</a></h2>
<p>Wasp is an opinionated, <strong>batteries-included</strong>, <strong>truly full-stack, spec-driven</strong> JS/TS web app framework, in spirit similar to RoR/Laravel/Django/Meteor, but reimagined from scratch for modern JS.</p>
<p><strong>“Truly full-stack”</strong> because Wasp covers frontend, backend, and the database; Wasp’s goal is to capture the web app development end-to-end, in its entirety, as one cohesive whole.</p>
<p><strong>“Spec-driven”</strong> because of Wasp’s unique, central concept of the <strong>spec</strong> (specification). It's in the name: Web App SPecification == Wasp.</p>
<p>Spec is a dedicated logic layer where you define your app at the full-stack level, that ties all parts of the stack together, and then you implement it all in the underlying technologies you are used to: React, Node, Prisma, … .
It is the place where you are not writing frontend, backend, or database logic, you are writing "full-stack" logic.</p>
<p>So far, the spec layer in Wasp was implemented as a standalone language (DSL), with our own custom compiler and tooling. <a class="" href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake">Turns out that was more trouble than it was worth</a>, and it started blocking us in further growing Wasp into what we want it to become, so by popular demand, we switched to spec in TypeScript!</p>
<p>Read on to learn more about it, how it works and what it unlocks.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-wasp-typescript-spec">The Wasp TypeScript Spec<a href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec#the-wasp-typescript-spec" class="hash-link" aria-label="Direct link to The Wasp TypeScript Spec" title="Direct link to The Wasp TypeScript Spec" translate="no">​</a></h2>
<p>You write the <a class="" href="https://wasp.sh/docs/general/spec">Wasp TypeScript Spec (TS Spec)</a> in <code>*.wasp.ts</code> files by constructing the <code>app</code> spec object.</p>
<p>You do so by using the spec constructors from <code>@wasp.sh/spec</code>: <code>page</code>, <code>route</code>, <code>query</code>, <code>job</code>, <code>api</code>, … . That is your "vocabulary" that you use to specify the parts of your web app, and then compose them all together into one big <code>app</code> spec object.</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> app</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> page</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> query</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> route </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">MainPage</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/MainPage"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// React</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> getTasks </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/tasks"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Node.js</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">app</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  wasp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> version</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"^0.24.0"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ToDo App"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  auth</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    userEntity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"User"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    methods</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> google</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> gitHub</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> email</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    onAuthFailedRedirectTo</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/login"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  spec</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"RootRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">MainPage</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      authRequired</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">query</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">getTasks</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"Task"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>Wasp takes the spec and the rest of your code (React, Node.js, <code>schema.prisma</code>, …), and builds them into a full-stack app.</p>
<p>In the example above, this app will have:</p>
<ul>
<li class="">Full-stack auth working out of the box (supporting Google, GitHub, or email &amp; password auth).</li>
<li class="">The <code>src/MainPage.tsx</code> React component mounted as the home page at <code>/</code>.</li>
<li class="">The <code>getTasks</code> function from <code>src/tasks.ts</code> made callable from the client (e.g. from <code>MainPage.tsx</code>) via RPC, either directly or via TanStack Query.</li>
</ul>
<blockquote>
<p>💡 If you used the old Wasp DSL, you might be thinking at this point "hey this looks the same as before, maybe even a bit more verbose". True, but this is where the similarities stop, and exciting new possibilities start: read on.</p>
</blockquote>
<p>While writing the spec, you are just writing TypeScript.<br>
<!-- -->You can use third-party libraries, you can split the spec into multiple TypeScript files, you can define your own helper functions, use for loops, read from the disk, inspect env vars, you can organize your app vertically by features together with their spec, launch a rocket, … .<br>
<!-- -->The only thing Wasp requires from you is that you export an <code>app</code> spec object from the <code>main.wasp.ts</code> file.</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockTitle_OeMC">main.wasp.ts</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> readFile </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"fs/promises"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> app</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> page</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> route </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> authSpec </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/auth/auth.wasp"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> cardsSpec </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/cards/cards.wasp"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">MainPage</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/cards/MainPage"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">Layout</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/Layout"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">app</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"waspello"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// A Trello clone app made with Wasp!</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  wasp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> version</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"^0.24.0"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">readFile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"appTitle.txt"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"utf-8"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">trim</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Reading from disk.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  auth</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    userEntity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"User"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    methods</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      usernameAndPassword</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      google</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    onAuthFailedRedirectTo</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/login"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  client</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    rootComponent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">Layout</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  spec</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"MainRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">MainPage</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> authRequired</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// Parts of specification extracted into their own files.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    authSpec</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    cardsSpec</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockTitle_OeMC">src/cards/cards.wasp.ts</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// A specification for the "cards" functionality.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> action</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> query</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">Spec</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> withFields </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"../utils.wasp"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Custom helper function.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> createCard</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> updateCard </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./cards"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports">  createList</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports">  createListCopy</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports">  deleteList</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports">  getListsAndCards</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports">  updateList</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports"></span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./lists"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> cardsSpec</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">Spec</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token spread operator" style="color:#393A34">...</span><span class="token method function property-access" style="color:#d73a49">withFields</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"List"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">action</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">createList</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">action</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">updateList</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token spread operator" style="color:#393A34">...</span><span class="token method function property-access" style="color:#d73a49">withFields</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"Card"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">action</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">createCard</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">action</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">updateCard</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token spread operator" style="color:#393A34">...</span><span class="token method function property-access" style="color:#d73a49">withFields</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"List"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Card"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">query</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">getListsAndCards</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">action</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">deleteList</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"Log"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">action</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">createListCopy</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>Those <code>with { type: ref }</code> imports you might have noticed in the code examples above?<br>
<!-- -->That is how you connect the spec with the implementation (currently React, Node.js, Prisma), how you connect the full-stack "story" with the specific parts of the stack.<br>
<!-- -->A bit of bundler magic happens there: <code>with { type: ref }</code> imports don't actually get evaluated, so no importing really happens, but instead they get transformed into objects that "reference" the import for later evaluation, that you then pass to the spec. You can even construct these directly as objects, inline, if you wish (useful if you are creating them dynamically): <code>ref({ import: "getTasks", from: "./src/queries" })</code>.</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Ref import</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> getTasks </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/tasks"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ref"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// gets transformed by bundler into</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> getTasks </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ref</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">import</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"getTasks"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./src/tasks"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// where `ref` comes from</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> ref </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@wasp.sh/spec"</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>That's it really! Wasp gives you the building blocks, and you do what you want with them, with the full power of TypeScript behind you.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="do-not-try-this-at-home-implementing-file-based-routing-in-ts-spec">Do <del>not</del> try this at home: implementing file-based routing in TS Spec<a href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec#do-not-try-this-at-home-implementing-file-based-routing-in-ts-spec" class="hash-link" aria-label="Direct link to do-not-try-this-at-home-implementing-file-based-routing-in-ts-spec" title="Direct link to do-not-try-this-at-home-implementing-file-based-routing-in-ts-spec" translate="no">​</a></h2>
<p>An example of what is uniquely doable with Wasp's new TypeScript Spec (and wasn't possible with the old DSL): implementing file-based routing.</p>
<p>By default, Wasp doesn't come with file-based routing: instead, you define your routes explicitly in the spec:</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"MainRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">Main</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> authRequired</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"SignupRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/signup"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">SignupPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"LoginRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/login"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">LoginPage</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><br></div></code></pre></div></div>
<p>But, let's say you want file-based routing nevertheless. With the new TypeScript spec you can do that in Wasp!</p>
<p>You could create, let's say, a <code>fileRouter.wasp.ts</code> that exports a helper function that reads the contents of the <code>src/</code> dir and based on dirs and files produces an array of <code>route</code> and <code>page</code> spec objects.</p>
<p>For example, a file at path <code>src/pages/(auth)/report/[id].tsx</code> could result in</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ReportRoute"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/report/:id"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">ReportPage</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> authRequired</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div>
<p>Import that function in your <code>main.wasp.ts</code> and add its output to the <code>app.spec</code> array, and that is it, you implemented your own custom file-based routing! What does this make Wasp: a meta-meta-framework?</p>
<p>If you are interested in the exact implementation, stay tuned: we will be publishing it as a separate post soon. If you implement it on your own in the meantime though, please share it with us, we would love to see how you did it!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-specification-at-all">Why spec(ification) at all?<a href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec#why-specification-at-all" class="hash-link" aria-label="Direct link to Why spec(ification) at all?" title="Direct link to Why spec(ification) at all?" translate="no">​</a></h2>
<p>We mentioned the "spec" so many times in this article, but why should you care about it?</p>
<p>Why do we at Wasp care about it so much that we have spent years making and remaking the whole web framework around this idea, when every other "normal" JS web framework out there is focusing on data fetching, hydration strategies, tight integration with the UI library, … ?</p>
<p>All web frameworks, by definition, capture the domain/essence of what a web app is, give it structure, introduce their concepts, and then let you fill in the gaps, effectively separating the <strong>specification</strong> (what you want) from the <strong>implementation</strong> (how you want it).
This lets you focus on the details of your business logic, what your app is really about (essential complexity), and implement it exactly how you want it, while the framework handles the rest of the details for you (accidental complexity) as you specified it.</p>
<p>What we are doing with the spec(ification) at Wasp though, is making that specification a <strong>first-class citizen</strong> in your app.</p>
<p>It's not convention based (e.g. magic names, dir structure, …), it's not implicit, it's not tied up in the runtime of the underlying stack.
Instead, it is <strong>explicit, malleable, programmable, standalone, stack-agnostic, extendable</strong>. It has its own Turing-complete logic layer (thank you TypeScript), and just as you could write frontend or backend logic so far, now you can also write "app"/"full-stack" logic next to it. That logic has its own standalone runtime and can affect how the rest of the code is used and generated.</p>
<p>This means you can organize your spec as you want, you can reuse it, <strong>share/distribute</strong> it.</p>
<p>Right now the Wasp spec comes with a predefined "vocabulary": <code>route</code>, <code>query</code>, <code>job</code>, … , but we are excited about the idea of <strong>"extensible spec"</strong>: being able to add your own "words" to it.</p>
<p>Or, imagine being able to take the whole <strong>vertical slice of your app, truly full-stack</strong> (fe, be, db, env, even infra), and making it a reusable, distributable unit with a defined interface (in spec) that can then be used and configured from anybody's spec. We are working on this currently under the working name of <a href="https://github.com/wasp-lang/wasp/issues/2873" target="_blank" rel="noopener noreferrer" class="">Full Stack Modules</a> (FSMs).</p>
<p>Additionally, although now implemented in TypeScript, spec is standalone and therefore also <strong>stack and language agnostic</strong>. We could support writing (additional) backend logic in Python, Go or Rust in the future, you would just specify them in the spec (e.g. as queries and actions). We could add other frontend libraries next to React and spec wouldn’t change.</p>
<p><strong>To summarize</strong>: we are doing the "spec" because we are excited about what gets unlocked when you make full-stack logic a "first-class" citizen in a batteries-included web framework.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="spec-and-ai">Spec and AI<a href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec#spec-and-ai" class="hash-link" aria-label="Direct link to Spec and AI" title="Direct link to Spec and AI" translate="no">​</a></h2>
<p>Per design, as a full-stack, batteries-included framework, Wasp is in a position to work great with AI, due to the same reasons as Ruby on Rails, Laravel and others: less code to write, stronger constraints, clear best practices, one cohesive codebase, ... .</p>
<p>Wasp's spec, however, brings additional promise of serving as a central place where one can easily get a high-level overview of the app and the intention behind it, before getting lost in the implementation. Not just for AI, but also for you, especially if leaning harder into the "vibes".</p>
<p>Spec-driven approach to AI engineering is a popular method these days, with tools like <a href="https://github.com/Fission-AI/OpenSpec/" target="_blank" rel="noopener noreferrer" class="">OpenSpec</a> or <a href="https://github.com/github/spec-kit" target="_blank" rel="noopener noreferrer" class="">GitHub Spec Kit</a>, where you write down the specification in Markdown files.<br>
<!-- -->Wasp's spec takes this a level further: your spec is an actual, compiled part of your app, not Markdown files that drift with time. It is though much more constrained in what it can express at the moment, but we are excited to see how it evolves (e.g. once we make it extensible).</p>
<p>Finally, there is the possibility of <a href="https://github.com/wasp-lang/wasp/issues/2873" target="_blank" rel="noopener noreferrer" class="">Full Stack Modules</a>, that Wasp spec unlocks: imagine being able to have truly full-stack pieces that you can test in isolation, reuse / distribute, and know they work? This means you can give AI bigger building blocks than normal, and ensure some things just can't get messed up: if I am using a Stripe payments full-stack module, AI can't just mess it up by accident.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="spec-and-you">Spec and you<a href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec#spec-and-you" class="hash-link" aria-label="Direct link to Spec and you" title="Direct link to Spec and you" translate="no">​</a></h2>
<p>If you find all this interesting, we would love to hear from you (you can find us in our <a href="https://discord.com/invite/rzdnErX" target="_blank" rel="noopener noreferrer" class="">Discord server</a>). This is the first iteration of the new TypeScript spec and any feedback helps us improve it: what you liked, what you didn't, what you would like to see in the future, anything goes.</p>
<p>You can try Wasp quickly by running <code>npm i -g @wasp.sh/wasp-cli@latest</code>, then <code>wasp new</code>, and then just follow the instructions, or go to our <a class="" href="https://wasp.sh/docs">docs</a> for detailed instructions and a tutorial.</p>
<p>If you are wondering why some part of Wasp is the way it is or how we plan to evolve it, you might find some answers quickly in our <a href="https://github.com/wasp-lang/wasp/issues" target="_blank" rel="noopener noreferrer" class="">GitHub issues</a>: we capture all our future ideas and plans there in the open (all ready for Cloudflare to slop-fork us, and then some). For a more high-level overview, there is <a href="https://github.com/orgs/wasp-lang/projects/5" target="_blank" rel="noopener noreferrer" class="">Wasp Roadmap</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-next">What's next?<a href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec#whats-next" class="hash-link" aria-label="Direct link to What's next?" title="Direct link to What's next?" translate="no">​</a></h2>
<p>Wasp works as it is: we know of people selling startups made with Wasp, building internal apps in Fortune 500 companies, using it in banks. But we are still keeping Wasp in Beta till we fully flesh out the spec side of the story and also bring all the parts of it to the level we are satisfied with.</p>
<p>There are a couple of main categories of improvements we are chasing at the moment:</p>
<ol>
<li class=""><strong>Spec</strong>: Further improving TS Spec design (e.g. bring data models into it), Full Stack Modules, spec extensibility, …</li>
<li class=""><strong>Wasp 1.0</strong>: Refurbishing core parts of Wasp (Auth, Operations, Jobs, Db, Routing/rendering, …) to bring them up to speed with the rest of Wasp, tightening our deployment/production features, …</li>
<li class=""><strong>Width</strong>: Explore adding support for other ORMs (e.g. Drizzle), other frontend libraries (e.g. Svelte, Vue), better support for mobile, maybe even add some infra as code and also support for other backend languages, …</li>
</ol>
<p>As mentioned above, you can find much more detail on all of these in our <a href="https://github.com/wasp-lang/wasp/issues" target="_blank" rel="noopener noreferrer" class="">GitHub issues</a> and on the <a href="https://github.com/orgs/wasp-lang/projects/5" target="_blank" rel="noopener noreferrer" class="">Wasp Roadmap</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="credits">Credits<a href="https://wasp.sh/blog/2026/06/15/wasp-typescript-spec#credits" class="hash-link" aria-label="Direct link to Credits" title="Direct link to Credits" translate="no">​</a></h2>
<p>Big thanks to everybody who put in the time and effort to help us deliver the TS Spec and supported us on the way! Special thanks to <a href="https://github.com/Reikon95" target="_blank" rel="noopener noreferrer" class="">@Reikon95</a>, <a href="https://github.com/Genyus" target="_blank" rel="noopener noreferrer" class="">@Genyus</a>, <a href="https://github.com/devagrawal09" target="_blank" rel="noopener noreferrer" class="">@devagrawal09</a>, <a href="https://github.com/AlemTuzlak" target="_blank" rel="noopener noreferrer" class="">@AlemTuzlak</a>, and the whole Wasp community, especially the amazing people on our Discord server.</p>]]></content:encoded>
            <author>martin@wasp-lang.dev (Martin Sosic)</author>
            <category>wasp</category>
            <category>typescript</category>
            <category>fullstack</category>
            <category>update</category>
        </item>
        <item>
            <title><![CDATA[Wasp Launch Week #12 - TS Spec aka MeTSamorphosis 🐛🦋]]></title>
            <link>https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec</link>
            <guid>https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec</guid>
            <pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Launch Week #12 is Wasp's biggest shift yet: the DSL is gone, and Wasp is now a fully TypeScript-native full-stack framework. Plus SSR/pre-rendering, a rebrand, Community Day, and easier deploys.]]></description>
            <content:encoded><![CDATA[<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Launch Week 12 - MeTSamorphosis" src="https://wasp.sh/img/lw12/lw12-banner.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>A few weeks ago, we published a post with a title that raised a few eyebrows: <a class="" href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake"><em>"5 years and $5M later: inventing our own programming language was a mistake."</em></a> Five years. One whole DSL. And we said it out loud, in public, on the internet, where it stays forever.</p>
<p>A lot of you wrote in afterwards (<a href="https://www.reddit.com/r/programming/comments/1tc02h0/5_years_and_5m_later_inventing_a_new_programming/" target="_blank" rel="noopener noreferrer" class="">179, to be precise</a>). Some to commiserate, some to cheer, a couple to ask if we were okay (we are, thank you). But the most common question by far was the obvious one:</p>
<p><strong>"Okay… so what are you going to do about it?"</strong></p>
<p>This is the moment where we answer that.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="caterpillars-dont-apologize-for-being-caterpillars-">Caterpillars don't apologize for being caterpillars 🐛<a href="https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec#caterpillars-dont-apologize-for-being-caterpillars-" class="hash-link" aria-label="Direct link to Caterpillars don't apologize for being caterpillars 🐛" title="Direct link to Caterpillars don't apologize for being caterpillars 🐛" translate="no">​</a></h2>
<p>They just turn into something cooler, with wings (I know this doesn't work 100% for wasps, but <em>licentia poetica</em>, right?).</p>
<p>Welcome to <strong>Launch Week #12</strong>, a.k.a. <strong>MeTSamorphosis</strong>: the week Wasp finally sheds the DSL and emerges in its <strong>true, fully TypeScript-native</strong> form. No more learning a config language to use a framework. No more "wait, why is this a <code>.wasp</code> file?" Just TypeScript, all the way down, the same language you're already writing the rest of your app in.</p>
<p><strong>This is the biggest shift in Wasp's history, so TS Spec is unapologetically the star of this launch</strong>. But it's far from the only thing we've got for you. There's pre-rendering/SSR support, a brand-new look for Wasp, and a few more surprises landing across the week. TS Spec leads the way, but none of the rest is filler.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="but-first---thank-you">But first - Thank you!<a href="https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec#but-first---thank-you" class="hash-link" aria-label="Direct link to But first - Thank you!" title="Direct link to But first - Thank you!" translate="no">​</a></h3>
<p>Before we dive in, none of what's coming this week happens without you. The community around Wasp has never felt more alive, more people are shipping real apps with it than ever, and we don't take that for granted. So, genuinely: thank you. 🙏</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-cocoon-cracks-open-on-monday-june-15-️">The cocoon cracks open on Monday, June 15 🗓️<a href="https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec#the-cocoon-cracks-open-on-monday-june-15-%EF%B8%8F" class="hash-link" aria-label="Direct link to The cocoon cracks open on Monday, June 15 🗓️" title="Direct link to The cocoon cracks open on Monday, June 15 🗓️" translate="no">​</a></h2>
<p>There's no launch party without a nice, cozy community call to kick it all off! We're assembling on <strong>Monday, June 15th, 8 AM PT / 11 AM ET / 5 PM CET</strong>. Martin and I will be there live to walk you through TS Spec, take your questions, and probably get a little emotional about it.</p>
<p>Make sure to <a href="https://discord.gg/ZAV4j6Dp?event=1512472513609859072" target="_blank" rel="noopener noreferrer" class="">register for the event on our Discord</a> and smash that "Interested" button so you don't miss it.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Register for the kickoff call" src="https://wasp.sh/img/lw12/lw12-register.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">See you there, fellow metamorphosee. (Not a word. It is now.)</figcaption></figure></div>
<p>Here's some of what's hatching this week. A few of these are locked and loaded. A couple are still in the cocoon and might land later, or show up as a surprise. 🤫</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-the-big-one---adiós-dsl-ts-spec-is-here">🦋 The Big One - Adiós DSL, TS Spec is here<a href="https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec#-the-big-one---adi%C3%B3s-dsl-ts-spec-is-here" class="hash-link" aria-label="Direct link to 🦋 The Big One - Adiós DSL, TS Spec is here" title="Direct link to 🦋 The Big One - Adiós DSL, TS Spec is here" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="main.wasp.ts in the new TS Spec — your whole app config defined in TypeScript: imports, app({...}), auth, and a spec list with route/page/query calls" src="https://wasp.sh/img/lw12/ts-spec-example.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">A taste of the new TS Spec: imports up top, app shell in the middle, your routes, pages and queries below - all in one (or more) type-checked TypeScript files.</figcaption></figure></div>
<p>This is the one. After five years, Wasp is now a <strong>TypeScript framework, end to end</strong>. You define your app (pages, routes, auth, jobs, the works) in TypeScript, with full type-safety and editor support, instead of a separate DSL you had to learn first.</p>
<p>We'll publish the full announcement to kick things off, walking through what changed, why it's better, and how to migrate. If you've ever wanted Wasp's superpowers without the "wait, what's this file" tax, this is the one for you.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="GitHub issue #3778 'Moving from the DSL to TS spec' — landing very soon (June 15!)" src="https://wasp.sh/img/lw12/dsl-ts-issue.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Spoon, my friend. Very spoon.</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-new-wings---ssr--pre-rendering">⚡ New wings - SSR &amp; pre-rendering<a href="https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec#-new-wings---ssr--pre-rendering" class="hash-link" aria-label="Direct link to ⚡ New wings - SSR &amp; pre-rendering" title="Direct link to ⚡ New wings - SSR &amp; pre-rendering" translate="no">​</a></h2>
<p>A fresh form needs a fresh set of wings. We're also showing off <strong>server-side rendering / pre-rendering in Wasp</strong>: better SEO, faster first paint, and all the things you've been (politely, mostly) asking us for.</p>
<p>It's already live, so you don't even have to wait for the launch — check out <a class="" href="https://wasp.sh/docs/advanced/prerendering">the prerendering docs</a> and try it on your app today.</p>
<p>Vinny also put together a video walking through it end to end. Keep an eye on YouTube for <em>"Wasp now supports prerendering."</em> 📺</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Lighthouse scores for a prerendered Wasp app: 98 Performance, 92 Accessibility, 96 Best Practices, 92 SEO" src="https://wasp.sh/img/lw12/wasp-prerendering.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Lighthouse scores for an Open SaaS landing page with prerendering turned on. Not bad. 💪</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-the-swarm---community-day">🐝 The swarm - Community Day<a href="https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec#-the-swarm---community-day" class="hash-link" aria-label="Direct link to 🐝 The swarm - Community Day" title="Direct link to 🐝 The swarm - Community Day" translate="no">​</a></h2>
<p>A Wasp tradition we love: the spotlight goes to <strong>you</strong>, the builders.</p>
<p>We're dropping <strong>"Made with Wasp, Vol. 2"</strong>, a fresh roundup of genuinely cool apps people shipped on Wasp, plus videos and write-ups from community members on what they built and how Wasp helped. Real users showing real things hits different than us claiming Wasp is good, so we'll let them do the talking.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Tweet exchange: Catalin asks for the simplest, most effective full-stack tech stack; Kyle replies '@WaspLang by @MatijaSosic hands down!'" src="https://wasp.sh/img/lw12/wasp-testimonial-kyle.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Kyle gets it. 🫡</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-new-look-who-dis---the-rebrand">🎨 New look, who dis - the Rebrand<a href="https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec#-new-look-who-dis---the-rebrand" class="hash-link" aria-label="Direct link to 🎨 New look, who dis - the Rebrand" title="Direct link to 🎨 New look, who dis - the Rebrand" translate="no">​</a></h2>
<p>A metamorphosis you can <em>see</em>. Wasp is getting a brand-new look (new visual identity, new vibe, the whole thing), and Boi's TS suit is just the start of it.</p>
<p>We'll also show you the new brand and, more interestingly, the story behind it: we'll talk through why we did it, how we got there, and what we were going for. (Spoiler: we have… opinions about monospace fonts.)</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp's new landing page: 'A truly full-stack TypeScript framework that just works' with a code sample on the right" src="https://wasp.sh/img/lw12/wasp-new-lp-screenshot.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Look at me. I am the designer now.</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-out-into-the-world---easier-deploys">🚀 Out into the world - easier deploys<a href="https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec#-out-into-the-world---easier-deploys" class="hash-link" aria-label="Direct link to 🚀 Out into the world - easier deploys" title="Direct link to 🚀 Out into the world - easier deploys" translate="no">​</a></h2>
<p>A framework is only as good as the places it can fly to, so we've been working on making Wasp even easier to deploy to more of your favorite platforms. We're not spilling all the details yet, but if there's a host you've been wishing was one click away, keep an eye out. 👀</p>
<p>And because we can't help ourselves, there might be <em>one more thing</em> tucked away for you, too. You'll have to show up to find out.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="see-you-on-the-other-side-of-the-cocoon-">See you on the other side of the cocoon 🐝<a href="https://wasp.sh/blog/2026/06/05/wasp-launch-week-12-ts-spec#see-you-on-the-other-side-of-the-cocoon-" class="hash-link" aria-label="Direct link to See you on the other side of the cocoon 🐝" title="Direct link to See you on the other side of the cocoon 🐝" translate="no">​</a></h2>
<p>That's the week: the molt, the wings, the swarm, the new look, and the take-off. We've been working toward this one for a long time, and we honestly can't wait to show you.</p>
<p>As always, we want <strong>all</strong> of your feedback: the good, the bad, and the "did Boi really need a suit." So come find us on <a href="https://discord.gg/rzdnErX" target="_blank" rel="noopener noreferrer" class="">Discord</a>, <a href="https://twitter.com/WaspLang" target="_blank" rel="noopener noreferrer" class="">Twitter/X</a>, or <a href="https://github.com/wasp-lang/wasp" target="_blank" rel="noopener noreferrer" class="">GitHub</a>. We're there.</p>
<p>To stay in the loop, <a href="https://twitter.com/WaspLang" target="_blank" rel="noopener noreferrer" class="">follow us on X</a> and <a href="https://discord.gg/rzdnErX" target="_blank" rel="noopener noreferrer" class="">join the Discord</a>. See you Monday. 🐝🐝</p>]]></content:encoded>
            <author>matija@wasp-lang.dev (Matija Sosic)</author>
            <category>launch-week</category>
            <category>update</category>
        </item>
        <item>
            <title><![CDATA[How To Build A SaaS with AI. Without Getting Stuck.]]></title>
            <link>https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck</link>
            <guid>https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck</guid>
            <pubDate>Wed, 03 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Vibe coding a SaaS can get you started fast but can also turn into a mess. The way out isn't a rewrite, it's a process. Here are 5 tips that keep you the architect while AI does the building.]]></description>
            <content:encoded><![CDATA[<p>Someone on <a href="https://www.reddit.com/r/vibecoding/comments/1su03dk/vibe_coded_for_6_months_my_codebase_is_a_disaster/" target="_blank" rel="noopener noreferrer" class="">r/vibecoding</a> posted something last week that I've been thinking a lot about. His post is titled: <em>"vibe coded for 6 months. my codebase is a disaster."</em></p>
<p><img decoding="async" loading="lazy" alt="r/vibecoding post titled &amp;quot;vibe coded for 6 months. my codebase is a disaster.&amp;quot;" src="https://wasp.sh/assets/images/reddit-vibe-coded-disaster-8b31aaf8b7e51cc1fe14fb4ed54a5fd8.webp" width="1614" height="812" class="img_ev3q"></p>
<p>Interestingly, he was able to build a functioning SaaS with paying customers, but he encountered endless problems whenever he tried to modify the app in any way.</p>
<!-- -->
<p>So he tried to onboard a developer to help him. The dev opened the repo, went quiet for two minutes, and said: <em>"what is this."</em></p>
<p>Six months of prompting led to a Frankenstein's monster of a codebase that is such a tangled mess, he's probably better off starting over from scratch. In his words, <em>"nobody was thinking about structure. the AI just kept adding. new file here, duplicate function there, 3 different ways to handle the same thing across the codebase."</em></p>
<p>As the creator and maintainer of <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a>, I get a lot of people asking for help when building a SaaS with AI. And they get stuck for the same handful of reasons. Usually one of two ways: the first 70% comes together almost too easily and the last 30% becomes next to impossible, or you ship something that works, but you lack any understanding of what you built (like the redditor above), and therefore don't understand what you need to do to modify, extend, or improve your app.</p>
<p>So, with that in mind, let's look at some of my favorite ways you can avoid these problems, build something you understand, and stay in control.</p>
<blockquote>
<p>📺 <strong>Prefer to watch?</strong> I walk through all five of these workflow tips with live demos in <a href="https://youtu.be/0DpqHItVkYs" target="_blank" rel="noopener noreferrer" class="">this video</a>.</p>
<p><a href="https://youtu.be/0DpqHItVkYs" target="_blank" rel="noopener noreferrer" class=""><img decoding="async" loading="lazy" alt="How To Build A SaaS with AI. Without Getting Stuck. — watch on YouTube" src="https://wasp.sh/assets/images/banner-708e48e00096b8276274c8cd26ba20b9.webp" width="1376" height="768" class="img_ev3q"></a></p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tldr">TL;DR<a href="https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck#tldr" class="hash-link" aria-label="Direct link to TL;DR" title="Direct link to TL;DR" translate="no">​</a></h2>
<p>The way out of a tangled, vibe-coded SaaS isn't to throw it all away and start over, rather it's a process that allows you to <strong>stay in control.</strong> AI is a great builder but you need to stay in charge of the decision making and understand the implementation process to avoid bigger problems down the line. Here are five tips that help you stay the architect:</p>
<ol>
<li class=""><strong>Give your agent rails to run on</strong> — start on a solid SaaS boilerplate (like <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a>) and feed it official, AI-friendly docs (<code>llms.txt</code>) via memory and skill files.</li>
<li class=""><strong>Converse, don't command</strong> — don't open with "build me X." Have the agent interview you (clarify-first) and brainstorm options with pros and cons before it writes any code.</li>
<li class=""><strong>Understand the systems you're building on</strong> — you don't need to read every line of code, but you do need to know what a database, a payments provider, etc. actually do, so you can debug and ask the right questions.</li>
<li class=""><strong>Build in vertical slices</strong> — ship thin, working slices that touch the whole stack (UI → database), one small testable milestone at a time, instead of half-finished layers.</li>
<li class=""><strong>Verify each slice before moving on</strong> — test every milestone as you build so small problems surface early, not six months from now.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-good-news-and-the-bad-news">The good news and the bad news<a href="https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck#the-good-news-and-the-bad-news" class="hash-link" aria-label="Direct link to The good news and the bad news" title="Direct link to The good news and the bad news" translate="no">​</a></h2>
<p>I won't oversell it. These five tips don't magically make your agent perform better for you without you having to put in some work.</p>
<p>The bad news is: it's still hard work.</p>
<p>The good news is: it's a lot easier than it used to be (and you don't need to know how to code to launch a SaaS this way).</p>
<p>If you're willing to understand some core concepts, and be patient and persistent, you'll definitely be able to build your idea, overcome issues, and get it launched.</p>
<p>So let's get into it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-root-cause-you-let-ai-be-the-architect">The root cause: you let AI be the architect<a href="https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck#the-root-cause-you-let-ai-be-the-architect" class="hash-link" aria-label="Direct link to The root cause: you let AI be the architect" title="Direct link to The root cause: you let AI be the architect" translate="no">​</a></h2>
<p>Here's the one mistake underneath all the others. <em>"Nobody was thinking about structure. The AI just kept adding."</em></p>
<p>AI can be an incredible <strong>builder</strong> and a terrible <strong>architect</strong> at the same time. It optimizes for "make this work right now," not "keep this coherent over months." And it's <em>so</em> good at the building part that it's easy to get lazy and quietly hand over the architect role, or in some cases not think deeply about the architecture planning to begin with. Don't do this.</p>
<p>Your job is to <strong>be the architect and stay the architect.</strong> To do this, you need to work with your agent to plan every step of the process and implement things in a methodical manner. Everything below is a concrete way to make that happen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-give-your-agent-rails-to-run-on">1. Give your agent rails to run on<a href="https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck#1-give-your-agent-rails-to-run-on" class="hash-link" aria-label="Direct link to 1. Give your agent rails to run on" title="Direct link to 1. Give your agent rails to run on" translate="no">​</a></h2>
<p>When you're building software, there are a million ways to achieve the same thing. Rather than letting your agent guess at every turn, give it a predetermined path to follow.</p>
<p><a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class=""><img decoding="async" loading="lazy" alt="The Open SaaS landing page — a free, open-source React + NodeJS + Prisma SaaS starter" src="https://wasp.sh/assets/images/open-saas-20d04fc92f743cac6f9c505303f31de4.webp" width="2880" height="1620" class="img_ev3q"></a></p>
<p>The simplest way is to start on a <strong>solid boilerplate</strong>. A SaaS starter has the generic pieces (auth, payments, emails, jobs) already decided and wired up, so you just add your unique feature set. It also gives the agent <strong>patterns to imitate</strong>: it can look at how payments were wired up and slot your new feature in the same way, instead of inventing a third way to do it.</p>
<p>There are great options out there. ShipFast and SuperStarter are popular paid ones (around $300 each). <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a> is free and open source, has had people contributing to and hardening it for years, and is the most popular SaaS starter on GitHub with 14,500+ stars. Whichever you pick, you can't really go wrong as they all give your agent good code rails to work within.</p>
<p><img decoding="async" loading="lazy" alt="Stripe&amp;#39;s llms.txt page at docs.stripe.com/llms.txt — a raw, AI-friendly version of the docs" src="https://wasp.sh/assets/images/stripe-llms-txt-fd6a020bc4f287592bf50a1ad201657c.webp" width="1586" height="1042" class="img_ev3q"></p>
<p>The second set of rails is <strong>knowledge rails</strong>: always feed the agent the official, up-to-date docs of the tools you're using. Human docs are full of HTML and JavaScript fluff that eats your agent's context and buries the part that matters. That's why a lot of tools now publish an <code>llms.txt</code> which is a raw, AI-friendly version of their docs.</p>
<p>You don't want to paste those URLs in by hand every time, so put them where the agent will actually see them: <strong>memory files</strong> (<code>AGENTS.md</code> / <code>CLAUDE.md</code>) and <strong>skill files</strong>. Open SaaS ships with a short memory file that tells the agent where to find its <code>llms.txt</code> docs. For everything else, I keep a <code>FetchDocs</code> skill that holds the correct <code>llms.txt</code> URL for each tool, so the agent fetches the exact up-to-date guide it needs for the task at hand.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-converse-dont-command">2. Converse, don't command<a href="https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck#2-converse-dont-command" class="hash-link" aria-label="Direct link to 2. Converse, don't command" title="Direct link to 2. Converse, don't command" translate="no">​</a></h2>
<p>The biggest prompting mistake people make is starting a fresh session with <em>"build me X."</em> You're the architect, but you might not yet know exactly what you want or the best way to build it. The good news is that the agent probably knows strategies you've never touched.</p>
<p>So have a conversation. Think of it as talking to a really smart, really patient friend who knows everything about software engineering and product development.</p>
<p>Here are two prompt types that I use all the time and that do most of the heavy lifting for me:</p>
<ul>
<li class=""><strong>Clarify-first:</strong> tell it your idea and have it <em>interview you</em> until you both reach a complete, shared understanding. ("I'd like to add a chatbot feature — ask me any questions you have so we reach a mutual understanding before moving forward.") It'll ask who it's for, which LLM provider, floating widget or dedicated page, whether to persist conversations — and you answer until the fog clears.</li>
<li class=""><strong>Brainstorm with pros and cons:</strong> when you don't know your options, ask for several variations <em>with their tradeoffs</em>. ("Give me three implementation strategies with pros and cons, and we'll decide before building.") Three to five is usually the sweet spot, and the agent will even flag its own recommendation so you can weigh it against your constraints (like a budget you don't want to blow).</li>
</ul>
<p>The point is to stop, take a step back, and not command right away. By commanding, you create an app that's a complete, tangled mess like the one from the reddit post above, because the agent will perform whatever request you give it, and that request might be a bad one.</p>
<p>By having a conversation with your agent, it will help you decide what the best course of action is. And when you decide, you have a better understanding of what's going on, and where things go wrong when they do.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-understand-the-systems-youre-building-on">3. Understand the systems you're building on<a href="https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck#3-understand-the-systems-youre-building-on" class="hash-link" aria-label="Direct link to 3. Understand the systems you're building on" title="Direct link to 3. Understand the systems you're building on" translate="no">​</a></h2>
<p>A SaaS is just a bunch of existing tools pieced together in a creative way to solve a problem. You don't need to read every line of code, but you do need to understand the things being pieced together, like what a database is, how your payments provider is pieced together, and how the other pieces generally work.</p>
<p>That understanding is what lets you debug and ask the right questions when something breaks, instead of flailing in the wrong direction. A great move you can use along with your SaaS boilerplate starter is to ask your agent e.g.: <code>fetch the llms.txt docs from Stripe and explain at a high level how payments work in my current codebase</code>. Using your SaaS starter code to explain the concepts is the best way to learn. You can even start at a very high level (e.g. "explain like I'm five") before going deeper.</p>
<p><a href="https://openvibe.sh/" target="_blank" rel="noopener noreferrer" class=""><img decoding="async" loading="lazy" alt="Open Vibe" src="https://wasp.sh/assets/images/banner-e5fcf82c724301f4fe50e6680a76c760.webp" width="2880" height="1620" class="img_ev3q"></a></p>
<p>If that whole learning curve feels overwhelming and you want your hand held through it, I'm building <a href="https://openvibe.sh/" target="_blank" rel="noopener noreferrer" class="">Open Vibe</a>, a free, open-source curriculum that lives inside your agent, set up on top of Open SaaS, and tutors you on the fundamentals while you build.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-build-in-vertical-slices">4. Build in vertical slices<a href="https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck#4-build-in-vertical-slices" class="hash-link" aria-label="Direct link to 4. Build in vertical slices" title="Direct link to 4. Build in vertical slices" translate="no">​</a></h2>
<p>This is probably my favorite approach, and is the direct antidote to <em>"the AI just kept adding."</em>
Instead of building half-finished layers, build thin, <strong>working</strong> slices that touch the whole stack, from the UI in the browser down to the database, so the feature actually does something at every step.</p>
<p><img decoding="async" loading="lazy" alt="A vertical slice cutting through every layer of the stack — UI, Application, Domain, and Database" src="https://wasp.sh/assets/images/what-is-vertical-slice-f72b353f785893d73d5842970ef25ad3.webp" width="760" height="800" class="img_ev3q"></p>
<p>Say I want to make a generated schedule shareable in the planner app I'm building. I wouldn't say <em>"make it shareable and build it."</em> Instead, I'd ask the agent to design a feature plan with <strong>small milestones, one goal each, using a vertical slice approach</strong>.</p>
<p>As a result, we get a plan that we can get the agent to implement in small steps, and test that each small addition works before moving on, rather than implementing all the parts of a new feature in one shot and hoping that it works (or that we can debug it easily).</p>
<p>Such a plan for our sharing feature might look something like this:</p>
<ol>
<li class=""><strong>Read a schedule by its ID at a public URL.</strong> Go to <code>/schedule/&lt;id&gt;</code>, see the schedule. Testable on its own.</li>
<li class=""><strong>Add a share button</strong> plus the small bit of data handling behind it.</li>
<li class=""><strong>Add a share token</strong> and the rest of the hardening.</li>
<li class="">...</li>
</ol>
<p>Each milestone is testable, vertically sliced, and has exactly one goal. Again, the complexity builds up in steps you can verify instead of one big leap.</p>
<p>Once the agent designs a plan you're happy with, <strong>save the plan to a file</strong> (<code>schedule-sharing-implementation-plan.md</code>) so you can reference it later in the chat, e.g.: <em>"reference <code>@schedule-sharing-implementation-plan.md</code> and build milestone two."</em></p>
<p><img decoding="async" loading="lazy" alt="An AI agent&amp;#39;s context window filling up over a session, degrading performance as it fills" src="https://wasp.sh/assets/images/context-38439ed6997cfaf810f22d1a0d6e7b34.webp" width="960" height="829" class="img_ev3q"></p>
<p>That file is also a great <strong>context-management</strong> trick. Remember, agents don't have memory, only the context of the current session, and performance degrades as that context fills up. So I treat each milestone as a fresh session and <code>/clear</code> or <code>/compact</code> the conversation before moving on to the next milestone.</p>
<p>After clearing or starting a new session, I point the agent back at the implementation plan, and move on to the next milestone with a clean slate.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-verify-each-slice-before-moving-on">5. Verify each slice before moving on<a href="https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck#5-verify-each-slice-before-moving-on" class="hash-link" aria-label="Direct link to 5. Verify each slice before moving on" title="Direct link to 5. Verify each slice before moving on" translate="no">​</a></h2>
<p>Vertical slices are only worth it if you actually test each one. It's better to surface small issues now, instead of six months from now like our poor protagonist in this story.</p>
<p>You can do it the "manual way", which is when something fails, copy the error, paste it into your agent with a bit of context, and let it fix it. Agents are surprisingly good at this. But hopping between terminal windows gets tedious, and some bugs are stubborn.</p>
<p>So hand the loop to the agent. One great tool is Claude Code's <code>run-skill-generator</code>. It's a skill that comes with Claude and it learns how to start your specific app <em>within</em> a running session and saves it as a custom reusable skill (e.g. <code>/run-mysaasapp</code>). You (or your agent) can run this skill and it essentially gives the agent "eyes" to see your client and server code, along with their respective logs.</p>
<p>With this run skill in hand, you can just say something like <code>run my saas app and fix the issue</code>. It will start the app with your <code>/run-mysaasapp</code> skill, read the server logs, drive the browser like a human would, find the error, fix it, and confirm it's running, completely hands-off.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-way-out">The way out<a href="https://wasp.sh/blog/2026/06/03/build-a-saas-with-ai-without-getting-stuck#the-way-out" class="hash-link" aria-label="Direct link to The way out" title="Direct link to The way out" translate="no">​</a></h2>
<p>By staying the architect, and providing your agent with good structure and process, you can avoid most of the problems that plague builders trying to create their SaaS idea with AI.</p>
<p>I walk through all five of these with live demos (e.g. fetching llms.txt docs, the clarify-and-brainstorm prompts, the vertical-slice plan, and the agent fixing its own startup error) in the video:</p>
<p><strong>▶️ <a href="https://youtu.be/0DpqHItVkYs" target="_blank" rel="noopener noreferrer" class="">How To Build A SaaS with AI. Without Getting Stuck.</a></strong></p>
<p>And if you want the rails to start on:</p>
<ul>
<li class=""><strong>Open SaaS</strong> (free, open source, 14.5k+★): <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">opensaas.sh</a></li>
<li class=""><strong>Docs:</strong> <a href="https://docs.opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">docs.opensaas.sh</a></li>
<li class=""><strong>Discord</strong> (come say hi, we're happy to help): <a href="https://discord.gg/rzdnErX" target="_blank" rel="noopener noreferrer" class="">discord.gg/rzdnErX</a></li>
</ul>]]></content:encoded>
            <author>vince@wasp-lang.dev (Vince Canger)</author>
            <category>wasp</category>
            <category>open saas</category>
            <category>vibe coding</category>
            <category>ai</category>
            <category>claude code</category>
        </item>
        <item>
            <title><![CDATA[Wasp 0.23: Static prerendering for crawlers and better performance]]></title>
            <link>https://wasp.sh/blog/2026/05/25/wasp-static-prerendering</link>
            <guid>https://wasp.sh/blog/2026/05/25/wasp-static-prerendering</guid>
            <pubDate>Mon, 25 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Wasp 0.23 ships static prerendering: add prerender: true to any route and get real HTML for search engines, AI crawlers, and faster load times.]]></description>
            <content:encoded><![CDATA[<p>Wasp apps are single-page applications, and SPAs have a well-known problem: some search engines and AI crawlers see a blank page until JavaScript loads. Wasp 0.23 adds static prerendering so you can generate real HTML at build time. Just add <code>prerender: true</code> to any route.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-blank-page-problem">The blank page problem<a href="https://wasp.sh/blog/2026/05/25/wasp-static-prerendering#the-blank-page-problem" class="hash-link" aria-label="Direct link to The blank page problem" title="Direct link to The blank page problem" translate="no">​</a></h2>
<p>Single-page applications work by shipping an empty HTML shell to the browser, then letting JavaScript render everything on the client. That's great for rich interactive applications, but it means anything that reads your HTML before JavaScript runs sees... nothing.</p>
<p>This used to be "just" an SEO problem. But the web has changed. Googlebot did start running JavaScript; but new crawlers and AI assistants like ChatGPT, Claude, and Perplexity are trying to get your content to answer user questions. If your site is invisible to them, you're leaving traffic and visibility on the table.</p>
<p>The traditional fixes aren't great either. You could set up a separate static site generator, maintain a full SSR server, or bolt on a prerendering service. Each one adds complexity, infrastructure, and another thing to maintain. For a framework that prides itself on curation, we wanted something better.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-fix-prerender-true">The fix: <code>prerender: true</code><a href="https://wasp.sh/blog/2026/05/25/wasp-static-prerendering#the-fix-prerender-true" class="hash-link" aria-label="Direct link to the-fix-prerender-true" title="Direct link to the-fix-prerender-true" translate="no">​</a></h2>
<p>Here's what it looks like in Wasp 0.23:</p>
<div class="language-wasp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockTitle_OeMC">main.wasp</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-wasp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token declaration-type keyword" style="color:#00009f">route</span><span class="token plain"> </span><span class="token class-name variable" style="color:#36acaa">LandingRoute</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token dict-key plain" style="color:#393A34">path</span><span class="token plain">: </span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token dict-key plain" style="color:#393A34">to</span><span class="token plain">: </span><span class="token class-name variable" style="color:#36acaa">LandingPage</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain">  </span><span class="token dict-key plain" style="color:#393A34">prerender</span><span class="token plain">: </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token declaration-type keyword" style="color:#00009f">page</span><span class="token plain"> </span><span class="token class-name variable" style="color:#36acaa">LandingPage</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token dict-key plain" style="color:#393A34">component</span><span class="token plain">: </span><span class="token js-import keyword module" style="color:#00009f">import</span><span class="token js-import"> </span><span class="token js-import imports punctuation" style="color:#393A34">{</span><span class="token js-import imports"> </span><span class="token js-import imports maybe-class-name">LandingPage</span><span class="token js-import imports"> </span><span class="token js-import imports punctuation" style="color:#393A34">}</span><span class="token js-import"> </span><span class="token js-import keyword module" style="color:#00009f">from</span><span class="token js-import"> </span><span class="token js-import string" style="color:#e3116c">"@src/LandingPage"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>That's it. One field. No config files, no extra server, no separate build pipeline.</p>
<p>When you run <code>wasp build</code>, Wasp renders that route's component to static HTML at build time. When a browser or crawler hits that URL, they get real, meaningful content immediately. Then React <a href="https://react.dev/reference/react-dom/client/hydrateRoot" target="_blank" rel="noopener noreferrer" class="">hydrates</a> the page on the client for full interactivity. Your users get instant content, and your crawlers get something to actually read.</p>
<p>Every route without <code>prerender</code> keeps working exactly as before. You opt in per route, so you can choose which pages get prerendered and which stay dynamic. It's flexible and non-intrusive.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-to-use-it">When to use it<a href="https://wasp.sh/blog/2026/05/25/wasp-static-prerendering#when-to-use-it" class="hash-link" aria-label="Direct link to When to use it" title="Direct link to When to use it" translate="no">​</a></h2>
<p>Prerendering is perfect for pages where the content is known at build time:</p>
<ul>
<li class=""><strong>Landing pages</strong> and marketing pages</li>
<li class=""><strong>About, pricing, and FAQ</strong> pages</li>
<li class=""><strong>Any mostly-static content</strong> that doesn't depend on the logged-in user</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-not-to-use-it">When not to use it<a href="https://wasp.sh/blog/2026/05/25/wasp-static-prerendering#when-not-to-use-it" class="hash-link" aria-label="Direct link to When not to use it" title="Direct link to When not to use it" translate="no">​</a></h2>
<p>Prerendering generates HTML once, at build time. That means it's not the right tool for pages that need to be different for each request:</p>
<ul>
<li class=""><strong>User dashboards and authenticated pages</strong>, where content depends on who's logged in, so there's nothing meaningful to render ahead of time.</li>
<li class=""><strong>Pages that rely heavily on browser APIs</strong>, like reading from <code>localStorage</code>, geolocation, or other client-specific data that isn't available at build time.</li>
</ul>
<p>For these cases, the regular SPA behavior works just fine. Prerendering is an opt-in upgrade for the pages that benefit from it, not something you need on every route.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-we-got-here">How we got here<a href="https://wasp.sh/blog/2026/05/25/wasp-static-prerendering#how-we-got-here" class="hash-link" aria-label="Direct link to How we got here" title="Direct link to How we got here" translate="no">​</a></h2>
<p>This feature didn't happen overnight. We published a <a href="https://wasp-lang.notion.site/PUBLIC-RFC-Wasp-Deep-Freeze-30618a74854c80d787e4e3b99a66db43" target="_blank" rel="noopener noreferrer" class="">public RFC ("Deep Freeze")</a> outlining the design, then built it in two stages: first the <a href="https://github.com/wasp-lang/wasp/pull/3855" target="_blank" rel="noopener noreferrer" class="">SSR rendering machinery</a> under the hood, then the <a href="https://github.com/wasp-lang/wasp/pull/3982" target="_blank" rel="noopener noreferrer" class="">user-facing <code>prerender</code> field</a> that wires it all together.</p>
<p>This is also a foundation. The SSR infrastructure we built opens the door to more rendering strategies down the road. Prerendering static routes is just the beginning, and rendering on the server in response to a request is on our roadmap for the future.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="giving-back">Giving back<a href="https://wasp.sh/blog/2026/05/25/wasp-static-prerendering#giving-back" class="hash-link" aria-label="Direct link to Giving back" title="Direct link to Giving back" translate="no">​</a></h2>
<p>At the core of our prerendering approach is a Vite plugin built on the <a href="https://vite.dev/guide/api-environment" target="_blank" rel="noopener noreferrer" class="">Vite Environments API</a>. It handles the SSR rendering pipeline, HTML generation, and hydration wiring. We designed it from the get go <a href="https://github.com/wasp-lang/wasp/tree/main/waspc/data/Generator/libs/vite-ssr" target="_blank" rel="noopener noreferrer" class="">as a self-contained project</a>, and we're planning to extract it from our codebase and open-source this foundation once we stabilize it, so that other projects and frameworks can build on top of it too. Stay tuned.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-it-out">Try it out<a href="https://wasp.sh/blog/2026/05/25/wasp-static-prerendering#try-it-out" class="hash-link" aria-label="Direct link to Try it out" title="Direct link to Try it out" translate="no">​</a></h2>
<p>If you're on Wasp 0.23, you can start prerendering routes right now. Pick your landing page, add <code>prerender: true</code>, build, and deploy. That's the whole migration.</p>
<p>Check out the <a class="" href="https://wasp.sh/docs/advanced/prerendering">full prerendering docs</a> for details on how it works, limitations, and troubleshooting.</p>
<p>We'd love to hear how it works for you, come chat with us on <a href="https://discord.gg/rzdnErX" target="_blank" rel="noopener noreferrer" class="">Discord</a>!</p>]]></content:encoded>
            <category>wasp</category>
            <category>seo</category>
            <category>prerendering</category>
            <category>webdev</category>
            <category>react</category>
        </item>
        <item>
            <title><![CDATA[5 Years and $5M Later: Inventing a New Programming Language for Web Development Was a Mistake]]></title>
            <link>https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake</link>
            <guid>https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake</guid>
            <pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[After 5 years and $5M building Wasp, we're replacing our custom config language with TypeScript. Here's why we thought a new language was a good idea, what we learned, and why TypeScript won.]]></description>
            <content:encoded><![CDATA[<p>At <a href="https://github.com/wasp-lang/wasp" target="_blank" rel="noopener noreferrer" class="">Wasp</a>, we're building a full-stack web framework - think Rails / Laravel for JS, but "stretched" over the frontend, too. My twin brother and I started it back in 2021 when we went through Y Combinator, and raised over $5M in total.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp's Launch HN post on Hacker News from February 2021" src="https://wasp.sh/img/new-language-mistake/hn-launch.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>Our initial idea was to build a new programming language that would abstract common web app patterns while also working with any stack (we started with React, Node.js and Prisma) when you need to go deeper. Think Terraform, but for your web app stack instead of cloud infrastructure.</p>
<p>Five years in, we realize it was a mistake. Creating a new language makes sense for certain problems and domains, but in this case, it wasn't a fit and brought us more trouble than it was worth.</p>
<!-- -->
<p>This post is about why we thought it was a good idea, what we learned, and why we're replacing our custom language with TypeScript, while Wasp itself stays the same under the hood.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tldr">TL;DR<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#tldr" class="hash-link" aria-label="Direct link to TL;DR" title="Direct link to TL;DR" translate="no">​</a></h2>
<p>This is a long post. Here are the key things covered, so you can jump to what interests you without reading everything:</p>
<ul>
<li class="">Having switched web dev stacks for years, we thought it would be cool to create a "universal framework" that could work with any stack. <a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#why-create-a-new-language-from-scratch-why-not-use-the-existing-ones" class="">We concluded we need to create a new language</a> to achieve that.</li>
<li class=""><a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#reception-you-loved-the-idea-and-tolerated-the-language" class="">Developers resonated with the problem Wasp was solving, but the language was a tough sell</a>. "Lang" in the name made them think our aim was to replace JavaScript (it wasn't) and were skeptical of how it'd work with their tooling.</li>
<li class="">Many developers loved Wasp once they gave it a try. But getting them to try it out was the hard part.</li>
<li class="">Adoption still grew, but as we kept pushing towards 1.0 we realised the "language" concern wasn't going away. Also, <a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#the-final-nail---trying-to-make-ide-support-work" class="">maintaining a good IDE experience for a custom language proved to be way harder than expected</a>.</li>
<li class="">It turned out having a custom language wasn't Wasp's core value. It was <a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#language-was-never-the-moat-its-having-a-high-level-understanding-of-your-entire-app-at-compile-time" class="">maintaining a high-level specification of the entire full-stack app</a>, making it easier to reason about for both humans and AI.</li>
<li class=""><a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#goodbye-wasp-language-welcome-typescript" class="">We decided to replace Wasp's config language with TypeScript</a>. It is "only" an interface change, while everything else stays the same.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-we-thought-creating-a-new-language-was-a-good-idea">Why we thought creating a new language was a good idea<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#why-we-thought-creating-a-new-language-was-a-good-idea" class="hash-link" aria-label="Direct link to Why we thought creating a new language was a good idea" title="Direct link to Why we thought creating a new language was a good idea" translate="no">​</a></h2>
<p>My brother and I both come from a traditional Computer Science background, where we focused mostly on algorithms and applying them to areas such as bioinformatics and machine learning. We never considered ourselves web developers, and were even somewhat opposed to it (<em>"look at these folks just painting buttons the whole day, while I'm writing all the cool algorithms."</em>). Obviously, we didn't know enough.</p>
<p>Still, we had one entrepreneurial friend in our group who was always doing something on the side, and he would pull us in whenever he'd land a bigger project. Each of these projects had something that made it "interesting" for us (e.g., building a news portal that also needed a recommendation engine, or an option trading analytics platform), but at the end of the day, it was mostly a classic web development job.</p>
<p>That way, we got exposed to things like PHP (we built a complete CMS from scratch in it and then later realized there were frameworks and even off-the-shelf solutions we could have used), Java/JBoss, and also Backbone.js, Angular.js, and React as they were popping up, along with the build tools (remember Bower, Grunt, and Gulp)?</p>
<p>2013 and later marked the era of reactive front-end frameworks, and each new one was something you absolutely had to use in your latest project, because that was part of what made your project/startup cool. The result was that each new project we started, we started with almost a completely different stack from the last one and had to re-learn all the patterns that we just figured out - authentication, routing, state management, …</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="we-got-tired-of-switching-and-stitching-stacks">We got tired of switching and stitching stacks<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#we-got-tired-of-switching-and-stitching-stacks" class="hash-link" aria-label="Direct link to We got tired of switching and stitching stacks" title="Direct link to We got tired of switching and stitching stacks" translate="no">​</a></h3>
<p>Before, one would just choose between Spring Boot, Django, or Rails and have everything handled and managed. Now, I had to first pick each library, then make React, Redux, Webpack, Express, Passport, and Sequelize work together.</p>
<p>No system was in charge, with a full understanding of the web app, and that feeling of responsibility didn't really sit well with Martin and me.</p>
<p>Having gone through this stack-switching dance a few times (eventually landing on React, Node.js, and Mongoose at the time), we thought - isn't there a better way? It felt like we were constantly reinventing the same thing, spending most of our time managing our stack rather than writing our unique business logic.</p>
<p>There's a name for this: <em>accidental complexity</em>. The work that has nothing to do with your actual problem, but you can't avoid it because of the tools you're using. That's exactly what we were feeling.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-if-i-could-just-say-what-i-want-once">What if I could just say what I want, once?<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#what-if-i-could-just-say-what-i-want-once" class="hash-link" aria-label="Direct link to What if I could just say what I want, once?" title="Direct link to What if I could just say what I want, once?" translate="no">​</a></h3>
<p>We asked ourselves: what if you could simply write what you want, and that's it? For example:</p>
<ul>
<li class=""><em>"I want my app to have authentication with Google and GitHub"</em></li>
<li class=""><em>"I want to have a route /profile, which is available only to authenticated users"</em></li>
<li class=""><em>"I want a cron job that runs this function every day at 5pm"</em></li>
</ul>
<p>Obviously, this is too conversational-y, so we imagined boiling it down to something more structured, like:</p>
<ul>
<li class=""><code>auth: Google, GitHub</code></li>
<li class=""><code>page Profile -&gt; /profile, authRequired: true</code></li>
<li class=""><code>job updateStats: run function doSomeCalc from stats.js every day at 5pm</code></li>
</ul>
<p>It would basically be a way to define your app's requirements at a higher, implementation-free level (aka specification). We felt something like this could and should be possible, but with the tools we had, we had to implement each of these requirements scattered across the stack at what felt like an abstraction level too low.</p>
<p>Our goal was never to replace the existing stack. You'd still implement most of your logic in a specific runtime (e.g., React or Node.js). But there'd be a central backbone which holds everything together.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp architecture diagram" src="https://wasp.sh/img/new-language-mistake/wasp-architecture.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">How we imagined (and implemented) Wasp. The config .wasp file with high-level declarations + your custom logic in React &amp; Node.js goes in, and the fully working full-stack app is produced as a build artefact.</figcaption></figure></div>
<p>If you're curious, this is a code excerpt showing what the DSL looked like in its final form:</p>
<div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">app todoApp </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ToDo App"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// visible in the browser tab</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">auth</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// full-stack auth out-of-the-box</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">userEntity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">User</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">methods</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">google</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">gitHub</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">email</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token spread operator" style="color:#393A34">...</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">route </span><span class="token maybe-class-name">RootRoute</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">path</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">to</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">MainPage</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">page </span><span class="token maybe-class-name">MainPage</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">authRequired</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Limit access to logged in users.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">component</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">Main</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@client/Main"</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// &lt;-- Your React code.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">query getTasks </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">fn</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> getTasks </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@server/tasks"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// &lt;-- Your Node.js code.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token literal-property property" style="color:#36acaa">entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token maybe-class-name">Task</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Automatic cache invalidation.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>The key insight for us was that the <strong>web app domain changes very slowly compared to the velocity at which web dev implementation techniques evolve</strong>. Both ten years ago and today, we refer to pages, routes, API endpoints, and data models. In the meantime, the UI implementation went full circle from server templates to the rich client and back to server actions.</p>
<p>Our goal with Wasp was to let developers express as much as possible at the domain level, and let the system handle the (latest) implementation details. That way, whatever they build will last much longer and be easier to understand.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-create-a-new-language-from-scratch-why-not-use-the-existing-ones">Why create a new language from scratch? Why not use the existing ones?<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#why-create-a-new-language-from-scratch-why-not-use-the-existing-ones" class="hash-link" aria-label="Direct link to Why create a new language from scratch? Why not use the existing ones?" title="Direct link to Why create a new language from scratch? Why not use the existing ones?" translate="no">​</a></h3>
<p>While our understanding of the problem has mostly proven correct, because we experienced it first-hand, this was where we took the wrong turn.</p>
<p>There were two main reasons why we opted to design a new language from scratch, with its own custom compiler, rather than use, e.g., TypeScript or Python as a "host" language:</p>
<ul>
<li class=""><strong>We wanted to have a full control over the syntax</strong>, and make it as clean and boilerplate-free as possible.</li>
<li class=""><strong>We envisioned Wasp as a runtime-agnostic tool</strong>, eventually. E.g. maybe you write a piece of a typical CRUD logic in Node.js, but then some data-heavy stuff you'd rather do in Python which has better libraries for it.</li>
</ul>
<p>While we considered using TypeScript (and that was some of the early feedback we got, to have an embedded DSL, which is basically just a library, instead of the explicit DSL), it felt "wrong" to us at the time, as if we'd betray the core principles and a big vision we had for Wasp.</p>
<p>In a way, we were very excited to ship Wasp as a standalone language because we believed it would make a stronger statement and make it clear it's not another typical language-bound framework (e.g., Rails or Django), but something more general.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp first landing page" src="https://wasp.sh/img/new-language-mistake/first-landing-page.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Our first landing page, back in 2021. We were proudly putting "language" front and center.</figcaption></figure></div>
<p>To be completely honest, we were also excited about the fact that we get to work on something so "cool" and foundational, such as a language and a compiler. Martin and I are both Haskell aficionados, and this was the perfect nail to hit with our functional hammer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reception-you-loved-the-idea-and-tolerated-the-language">Reception: You loved the idea and tolerated the language<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#reception-you-loved-the-idea-and-tolerated-the-language" class="hash-link" aria-label="Direct link to Reception: You loved the idea and tolerated the language" title="Direct link to Reception: You loved the idea and tolerated the language" translate="no">​</a></h2>
<p>After about <a class="" href="https://wasp.sh/blog/2022/09/29/journey-to-1000-gh-stars">a year of working on Wasp, releasing an Alpha version, and launching it to developers</a>, we found a small tribe of excited early adopters and were accepted into Y Combinator. We raised our <a href="https://techcrunch.com/2021/10/04/yc-grads-wasp-land-1-5m-seed-to-help-developers-build-web-apps-faster/" target="_blank" rel="noopener noreferrer" class="">pre-seed round</a> shortly afterward, which enabled us to assemble the team and go build the first "real" version.</p>
<p>Things were slow at the beginning as we were getting everything off the ground, but developers acutely felt the pain of boilerplate and were tired of stack-stitching, so Wasp gained momentum, especially after we entered Beta.</p>
<p>You might also remember that at a similar time, back in 2021, a couple of other "Rails for JS" frameworks appeared on the scene, like RedwoodJS (by the creator of GitHub) and BlitzJS. They quickly got solid community traction, and it became clear to us that we're on the track to solve a very important problem.</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>Being weird sometimes pays off</div><div class="admonitionContent_BuS1"><p>In a way, Wasp's "weirdness" is what saved it from Redwood's and Blitz's fate. By avoiding tight coupling to a specific technology (as Redwood did with GraphQL and Blitz with Next.js), it remained general enough to adapt quickly and avoid becoming obsolete.</p></div></div>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="GitHub stars growth" src="https://wasp.sh/img/new-language-mistake/github-stars.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>Still, talking to developers, the question of "But why did you create a new language?" kept popping up in different contexts. There were a few main objections we kept hearing.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="i-see-wasp-lang---does-this-replace-javascript">I see "wasp-lang" - does this replace JavaScript?<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#i-see-wasp-lang---does-this-replace-javascript" class="hash-link" aria-label="Direct link to I see &quot;wasp-lang&quot; - does this replace JavaScript?" title="Direct link to I see &quot;wasp-lang&quot; - does this replace JavaScript?" translate="no">​</a></h3>
<p>Although we never meant to replace the existing web dev stack, but rather complement it (with Wasp, you still write 90% of your code in React &amp; Node.js),  the notion of the "lang" suffix was simply too strong to convey that.</p>
<p>Whenever a developer read "wasp-lang.dev", their mind automatically went to "oh, this is something like Rust/Java", and it was very hard to remove that lens once it came up. It immediately put Wasp into the "looks cool, but it's way too early" bucket.</p>
<p>We were very excited about building a language and even highlighted it, but we underestimated how strong the term's meaning already is.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="is-this-going-to-work-with-my-ide-and-existing-tooling">Is this going to work with my IDE and existing tooling?<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#is-this-going-to-work-with-my-ide-and-existing-tooling" class="hash-link" aria-label="Direct link to Is this going to work with my IDE and existing tooling?" title="Direct link to Is this going to work with my IDE and existing tooling?" translate="no">​</a></h3>
<p>Even if you gave the language thing the benefit of the doubt, the next question is, "Does this come with its own ecosystem?" Developers know how much work it is to create a new standard and how much time it takes to grow the ecosystem around it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="this-isnt-for-me-i-dont-want-to-learn-haskell">This isn't for me, I don't want to learn Haskell<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#this-isnt-for-me-i-dont-want-to-learn-haskell" class="hash-link" aria-label="Direct link to This isn't for me, I don't want to learn Haskell" title="Direct link to This isn't for me, I don't want to learn Haskell" translate="no">​</a></h3>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Haskell love meme" src="https://wasp.sh/img/new-language-mistake/haskell-love.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>We're using Haskell internally for our compiler, but end users will never see it and will use only TypeScript. It's similar to how Prisma's core was initially developed in Rust, and Terraform's HCL in Go.</p>
<p>Still, our initial marketing around using Haskell worked out a bit too well for our own good. The Haskell community is small, but engaged, enthusiastic, and very hungry for real-world projects, especially around developer tooling.</p>
<p>Sharing our progress with the community was often very well accepted, but also reinforced Wasp as a "Haskell-based language", especially if you just glanced over the title.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Haskell positioning issue" src="https://wasp.sh/img/new-language-mistake/haskell-positioning.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>Pair that up with the fact that on the GitHub repo page in the "Languages" bar, it said "Haskell: 90%" (we later found a way to circumvent this), and you got yourself a perfectly executed wrong positioning.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="its-the-packaging-issue">It's the packaging issue<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#its-the-packaging-issue" class="hash-link" aria-label="Direct link to It's the packaging issue" title="Direct link to It's the packaging issue" translate="no">​</a></h3>
<p>Many developers who actually tried Wasp loved it and didn't mind the language much. They could ship faster than ever without spending weeks learning the stack, and keep using React &amp; Node.js. But getting developers to make that leap from <em>"what is this?"</em> to <em>"I will give it a go"</em> was really hard.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Developer testimonials about Wasp" src="https://wasp.sh/img/new-language-mistake/developer-love.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Once they tried Wasp, many developers really loved it. The hard part was getting them to try it.</figcaption></figure></div>
<p>We kept pushing. With Beta, adoption soared, and we started seeing startups built with Wasp getting acquired, as well as enterprises building internal tools and deploying them into their local infra setup (in the end, the Wasp app is just static frontend files + a single Docker image for the backend).</p>
<p>To get past the "this is a weird new framework, why should I try it?" barrier, we built products on top of Wasp (an <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">open-source SaaS boilerplate starter</a> and <a class="" href="https://wasp.sh/blog/2023/07/10/gpt-web-app-generator">our own "early" Lovable</a>) that shifted the decision to a higher level. It worked great and brought many people to Wasp.</p>
<p>Still, the core issue persisted. Developers who came to Wasp couldn't understand what we were building, so they couldn't get excited about it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-final-nail---trying-to-make-ide-support-work">The final nail - trying to make IDE support work<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#the-final-nail---trying-to-make-ide-support-work" class="hash-link" aria-label="Direct link to The final nail - trying to make IDE support work" title="Direct link to The final nail - trying to make IDE support work" translate="no">​</a></h3>
<p>We always assumed that if our language proves to be a wrong choice, it will be because our users told us so. It turned out to be the opposite: while developers who adopted Wasp were happy to keep using it, we started having more and more issues with it internally.</p>
<p>One of the main things we underestimated was how much work it takes to develop the necessary tooling for a custom language, especially IDE/editor support. The bar developers expect today, especially in the JS world, is incredibly high, and the line between an IDE and a compiler is getting blurred.</p>
<p>Once we dove deeper into building tooling for a custom language, we quickly realized the entire ecosystem is built for "standard" JavaScript and TypeScript frameworks. Anything else, and you're on your own, hitting walls really fast.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="VS Code extension for Wasp language" src="https://wasp.sh/img/new-language-mistake/vscode-extension.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">VS Code extension for Wasp language</figcaption></figure></div>
<p>We ended up developing our own language server and a VS Code extension for it, but since Wasp used Prisma's DSL as an embedded language and had many references to React &amp; Node.js files, we still only reached 80% of where we wanted to be.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="goodbye-wasp-language-welcome-typescript">Goodbye Wasp language, welcome TypeScript<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#goodbye-wasp-language-welcome-typescript" class="hash-link" aria-label="Direct link to Goodbye Wasp language, welcome TypeScript" title="Direct link to Goodbye Wasp language, welcome TypeScript" translate="no">​</a></h2>
<p>With all the accumulated pain around maintaining a custom language, and the constant friction of getting new developers to give it a try, we realized the root cause is deeper than "developers are afraid to try a new web framework".</p>
<p>The usage kept growing, and more and more people were shipping their Wasp apps to production, but whatever we tried, it would always ultimately come back to "I really like what you're doing with Wasp, but why a custom language?". It felt like we were constantly driving with a handbrake pulled up.</p>
<p>Finally, the ergonomics we aimed for with the language didn't turn out to be as important as we thought. Developers are perfectly happy using TypeScript, a language they are familiar with, even if it requires a few extra lines and braces.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="language-was-never-the-moat-its-having-a-high-level-understanding-of-your-entire-app-at-compile-time">Language was never the moat. It's having a high-level understanding of your entire app at compile time.<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#language-was-never-the-moat-its-having-a-high-level-understanding-of-your-entire-app-at-compile-time" class="hash-link" aria-label="Direct link to Language was never the moat. It's having a high-level understanding of your entire app at compile time." title="Direct link to Language was never the moat. It's having a high-level understanding of your entire app at compile time." translate="no">​</a></h3>
<p>When we started Wasp, the terms "language" and "specification" were basically synonyms to us. We couldn't imagine one without the other.</p>
<p>But watching developers use Wasp and seeing what they are excited about, it became clear it was never the language. It was the fact that Wasp has a full understanding of their entire app via a high-level spec (e.g., <code>main.wasp</code>, now <code>main.wasp.ts</code>), which lets them easily grasp how their app works at a glance and feel in control.</p>
<p>One way in which we tried to make it more tangible is by having a <code>wasp studio</code> command - if you run it in your terminal, you'll see how Wasp "sees" your app at compile time, and can reason about it before generating the target code:</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp studio showing app structure" src="https://wasp.sh/img/new-language-mistake/wasp-studio.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">wasp studio - how Wasp "sees" the structure of your app</figcaption></figure></div>
<p>Having this kind of "support system" made the process of building an app a much more managed experience. With AI and developers reviewing generated code less frequently, this became even more valuable, especially for a new generation of builders with less technical backgrounds (so-called "vibe-coders").</p>
<p>And this app-level understanding doesn't change at all with the switch from Wasp's custom language to TypeScript. We only swapped the "front end" of the compiler, or how you define a high-level app spec in Wasp.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="typescript-sdk---from-an-experiment-to-production">TypeScript SDK - from an experiment to production<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#typescript-sdk---from-an-experiment-to-production" class="hash-link" aria-label="Direct link to TypeScript SDK - from an experiment to production" title="Direct link to TypeScript SDK - from an experiment to production" translate="no">​</a></h3>
<div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Page &amp; route definition</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> loginPage </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'LoginPage'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">component</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">importDefault</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Login'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@src/pages/auth/Login'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'LoginRoute'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">path</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'/login'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">to</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> loginPage </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Query</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">query</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'getTasks'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">fn</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">import</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'getTasks'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@src/queries'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'Task'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Async job</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">job</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'mySpecialJob'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">executor</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'PgBoss'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">perform</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">fn</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">import</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'foo'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@src/jobs/bar'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">executorOptions</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">pgBoss</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">retryLimit</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'Task'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">default</span><span class="token plain"> app</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p>We introduced the <a class="" href="https://wasp.sh/docs/0.23/general/wasp-ts-config">TypeScript SDK for Wasp</a> as an experimental, early preview feature to gauge feedback. And we've received very positive feedback - some of our new users immediately opted for it and never even tried using the Wasp language. This has been a signal for us to keep going.</p>
<p>After testing it internally and within the Wasp community, we're now confident this is the way forward. Besides circumventing the initial "why a new language" question, it solves all the problems we've been fighting for years.</p>
<p>Every editor works out of the box. Developers can use conditionals, loops, and imports (e.g., you could implement your own file-based routing!). Splitting the spec across multiple files becomes trivial. And it gives us a solid foundation for the advanced features we've been wanting to build, like <a href="https://github.com/wasp-lang/wasp/issues/2873" target="_blank" rel="noopener noreferrer" class="">Full Stack Modules</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dsl-thanks-for-all-the-fish---wed-never-come-this-far-without-you">DSL, thanks for all the fish - we'd never come this far without you<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#dsl-thanks-for-all-the-fish---wed-never-come-this-far-without-you" class="hash-link" aria-label="Direct link to DSL, thanks for all the fish - we'd never come this far without you" title="Direct link to DSL, thanks for all the fish - we'd never come this far without you" translate="no">​</a></h2>
<p>Looking back, Wasp probably wouldn't have come to life if we hadn't started the way we did. It was our best way to express what we wanted to build at the time, paired up with some personal biases, and well, what we simply thought was fun. It was the curiosity of "We know this sounds like a crazy idea, but we really want to see if it can work" that kept us pushing even when no one was watching.</p>
<p>The DSL approach kept us honest to our vision that the specification should be separate from the implementation. We're still curious about opening support for other languages and runtimes (e.g., Python or Rust) and about the benefits of having a full picture of your app at compile time and being able to reason about it before generating the target code (which opens space for supporting different architectures, optimizations, etc.).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="your-ai-agent-might-like-wasp-too">Your AI agent might like Wasp, too<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#your-ai-agent-might-like-wasp-too" class="hash-link" aria-label="Direct link to Your AI agent might like Wasp, too" title="Direct link to Your AI agent might like Wasp, too" translate="no">​</a></h2>
<p>Finally, as AI agents write more and more of our code, we see many developers looking for tools that provide more structure and opinions upfront, which in turn makes adding features more predictable and the generated code easier to understand and review.</p>
<p>Wasp fits perfectly here, since it spans the full stack of your app and ensures everything works together at all times. It's why many developers have also found renewed love for "old-school" monolithic frameworks such as Django, Rails, and Laravel. Wasp aims to provide the same experience and that feeling of "it just all works", but for the JS ecosystem.</p>
<p>We repeatedly hear from developers using Wasp that it is the stack that works best with AI, and some of them even went so far as to <a class="" href="https://wasp.sh/blog/2026/03/29/hireveld-from-10-stacks-to-production-with-wasp">try 10 different solutions before settling on Wasp</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="we-are-shipping-typescript-first-wasp-soon">We are shipping TypeScript-first Wasp soon<a href="https://wasp.sh/blog/2026/05/13/new-language-for-web-dev-was-a-mistake#we-are-shipping-typescript-first-wasp-soon" class="hash-link" aria-label="Direct link to We are shipping TypeScript-first Wasp soon" title="Direct link to We are shipping TypeScript-first Wasp soon" translate="no">​</a></h2>
<p>In the next few weeks, we're hosting Launch Week, during which we'll ship the TypeScript SDK as the primary way to use Wasp. We've already seen the excitement within our community, and can't wait to hear what you think about it.</p>
<p>If you've ever been curious about Wasp, now is the perfect time to give it a try. No new language to learn. Just TypeScript, and everything Wasp does for you under the hood.</p>
<p>To stay in the loop, join our <a href="https://discord.gg/rzdnErX" target="_blank" rel="noopener noreferrer" class="">Discord</a>, or follow <a href="https://twitter.com/WaspLang" target="_blank" rel="noopener noreferrer" class="">Wasp</a> or <a href="https://twitter.com/MatijaSosic" target="_blank" rel="noopener noreferrer" class="">me</a> on X/Twitter.</p>]]></content:encoded>
            <author>matija@wasp-lang.dev (Matija Sosic)</author>
            <category>wasp</category>
            <category>web dev</category>
            <category>startup</category>
            <category>programming language</category>
            <category>typescript</category>
        </item>
        <item>
            <title><![CDATA[Introducing Open Vibe: Learn Web Dev While You Build Apps With an AI Tutor]]></title>
            <link>https://wasp.sh/blog/2026/04/29/introducing-open-vibe</link>
            <guid>https://wasp.sh/blog/2026/04/29/introducing-open-vibe</guid>
            <pubDate>Wed, 29 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Open Vibe is a free, open-source curriculum that lives inside your AI coding agent. Build real full-stack web apps while an AI tutor teaches you the fundamentals. No signup required.]]></description>
            <content:encoded><![CDATA[<p>You watch a twelve-hour tutorial, copy along, finish with a working app, and feel great. But a week later, you can't actually build a thing on your own.</p>
<p>So you open Claude Code and vibe code your way to a working MVP in a weekend. But you quickly hit a wall trying to debug something and lack the knowledge to express what it is you actually want.</p>
<p>In the end, you may get something that <em>works</em>, but you have no idea <em>how</em> it works.</p>
<!-- -->
<p><strong>Following static tutorials or blindly vibe coding isn't a great way to build professional apps or web dev skills. Without the fundamental knowledge of how the technologies work you'll have a lot of trouble fixing problems, or properly extending the functionality of your app.</strong></p>
<p>That's why we built <a href="https://openvibe.sh/" target="_blank" rel="noopener noreferrer" class="">Open Vibe</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-open-vibe-is">What Open Vibe is<a href="https://wasp.sh/blog/2026/04/29/introducing-open-vibe#what-open-vibe-is" class="hash-link" aria-label="Direct link to What Open Vibe is" title="Direct link to What Open Vibe is" translate="no">​</a></h2>
<p><img decoding="async" loading="lazy" alt="Open Vibe landing page" src="https://wasp.sh/assets/images/banner-e5fcf82c724301f4fe50e6680a76c760.webp" width="2880" height="1620" class="img_ev3q"></p>
<p>Open Vibe is a free, open-source course where an AI coding agent tutors you while you build real full-stack web apps on your own machine, and in your own terminal (and/or editor).</p>
<ul>
<li class=""><strong>100% free, open-source, and no signup required.</strong></li>
<li class=""><strong>Works with Claude Code, Cursor, Codex, Copilot, Open Code</strong>, or any agent that can read files and run tools.</li>
<li class=""><strong>Built by the Wasp team</strong> (the same people behind <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a>, the #1 free SaaS boilerplate on GitHub).</li>
</ul>
<p>There's no platform to log into, no $299 video library to wade through.</p>
<p>You just paste a one-line prompt into your agent of choice, and the curriculum loads. From that point on, you get a guided curriculum that teaches you the fundamentals of building professional-grade web apps with AI. But since the course lives in your agent, you're free to ask it whatever you want and take the course in any direction that suits your interests.</p>
<p><strong>v1.0 is live today. Setup and Module 0 are out, with new modules dropping next week.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-it-actually-works">How it actually works<a href="https://wasp.sh/blog/2026/04/29/introducing-open-vibe#how-it-actually-works" class="hash-link" aria-label="Direct link to How it actually works" title="Direct link to How it actually works" translate="no">​</a></h2>
<video controls="" width="100%" style="border-radius:8px"><source src="https://openvibe.sh/assets/open-vibe-demo.mp4" type="video/mp4"><p>Your browser does not support the video tag.</p></video>
<p>We wanted to make Open Vibe as simple as possible. After you install a coding agent (we suggest <a href="https://claude.com/pricing" target="_blank" rel="noopener noreferrer" class="">Claude Code's $17 Pro Plan</a> for the best experience), just follow these steps:</p>
<p><strong>1. Paste the install prompt.</strong> One short prompt to your agent (full text in the "<a href="https://wasp.sh/blog/2026/04/29/introducing-open-vibe#get-started" class="">Get started</a>" section below). The agent fetches the raw file, loads the curriculum, skills, instructions, and interactive diagrams, and from that moment knows every module and every checkpoint.</p>
<p><strong>2. Build alongside your tutor.</strong> The agent guides and watches you in realtime as you build your app, stepping in to help out whenever you need it. It paces with you, explains important concepts, quizzes you, and pair-programs the parts you don't yet trust yourself with. Stop, ask, branch off, and come back to the guided curriculum whenever you want. There's no video to rewind to find the bit you missed, and your agent is always there to help you.</p>
<p><strong>3. Learn with interactive diagrams on your real app.</strong> This course is hands-on, but also interactive. We built interactive explainers <em>on top of the apps you build</em> to help explain important topics like the request cycle, auth flows, git, and database writes.</p>
<p><img decoding="async" loading="lazy" alt="Interactive diagram example" src="https://wasp.sh/assets/images/diagram-example-58d0491ad7cd571f7ec7aba0bbfa3197.webp" width="1704" height="1306" class="img_ev3q">
<em>Interactive Diagrams show up on top of your running apps as you build.</em></p>
<p><strong>Open Vibe takes advantage of the tools we have today to provide a living course you interact with, so you build the skills you need to ship apps like a pro.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-youll-build">What you'll build<a href="https://wasp.sh/blog/2026/04/29/introducing-open-vibe#what-youll-build" class="hash-link" aria-label="Direct link to What you'll build" title="Direct link to What you'll build" translate="no">​</a></h2>
<p>The course is currently broken down into three phases (this may change as we keep iterating):</p>
<ul>
<li class=""><strong>Phase 1 — Basics.</strong> You build on top of a real full-stack Task Management app: login, database, the works. Setup and Module 0 are available now. Modules 1–3 cover data, styling, and how the front-end and back-end actually wire together.</li>
<li class=""><strong>Phase 2 — Your app from scratch.</strong> Spec your own idea, scaffold a new project, and ship it end-to-end with the agent pairing the whole way. You'll learn tools like Git/GitHub, and how to deploy apps to the web.</li>
<li class=""><strong>Phase 3 — A real SaaS.</strong> Build on <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a>, a free $300-value SaaS template the Wasp team maintains. Auth, payments, an admin dashboard, file uploads, AI integrations, deploy, and more.</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Open SaaS — the free, open-source SaaS template you&amp;#39;ll build on in Phase 3" src="https://wasp.sh/assets/images/open-saas-20d04fc92f743cac6f9c505303f31de4.webp" width="2880" height="1620" class="img_ev3q">
<em>Open SaaS gives you auth, payments, an admin dashboard, and AI integrations out of the box, so Phase 3 is about shipping a real product, not wiring boilerplate.</em></p>
<p><strong>You'll finish with three real apps in your GitHub</strong>, and the best part is that you'll actually understand what they are and how you built them.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-now">Why now<a href="https://wasp.sh/blog/2026/04/29/introducing-open-vibe#why-now" class="hash-link" aria-label="Direct link to Why now" title="Direct link to Why now" translate="no">​</a></h2>
<p>Coding with an AI agent is how people build in 2026. But the ones who ship confidently, and with speed, understand the fundamentals.</p>
<p>If you're non-technical and trying to build by guess-and-check prompting, you know how tedious it can be.</p>
<p>Meanwhile, influencers and bootcamps are still selling video libraries and a Slack channel for hundreds of dollars. But the tutorials are static, obsolete by next month, and basically only teach you how to copy.</p>
<p>Both approaches miss this new fact: <strong>the agent in your terminal/editor is patient, current, and super knowledgeable.</strong> It knows how to code really well, and how to solve almost any problem... if you know what to ask of it!</p>
<p>Open Vibe is the missing layer: a curriculum <em>for</em> the agent, so it tutors you, teaching you the skills you need to vibe code like a pro while you build professional-grade web apps.</p>
<p><strong>The goal isn't to ship apps you don't understand faster. It's to ship apps you <em>do</em> understand, fast.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="get-started">Get started<a href="https://wasp.sh/blog/2026/04/29/introducing-open-vibe#get-started" class="hash-link" aria-label="Direct link to Get started" title="Direct link to Get started" translate="no">​</a></h2>
<p>Just open your agent of choice and paste:</p>
<blockquote>
<p><em>I'm starting the "Ship Your First App" course. Run <code>curl -fsSL https://openvibe.sh/llms.txt</code> and follow the file's instructions.</em></p>
</blockquote>
<p>That's it. The agent handles the rest.</p>
<p>If you get stuck, <a href="https://discord.gg/rzdnErX" target="_blank" rel="noopener noreferrer" class="">the Discord</a> has actual humans on it, or you can leave us feedback on our <a href="https://docs.google.com/forms/d/e/1FAIpQLSdRtrMJmoBXyTVDWKP1jCTe_PzELp5BTWlvv-TskMpNGoI9ww/viewform" target="_blank" rel="noopener noreferrer" class="">feedback form</a>. If you want to fork the course, run your own class with it, or contribute a module, <a href="https://github.com/wasp-lang/ship-your-first-app" target="_blank" rel="noopener noreferrer" class="">the repo is on GitHub</a>.</p>
<p><strong>Free forever. Fork it. Run your own class with it. We just want more people building real things.</strong></p>]]></content:encoded>
            <author>vince@wasp-lang.dev (Vince Canger)</author>
            <category>wasp</category>
            <category>open vibe</category>
            <category>vibe coding</category>
            <category>ai</category>
            <category>education</category>
            <category>claude code</category>
        </item>
        <item>
            <title><![CDATA[They said AI Would Kill SaaS Boilerplates. It's Doing the Opposite.]]></title>
            <link>https://wasp.sh/blog/2026/04/23/14k-stars-open-saas-ai-era</link>
            <guid>https://wasp.sh/blog/2026/04/23/14k-stars-open-saas-ai-era</guid>
            <pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Open SaaS just crossed 14,000 GitHub stars — right in the middle of the biggest AI coding boom in history. Here's why SaaS boilerplates matter more in the AI era, not less.]]></description>
            <content:encoded><![CDATA[<p>When ChatGPT launched everyone was predicting that developers would soon be obsolete.</p>
<p>This was about the same time we launched <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a>, our free, open-source SaaS starter. Using the same logic, you'd have expected that AI would have killed boilerplate starters by now. But we noticed the opposite trend.</p>
<!-- -->
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="The Open SaaS repo star growth chart annotated with key milestones in the vibe coding movement." src="https://wasp.sh/img/open-saas/open-saas-14k-annotated.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Open SaaS launched and grew during the vibe coding boom</figcaption></figure></div>
<p>Open SaaS just crossed 14,000 GitHub stars and its entire growth happened right in the middle of the vibe coding boom.</p>
<p>This surprised us. So we decided to talk to 40 of our users to learn why they turned to Open SaaS.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-renewed-energy-to-build">A renewed Energy to Build.<a href="https://wasp.sh/blog/2026/04/23/14k-stars-open-saas-ai-era#a-renewed-energy-to-build" class="hash-link" aria-label="Direct link to A renewed Energy to Build." title="Direct link to A renewed Energy to Build." translate="no">​</a></h2>
<p>After conducting 40+ user interviews, we noticed a trend: AI was unlocking all kinds of new builders.</p>
<p><strong>Career devs, DevOps engineers, PMs, an ex-woodworker, a marketer with zero coding experience</strong>, and more were getting inspired to start on their Ideas. These were people who always had a SaaS idea but never had the time or skills, or people who were getting inspired to build new apps on top of AI.</p>
<p>Regardless of which type they were, it was AI that unlocked them.</p>
<blockquote>
<p>"Two years ago I didn't believe I could build an application... So it's like kind of magic for me."
— Leo, ex-marketer who built <a href="https://messync.com/" target="_blank" rel="noopener noreferrer" class="">Messync</a></p>
</blockquote>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Messync app screenshot" src="https://wasp.sh/img/lp/examples/messync.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Messync — built by Leo, an ex-marketer with zero coding experience</figcaption></figure></div>
<blockquote>
<p>"If there was no Wasp, I don't think I would even start building this."
— Sergio, 20-year backend veteran who built <a href="https://ctobox.sergiovisinoni.com/" target="_blank" rel="noopener noreferrer" class="">CTOBox</a></p>
</blockquote>
<p>Another one of our users with no React or Node.js experience built a SaaS app and sold it to a major accounting firm for ~$100k.</p>
<p>And the most surprising fact of all: <strong>about half of those interviewed were people who had never built a full-stack app before.</strong></p>
<p>AI gave them the confidence and inspiration to start, but a lot of them turned to Open SaaS to help them focus their efforts.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ai-can-do-90-the-last-10-is-the-hardest">AI Can Do 90%. The Last 10% Is the Hardest.<a href="https://wasp.sh/blog/2026/04/23/14k-stars-open-saas-ai-era#ai-can-do-90-the-last-10-is-the-hardest" class="hash-link" aria-label="Direct link to AI Can Do 90%. The Last 10% Is the Hardest." title="Direct link to AI Can Do 90%. The Last 10% Is the Hardest." translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Using Claude Design to design a landing page for Open Vibe, our new vibe code course" src="https://wasp.sh/img/open-saas/claude-design.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Using Claude Design to design a landing page for Open Vibe, our new vibe code course</figcaption></figure></div>
<p>AI tools are getting insanely good. With <a href="https://claude.ai/design" target="_blank" rel="noopener noreferrer" class="">Claude Design</a> you can scaffold a professional-looking landing page in about an hour. Slap it into Claude Code, add on more components and CRUD logic, and you're almost done.</p>
<p>But it's things like <strong>Stripe webhooks, auth edge cases, environment management, deployment, and background jobs</strong> that really trip up builders.</p>
<p>These aren't necessarily code problems, they're architecture problems. And AI is great at generating code <em>within</em> a clean, working architecture, but you've got to have one first.</p>
<blockquote>
<p>"Open SaaS made me feel secure, like I am not cutting any corners. Just using AI would make it harder to sleep at night."
— Robbie, musician who built <a href="https://peakmastering.com/" target="_blank" rel="noopener noreferrer" class="">PeakMastering</a></p>
</blockquote>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="PeakMastering app screenshot showing trusted-by logos" src="https://wasp.sh/img/lp/examples/peakmastering.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">PeakMastering — built by Robbie, a musician. Trusted by YouTube, Spotify, Apple, and Fender.</figcaption></figure></div>
<blockquote>
<p>"With Next.js App Router, I was constantly fighting the LLM to get the syntax right. Svelte runes were just the Wild West."
— TK Garrett, ex-woodworker who built <a href="https://plottree.ai/" target="_blank" rel="noopener noreferrer" class="">PlotTree</a></p>
</blockquote>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="PlotTree app screenshot" src="https://wasp.sh/img/lp/examples/plottree.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">PlotTree — built by TK Garrett, an ex-woodworker turned developer</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="good-code-makes-ai-code-better">Good Code Makes AI Code Better<a href="https://wasp.sh/blog/2026/04/23/14k-stars-open-saas-ai-era#good-code-makes-ai-code-better" class="hash-link" aria-label="Direct link to Good Code Makes AI Code Better" title="Direct link to Good Code Makes AI Code Better" translate="no">​</a></h2>
<p>After talking with Open SaaS users, we heard the same thing over and over again: having a solid, clean codebase to start with drastically improved the perfomance of AI-coding agents and tools.</p>
<p>And that makes sense, because when you use an opinionated, boilerplate codebase like Open SaaS, you've effectively already decided the architecture of your app for the AI and it no longer needs to spend resources on these decisions, or communicating with you in order to make them.</p>
<p>In the end, you can just let AI handle WHAT you want built because Open SaaS (or any other good boilerplate) already handles HOW it should be built.</p>
<p>Combining them solves for that tedious 10% that kills most projects.</p>
<blockquote>
<p>"If you start with Open SaaS, 80% of the pains of vibe coding are taken care of for you already."
— Kenny Rogers, vibe coding educator</p>
</blockquote>
<blockquote>
<p>"It covers 99% of what I need without me babysitting the AI."
— TK Garrett</p>
</blockquote>
<p>This is why Open SaaS grew steadily to 14k stars through every AI milestone — Cursor blowing up, Karpathy coining "vibe coding," and Claude Code launching.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="The Open SaaS repo reached 14k GitHub stars on April 2026." src="https://wasp.sh/img/open-saas/open-saas-14k.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">The Open SaaS repo reached 14k GitHub stars on April 2026.</figcaption></figure></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-starting-line-just-got-more-crowded">The Starting Line Just Got More Crowded<a href="https://wasp.sh/blog/2026/04/23/14k-stars-open-saas-ai-era#the-starting-line-just-got-more-crowded" class="hash-link" aria-label="Direct link to The Starting Line Just Got More Crowded" title="Direct link to The Starting Line Just Got More Crowded" translate="no">​</a></h2>
<p>AI is empowering more people to become builders and entrepreneurs. But you still have to actually <em>ship</em> something, and few end up doing that.</p>
<p>The ones who do, they tend not to build or reinvent auth and payments from scratch. They start with a foundation that handles it for them, point their AI tools at it, and <strong>focus on what makes their app unique.</strong></p>
<p>That's how <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a> got to 14,000 GitHub stars.</p>
<p>Most people aren't interested in the boilerplate. They're interested in their ideas. Open SaaS is just the fastest way to go from idea to production.</p>
<p><strong>Don't let the 10% block you from shipping your SaaS.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-open-saas">Try Open SaaS<a href="https://wasp.sh/blog/2026/04/23/14k-stars-open-saas-ai-era#try-open-saas" class="hash-link" aria-label="Direct link to Try Open SaaS" title="Direct link to Try Open SaaS" translate="no">​</a></h2>
<p><a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a> is free, open-source, and ready to go. We're constantly updating it to be the best full-featured SaaS boilerplate out there. Here's what you get out of the box:</p>
<ul>
<li class="">Auth with Email, Google, GitHub, Slack, and more (no 3rd-party service needed)</li>
<li class="">Background Jobs with PG Boss (no 3rd-party)</li>
<li class="">Payments with Stripe, Polar.sh, or LemonSqueezy</li>
<li class="">AI Agent Skills and Memory files included</li>
<li class="">ShadCN UI components</li>
<li class="">Example apps to learn from</li>
<li class="">Email with SendGrid, Mailgun, or SMTP</li>
<li class="">End-to-end type safety</li>
</ul>
<p>Star us on <a href="https://github.com/wasp-lang/open-saas" target="_blank" rel="noopener noreferrer" class="">GitHub</a> if you think more people should know about it.</p>]]></content:encoded>
            <author>vince@wasp-lang.dev (Vince Canger)</author>
            <category>open saas</category>
            <category>saas</category>
            <category>ai</category>
            <category>vibe coding</category>
            <category>boilerplate</category>
        </item>
        <item>
            <title><![CDATA[From 10 Failed Stacks to Production: How a Data Scientist Built a Job Board with Wasp, a Full-stack Framework for the Agentic Era]]></title>
            <link>https://wasp.sh/blog/2026/03/29/hireveld-from-10-stacks-to-production-with-wasp</link>
            <guid>https://wasp.sh/blog/2026/03/29/hireveld-from-10-stacks-to-production-with-wasp</guid>
            <pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Hireveld is currently down while Marcel works on a major refactor - but it's real, we swear! It'll be back up soon.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>Hireveld is currently down while Marcel works on a major refactor - but it's real, we swear! It'll be back up soon.</p></div></div>
<p><em>Marcel Coetzee is a data scientist and AI consultant based in South Africa. With a background in actuarial science and data science, he runs his own consultancy. He also builds SaaS products on the side. His latest project, <a href="https://hireveld.co.za/" target="_blank" rel="noopener noreferrer" class="">Hireveld</a>, is a job board tackling South Africa's broken hiring market. He built it entirely with Wasp after trying nearly every other stack out there.</em></p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tell-us-about-yourself-how-did-you-end-up-building-web-apps-as-a-python-developer">Tell us about yourself. How did you end up building web apps as a Python developer?<a href="https://wasp.sh/blog/2026/03/29/hireveld-from-10-stacks-to-production-with-wasp#tell-us-about-yourself-how-did-you-end-up-building-web-apps-as-a-python-developer" class="hash-link" aria-label="Direct link to Tell us about yourself. How did you end up building web apps as a Python developer?" title="Direct link to Tell us about yourself. How did you end up building web apps as a Python developer?" translate="no">​</a></h2>
<p>My path has been a bit unconventional. I started in actuarial science, which involves insurance, mathematical statistics and risk modeling. From there I moved into data science, then data engineering, and eventually into building products. Python has been my main language through all of that.</p>
<p>Today I run my own consultancy doing data engineering and AI work. But I've always wanted to build my own things too, so I started learning the JavaScript ecosystem and working on SaaS products on the side. I'm not a JS native by any means, but with the rise of agentic coding tools, I realized I could finally turn my ideas into real full-stack applications without spending years mastering every corner of Node and React.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-hireveld-and-what-problem-are-you-solving">What's Hireveld, and what problem are you solving?<a href="https://wasp.sh/blog/2026/03/29/hireveld-from-10-stacks-to-production-with-wasp#whats-hireveld-and-what-problem-are-you-solving" class="hash-link" aria-label="Direct link to What's Hireveld, and what problem are you solving?" title="Direct link to What's Hireveld, and what problem are you solving?" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Hireveld homepage showing 'Hire without the markup' headline" src="https://wasp.sh/img/hireveld/hireveld-hero.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Hireveld's landing page - hire without the markup</figcaption></figure></div>
<p>Hiring in South Africa is expensive and opaque. Recruitment agencies take a massive cut of annual salary. The established job boards charge thousands of rands just to post a single listing. And too many roles still get filled through personal connections rather than merit.</p>
<p>I built <a href="https://hireveld.co.za/" target="_blank" rel="noopener noreferrer" class=""><strong>Hireveld</strong></a> to change that. Employers post for free, applicants get ranked anonymously, and employers pay a flat fee to reveal candidates. It's simple, it's cheap, and it puts merit first. The whole thing runs on Wasp - auth, background jobs for expiring old listings, transactional email, payment integration, the works.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="you-mentioned-trying-about-10-different-stacks-before-landing-on-wasp-what-happened">You mentioned trying about 10 different stacks before landing on Wasp. What happened?<a href="https://wasp.sh/blog/2026/03/29/hireveld-from-10-stacks-to-production-with-wasp#you-mentioned-trying-about-10-different-stacks-before-landing-on-wasp-what-happened" class="hash-link" aria-label="Direct link to You mentioned trying about 10 different stacks before landing on Wasp. What happened?" title="Direct link to You mentioned trying about 10 different stacks before landing on Wasp. What happened?" translate="no">​</a></h2>
<p>Yeah, I went through quite the journey. I started with PocketBase because I liked the idea of owning my code and not being locked into a cloud platform. It's a solid tool, but I quickly realized I needed PostgreSQL for search, background jobs, and a frontend that wasn't stitched together by hand. It just didn't scale to what I was building.</p>
<p>Then I tried Next.js, Nuxt, Svelte - they're decent, but those codebases can grow extremely quickly. As someone who's still relatively new to the JS ecosystem, I'd hit the limits of my knowledge fast. I even tried Django, thinking I'd stick with Python, but it's accumulated so much over the years. Too much magic, too much stuff.</p>
<p>My philosophy is: the projects that succeed expose as few abstractions as possible to the user. I try to keep myself at the highest level of abstraction I can. When I found Wasp on GitHub, the config file clicked for me immediately. You declare what you want - auth, database, jobs, email - and it all works together. I was writing actual product code on day one instead of gluing infrastructure together.</p>
<blockquote>
<p><em>Don't prioritize the important over the urgent. With other stacks I was spending time on infrastructure decisions that felt important but weren't getting me closer to a product. Wasp let me focus on the urgent thing: shipping.</em></p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="youre-a-big-advocate-for-agentic-coding-how-does-wasp-fit-into-that-workflow">You're a big advocate for agentic coding. How does Wasp fit into that workflow?<a href="https://wasp.sh/blog/2026/03/29/hireveld-from-10-stacks-to-production-with-wasp#youre-a-big-advocate-for-agentic-coding-how-does-wasp-fit-into-that-workflow" class="hash-link" aria-label="Direct link to You're a big advocate for agentic coding. How does Wasp fit into that workflow?" title="Direct link to You're a big advocate for agentic coding. How does Wasp fit into that workflow?" translate="no">​</a></h2>
<p>This is where Wasp really shines, and honestly I think more people need to know about it. I've been building Hireveld almost entirely through agentic coding - Claude Code in the terminal - and after trying 10 different things, Wasp is by far the best experience for AI-assisted development.</p>
<p>Here's why: context is the precious commodity. Every line of code in your project takes a chunk of the model's context window. Wasp keeps the codebase tight and small.</p>
<p>The .wasp config file means the AI can understand your entire app's architecture at a glance - your routes, your auth setup, your jobs, your entities. Instead of the agent crawling through hundreds of files trying to figure out how things connect, it's all declared in one place. And because Wasp is opinionated and constrained, the agent doesn't try to do 50 different things. When something is wrong, the compiler screams. That tight feedback loop is exactly what agentic coding needs.</p>
<blockquote>
<p><em>Wasp respects the model's context length. It keeps things tidy. The constraint is the feature - it's what <strong>keeps both you and the AI from spiraling into a 20,000-line mess</strong>.</em></p>
</blockquote>
<p>I should say - I'm not blindly vibe coding. I know where my files are, I know my routes, I hand-edit the main.wasp file when I need to. I take testing seriously, both e2e and unit tests. QA is the layer where you, the human, decide what you actually want to build. But Wasp gives me the structure to stay at a high level and be productive, even as someone whose main language is Python. Also bring my data science background to bear by simulating data to gauge how the system would react to real traffic.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="you-also-contributed-back-to-wasp---tell-us-about-the-microsoft-auth-integration">You also contributed back to Wasp - tell us about the Microsoft Auth integration.<a href="https://wasp.sh/blog/2026/03/29/hireveld-from-10-stacks-to-production-with-wasp#you-also-contributed-back-to-wasp---tell-us-about-the-microsoft-auth-integration" class="hash-link" aria-label="Direct link to You also contributed back to Wasp - tell us about the Microsoft Auth integration." title="Direct link to You also contributed back to Wasp - tell us about the Microsoft Auth integration." translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Hireveld job search showing filters and a Junior Web Developer listing" src="https://wasp.sh/img/hireveld/hireveld-search.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Hireveld's job search interface</figcaption></figure></div>
<p>Hireveld targets the South African enterprise market, and enterprises run on Microsoft. They need Entra ID (Azure AD) for single sign-on - it's non-negotiable. When I started building, Wasp didn't have a Microsoft OAuth provider. With most frameworks, that would mean either paying a fortune for a third-party service or building a fragile custom integration that becomes tech debt.</p>
<p>But Wasp's codebase is approachable enough that I could build the provider myself and contribute it back. The PR process was great - Carlos and the team were welcoming and helpful. That's the sweet spot I was looking for: a framework that's batteries-included enough that I'm not rebuilding auth from scratch, but open enough that when I need something custom, I can add it without fighting the framework.</p>
<p>The community in general has been one of the best parts. The developers are genuinely friendly, my contributions felt valued, and I can tell the team takes agentic coding seriously - they maintain a Claude Code skill, they keep their prompts updated, they engage with the tooling ecosystem. That's an unusual level of involvement for a framework team.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-would-you-say-to-a-developer-considering-wasp-for-their-next-project">What would you say to a developer considering Wasp for their next project?<a href="https://wasp.sh/blog/2026/03/29/hireveld-from-10-stacks-to-production-with-wasp#what-would-you-say-to-a-developer-considering-wasp-for-their-next-project" class="hash-link" aria-label="Direct link to What would you say to a developer considering Wasp for their next project?" title="Direct link to What would you say to a developer considering Wasp for their next project?" translate="no">​</a></h2>
<p>If you're building a full-stack web app in 2026 and you're using AI tools to code - which you should be - try Wasp. Seriously. I went through PocketBase, Next.js, Nuxt, Svelte, Django, and more. Wasp is the only one where I felt like I was building my product from day one instead of fighting my tools.</p>
<p>It gave me auth, type-safe full-stack operations, background jobs, and transactional email - all wired together from a single config file. Everything else - the ranking algorithm, payments, file storage - I built on top of what Wasp provided. That separation is what made it possible to ship as a solo developer.</p>
<p>And if you're coming from Python or another ecosystem and you're intimidated by the JavaScript world - don't be. Wasp abstracts away enough of the complexity that you can stay at a high level and be productive. I'm proof of that.</p>
<blockquote>
<p><em>Wasp is the full-stack framework for the agentic era. It's the one that lets you focus on what you're building, not how you're building it.</em></p>
</blockquote>
<hr>
<p><em>Marcel Coetzee is a data scientist, AI consultant, and SaaS builder based in South Africa. You can find him on <a href="https://github.com/Marcel-Coetzee" target="_blank" rel="noopener noreferrer" class="">GitHub</a> and reach out to him on <a href="mailto:coetzee.marcel2@gmail.com" target="_blank" rel="noopener noreferrer" class="">coetzee.marcel2@gmail.com</a></em></p>]]></content:encoded>
            <author>matija@wasp-lang.dev (Matija Sosic)</author>
            <category>wasp</category>
            <category>ai</category>
            <category>user-interview</category>
            <category>fullstack</category>
        </item>
        <item>
            <title><![CDATA[Next.js vs Wasp: 40% Less Tokens for the Same App]]></title>
            <link>https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app</link>
            <guid>https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app</guid>
            <pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We gave Claude Code the same feature prompt for two identical SaaS apps: one built with Next.js, one with Wasp. The Wasp version cost 44% less and used 38% fewer tokens.]]></description>
            <content:encoded><![CDATA[<img src="https://wasp.sh/img/nextjs-wasp-tokens/nextjs-vs-wasp-tokens.webp">
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tldr">TL;DR<a href="https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app#tldr" class="hash-link" aria-label="Direct link to TL;DR" title="Direct link to TL;DR" translate="no">​</a></h2>
<p>We gave Claude Code the exact same feature prompt for two identical apps - one built with Next.js, the other built with Wasp - and measured everything that Claude Code did to implement the feature.</p>
<table><thead><tr><th>Metric</th><th>Wasp</th><th>Next.js</th><th>Wasp's Advantage</th></tr></thead><tbody><tr><td><strong>Total cost</strong></td><td>$2.87</td><td>$5.17</td><td>44% cheaper</td></tr><tr><td><strong>Total input &amp; output tokens</strong></td><td>2.5M</td><td>4.0M</td><td>38% fewer</td></tr><tr><td><strong>API calls</strong></td><td>66</td><td>96</td><td>31% fewer</td></tr><tr><td><strong>Tool uses</strong></td><td>52</td><td>66</td><td>21% fewer</td></tr><tr><td><strong>Files read</strong></td><td>12</td><td>15</td><td>Smaller blast radius</td></tr><tr><td>Output tokens (code written)</td><td>5,416</td><td>5,395</td><td>~same</td></tr></tbody></table>
<p>Not surprisingly, the savings were proportional to the amount of actual total code/tokens the Wasp framework abstracts away, which we also measured by running a static token count across both codebases. In this case, the Wasp version reduced total code/tokens by ~40%.</p>
<p><strong>Because the framework allows Claude Code to get the same work done in fewer tokens, it delivers a ~70% higher token efficiency (output per token).</strong></p>
<p>So, if you're using AI coding tools daily, your framework choice might be your single biggest lever for improving your AI's ability to generate accurate, complex code, quickly and cheaply.</p>
<div class="figure-container" style="display:flex;justify-content:flex-start"><figure style="margin:0 0 1rem 0"><img style="width:650px" alt="Wasp vs Next.js token efficiency" src="https://wasp.sh/img/nextjs-wasp-tokens/saas-wasp.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">We compared the two frameworks on the same feature prompt in the same app: Vercel's SaaS Starter.</figcaption></figure></div>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-wasp-actually-is">What Wasp Actually Is<a href="https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app#what-wasp-actually-is" class="hash-link" aria-label="Direct link to What Wasp Actually Is" title="Direct link to What Wasp Actually Is" translate="no">​</a></h2>
<p><a href="https://wasp.sh/" target="_blank" rel="noopener noreferrer" class="">Wasp</a> is a full-stack framework for React, Node.js, and Prisma with batteries-included. Think Rails or Laravel-like productivity for the JS ecosystem but where authentication, routing, operations, database, and cron jobs are defined declaratively as config.</p>
<p>You write business logic and Wasp handles the boilerplate and glue code for you.</p>
<p>Here's a simple example of a Wasp app's config file:</p>
<div class="code-snippet"><div class="code-snippet__header"><div class="code-snippet__dots"><span></span><span></span><span></span></div><span class="code-snippet__title">main.wasp.ts</span><div class="code-snippet__dots" style="visibility:hidden"><span></span><span></span><span></span></div></div><div class="code-snippet__body"><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> App </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'wasp-config'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> app </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">App</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'todoApp'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  title</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'ToDo App'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  wasp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> version</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'^0.21'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// head: []</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">auth</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  userEntity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> User</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  methods</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    google</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    email</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token operator" style="color:#393A34">...</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> mainPage </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">page</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'MainPage'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  component</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> importDefault</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'Main'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@src/pages/MainPage'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">route</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'RootRoute'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> path</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'/'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> to</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> mainPage </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">query</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'getTasks'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  fn</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">import</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'getTasks'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@src/queries'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'Task'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">job</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'taskReminderJob'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  executor</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'PgBoss'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  perform</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    fn</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">import</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'sendTaskReminder'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@src/workers/taskReminder'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  schedule</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> cron</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"*/5 * * * *"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  entities</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'Task'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token plain"> app</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div></div>
<p>Wasp's opinionated approach via its config gives AI tools (and developers) a big advantage: it acts as a large "specification" that both you and your AI coding agents already understand and agree on.</p>
<p>This gives AI one common pattern to follow, fewer decisions to make, less boilerplate to write, and fewer tools to stitch together, making the entire development process more reliable.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="show-dont-tell">Show, Don't Tell<a href="https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app#show-dont-tell" class="hash-link" aria-label="Direct link to Show, Don't Tell" title="Direct link to Show, Don't Tell" translate="no">​</a></h2>
<p>Before the numbers mean anything, here's how Wasp's "declarative config" compares to Next.js's equivalent.</p>
<p>In this example, we're comparing the fundamental auth setup from the <a href="https://github.com/vincanger/token-compare-nextjs-wasp" target="_blank" rel="noopener noreferrer" class="">actual test apps</a>. The first tab shows Wasp, and the second tab shows Next.js. Click between the tabs to see the difference.</p>
<div class="code-sbs"><div class="code-sbs__tabs"><button class="code-sbs__tab code-sbs__tab--active">Wasp</button><button class="code-sbs__tab ">Next.js</button></div><div class="code-sbs__grid"><div class="code-sbs__cell code-sbs__cell--active"><div class="code-snippet code-snippet--panel"><div class="code-snippet__header"><div class="code-snippet__dots"><span></span><span></span><span></span></div><span class="code-snippet__title">main.wasp.ts</span><div class="code-snippet__dots" style="visibility:hidden"><span></span><span></span><span></span></div></div><div class="code-snippet__body"><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">auth</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  userEntity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'User'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  methods</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    email</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      fromField</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'SaaS App'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        email</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'hello@example.com'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      emailVerification</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        clientRoute</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'EmailVerificationRoute'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      passwordReset</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        clientRoute</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'PasswordResetRoute'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  onAfterSignup</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">import</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'onAfterSignup'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@src/auth/hooks'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  onAuthFailedRedirectTo</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'/login'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  onAuthSucceededRedirectTo</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'/dashboard'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div></div></div><div class="code-sbs__cell "><div class="code-snippet code-snippet--panel"><div class="code-snippet__header"><div class="code-snippet__dots"><span></span><span></span><span></span></div><span class="code-snippet__title">lib/auth/session.ts + middleware.ts</span><div class="code-snippet__dots" style="visibility:hidden"><span></span><span></span><span></span></div></div><div class="code-snippet__body"><div class="language-typescript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-typescript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// lib/auth/session.ts</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> SignJWT</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> jwtVerify </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'jose'</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> cookies </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'next/headers'</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> key </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">TextEncoder</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">encode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">AUTH_SECRET</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">type</span><span class="token plain"> </span><span class="token class-name">SessionData</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  user</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  expires</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">signToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payload</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> SessionData</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">SignJWT</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setProtectedHeader</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> alg</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'HS256'</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setIssuedAt</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">setExpirationTime</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'1 day from now'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">sign</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">verifyToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> payload </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">jwtVerify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">input</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> algorithms</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'HS256'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> payload </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> SessionData</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setSession</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> NewUser</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> expiresInOneDay </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Date</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">24</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">60</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">60</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> session</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> SessionData </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    user</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> user</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">!</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    expires</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> expiresInOneDay</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toISOString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> encryptedSession </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">signToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">session</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">cookies</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'session'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> encryptedSession</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    expires</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> expiresInOneDay</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    httpOnly</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> secure</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sameSite</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'lax'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getSession</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> session </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">cookies</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'session'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">value</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">session</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">verifyToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">session</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">hashPassword</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">password</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">hash</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">password</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">SALT_ROUNDS</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">comparePasswords</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">plainText</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> hashed</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">compare</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">plainText</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> hashed</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// middleware.ts — route protection + token refresh</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> signToken</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> verifyToken </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'@/lib/auth/session'</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> protectedRoutes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'/dashboard'</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">middleware</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">request</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> NextRequest</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> pathname </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">nextUrl</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sessionCookie </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">cookies</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'session'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> isProtectedRoute </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pathname</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">startsWith</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">protectedRoutes</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">isProtectedRoute </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">sessionCookie</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> NextResponse</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">redirect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name constant" style="color:#36acaa">URL</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'/sign-in'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">url</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> res </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> NextResponse</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">next</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sessionCookie </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">method </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'GET'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> parsed </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">verifyToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sessionCookie</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">value</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> expiresInOneDay </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Date</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">24</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">60</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">60</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">cookies</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'session'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        value</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">signToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token operator" style="color:#393A34">...</span><span class="token plain">parsed</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          expires</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> expiresInOneDay</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">toISOString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        httpOnly</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> secure</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sameSite</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'lax'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        expires</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> expiresInOneDay</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">error</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">cookies</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'session'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">isProtectedRoute</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> NextResponse</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">redirect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name constant" style="color:#36acaa">URL</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'/sign-in'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">url</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div></div></div></div></div>
<p>As you can see, Wasp's config is much more concise and readable than Next.js's.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-for-ai">Why This Matters for AI<a href="https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app#why-this-matters-for-ai" class="hash-link" aria-label="Direct link to Why This Matters for AI" title="Direct link to Why This Matters for AI" translate="no">​</a></h2>
<p>Now that you can see the abstraction gap, here's why it compounds for AI:</p>
<ol>
<li class="">
<p><strong>Performance degrades as context window fills up</strong></p>
<p>AI tools are stateless and have a finite context window and <a href="https://www.trychroma.com/research/context-rot" target="_blank" rel="noopener noreferrer" class="">performance degrades long before it's completely full</a>. A larger codebase fills it faster, which means earlier message compression, less room for reasoning, and degraded output. Developers also have to maintain detailed memory and skill files to help AI understand the app structure and implementation expectations.</p>
</li>
<li class="">
<p><strong>Signal-to-noise ratio</strong></p>
<p>More tokens isn't just more expensive, it's more noise. Wasp's config is pure signal for AI, acting like a spec and concise map of the app the AI can follow. Next.js's equivalent is spread across route files, API handlers, middleware, and config. Higher noise = higher chance of mistakes.</p>
</li>
<li class="">
<p><strong>Every LLM call to the API re-reads the codebase</strong></p>
<p>AI tools don't remember between turns. Each turn re-reads the session conversation and codebase context from scratch. A bigger codebase means every single turn costs more. In our test, cache reads alone cost $1.71 with Next.js vs. $1.09 for Wasp.</p>
</li>
<li class="">
<p><strong>It compounds over a project's lifetime</strong></p>
<p>This test only measured one full-stack feature (db model, server operation, client page), and the differences were already significant (2.5M tokens vs. 4.0M tokens). In larger codebases, these differences quickly compound.</p>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-full-results">The Full Results<a href="https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app#the-full-results" class="hash-link" aria-label="Direct link to The Full Results" title="Direct link to The Full Results" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="combined-planning--implementation-phases">Combined: Planning + Implementation Phases<a href="https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app#combined-planning--implementation-phases" class="hash-link" aria-label="Direct link to Combined: Planning + Implementation Phases" title="Direct link to Combined: Planning + Implementation Phases" translate="no">​</a></h3>
<table><thead><tr><th></th><th>Wasp</th><th>Next.js</th><th>Delta</th></tr></thead><tbody><tr><td>Total cost</td><td>$2.87</td><td>$5.17</td><td>Next.js 80% more expensive</td></tr><tr><td>Total duration</td><td>14.9m</td><td>15.0m</td><td>Nearly identical</td></tr><tr><td>Total API calls</td><td>66</td><td>96</td><td>Next.js 45% more</td></tr><tr><td>Total tokens</td><td>2,505,796</td><td>4,049,413</td><td>Next.js 62% more</td></tr><tr><td>Total tool uses</td><td>52</td><td>66</td><td>Next.js 27% more</td></tr><tr><td>Subagents spawned</td><td>3</td><td>3</td><td>Same</td></tr><tr><td>Unique files read</td><td>12</td><td>15</td><td></td></tr><tr><td>Files edited</td><td>6</td><td>6</td><td>Same</td></tr><tr><td>Files created</td><td>2</td><td>3</td><td></td></tr><tr><td><strong>Token cost by category</strong></td><td></td><td></td><td></td></tr><tr><td>Input</td><td>$0.0005</td><td>$0.0801</td><td></td></tr><tr><td>Output</td><td>$0.2099</td><td>$0.2135</td><td>Nearly identical</td></tr><tr><td>Cache read</td><td>$1.0861</td><td>$1.7073</td><td>Next.js 57% more</td></tr><tr><td>Cache creation</td><td>$1.3230</td><td>$2.8184</td><td>Next.js 113% more</td></tr><tr><td>Subagent</td><td>$0.25</td><td>$0.35</td><td></td></tr></tbody></table>
<p><strong>Here are the main takeaways:</strong></p>
<ul>
<li class=""><strong>Output tokens</strong> (what the AI wrote): nearly identical — $0.21 vs $0.21</li>
<li class=""><strong>Cache creation</strong> (what the AI first loaded): Next.js 113% more — $2.82 vs $1.32</li>
<li class=""><strong>Cache read</strong> (what the AI re-read each turn): Next.js 57% more — $1.71 vs $1.09</li>
</ul>
<p><strong>Cache creation cost.</strong> By <a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching" target="_blank" rel="noopener noreferrer" class="">caching your prompts</a>, Claude optimizes your API usage by reusing prompts. But while reading from cache is cheap, creating new cache entries is still expensive. Next.js had 2.2x more cache creation tokens (343K vs 155K), at $6.25/M that's $2.15 vs $0.97. The bigger codebase means more new content being loaded into cache.</p>
<p><strong>Cache read cost.</strong> Each LLM call to the API re-reads the growing context. The Next.js codebase is bigger so each read costs more: $1.14 vs $0.67. Output tokens were nearly identical (~5,400), meaning the AI wrote roughly the same amount of code but had to read far more to do it.</p>
<p>The main differences comes from the Next.js codebase being bigger, meaning more tokens to load and re-read across every single API call to get the same output.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="context-efficiency-a-new-evaluation-metric">Context Efficiency: a New Evaluation Metric<a href="https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app#context-efficiency-a-new-evaluation-metric" class="hash-link" aria-label="Direct link to Context Efficiency: a New Evaluation Metric" title="Direct link to Context Efficiency: a New Evaluation Metric" translate="no">​</a></h2>
<p>The savings mirror what Wasp abstracts: authentication, routing, database management, operations, and jobs are defined in the config, so AI doesn't need to read, navigate, or generate layers of glue code.</p>
<p>And while this is a new approach in the framework space, Wasp is just following a general principle here: <strong>tools that make coding easier for humans make it easier for AI, too.</strong> They offload structural work and let the AI focus on business logic, giving a clearer path to generating complex code more accurately.</p>
<p>Think of it as a new evaluation metric for any framework: <strong>"context efficiency"</strong>, or how much of an AI's context window goes to signal vs. boilerplate. Add it alongside DX, performance, and ecosystem when choosing your stack in the AI-assisted coding era.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="methodology">Methodology<a href="https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app#methodology" class="hash-link" aria-label="Direct link to Methodology" title="Direct link to Methodology" translate="no">​</a></h2>
<p>First, we took Vercel's official SaaS starter app in Next.js and converted it to a Wasp (React, Node.js, Prisma) app.</p>
<p>Then, we made sure to use the same models (Opus for planning and implementation, Haiku for exploring), same framework-agnostic prompt, and same plan-then-implement flow, outlined in the <a href="https://github.com/vincanger/token-compare-nextjs-wasp/blob/main/code-generation-test.md" target="_blank" rel="noopener noreferrer" class="">test protocol</a>.</p>
<p>Finally, we created measurement scripts to pull metrics from Claude Code's detailed JSONL session transcripts.</p>
<p>We used <a href="https://www.anthropic.com/pricing" target="_blank" rel="noopener noreferrer" class="">Anthropic's API pricing</a> as of March 2026 (per 1M tokens), e.g.:</p>
<table><thead><tr><th></th><th>Input</th><th>Output</th><th>Cache Read</th><th>Cache Create</th></tr></thead><tbody><tr><td><strong>Opus 4.6</strong></td><td>$5.00</td><td>$25.00</td><td>$0.50</td><td>$6.25</td></tr></tbody></table>
<p><strong>Fairness caveats:</strong> Claude has seen far more Next.js training data (advantage: Next.js). Wasp's codebase is ~40% smaller, but that's the point. And this is a single feature test, not a comprehensive benchmark.</p>
<p><strong>Explore the comparison yourself:</strong> both apps, the test protocol, and measurement scripts are in the <a href="https://github.com/vincanger/token-compare-nextjs-wasp" target="_blank" rel="noopener noreferrer" class="">comparison repo</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-wasp">Try Wasp<a href="https://wasp.sh/blog/2026/03/26/nextjs-vs-wasp-40-percent-less-tokens-same-app#try-wasp" class="hash-link" aria-label="Direct link to Try Wasp" title="Direct link to Try Wasp" translate="no">​</a></h2>
<p><strong>Want to try Wasp?</strong> Get started with:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> i </span><span class="token parameter variable" style="color:#36acaa">-g</span><span class="token plain"> @wasp.sh/wasp-cli@latest</span><br></div></code></pre></div></div>
<p>Then start a new Wasp app:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">wasp new my-app</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">wasp start</span><br></div></code></pre></div></div>
<p>And don't forget to <a href="https://github.com/wasp-lang/wasp-agent-plugins/tree/main/plugins/wasp" target="_blank" rel="noopener noreferrer" class="">add the Wasp Agent Plugin / Skills</a> to turn your agent into a Wasp framework expert!</p>]]></content:encoded>
            <author>vince@wasp-lang.dev (Vince Canger)</author>
            <category>wasp</category>
            <category>ai</category>
            <category>vibe-coding</category>
            <category>fullstack</category>
            <category>nextjs</category>
        </item>
        <item>
            <title><![CDATA[A gentle intro to npm workspaces, with visuals]]></title>
            <link>https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces</link>
            <guid>https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces</guid>
            <pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A deep dive into how npm workspaces work under the hood: how package managers resolve shared dependencies across packages, how Node.js finds imports via symlinks, and when to use workspaces in your JavaScript projects.
]]></description>
            <content:encoded><![CDATA[<p>I am a founding engineer at <a class="" href="https://wasp.sh/">Wasp</a> - a full-stack TypeScript framework based on React, Node.js, and Prisma. Like Rails, but for JavaScript. A Wasp app is built as a multi-package setup - frontend, backend and shared code all live in a single repo as separate packages managed by npm workspaces.</p>
<p>Getting this right was harder than expected, and this post is what we learned. We'll dig into how npm workspaces actually work: how dependencies get resolved and hoisted, how Node.js finds modules at runtime, and help you build intuition/mental model that will come in useful when debugging this setup.</p>
<hr>
<p>Your side project started as a single folder with a hundred lines of code. Now it's a full-stack app with a React frontend, an Express backend, and a shared library of types and helpers that both sides need. You might be copy-pasting files between folders like it's 2005, and the cracks are showing: a type definition got updated in one place but not the other, and now your API responses don't match what the frontend expects. Something has to change.</p>
<!-- -->
<p>The first step would probably be splitting your code into multiple packages. But without the right tools, you'll quickly find yourself drowning in running npm install in five different folders, or fighting runtime errors because React is installed three times.To fix this, modern package managers introduced <strong>workspaces</strong>. Intended to allow you to manage multiple packages within a single repository without the dependency headaches, workspaces are widely used in many projects, but their inner workings are often misunderstood.</p>
<p>In this post, we'll dig into the magic that makes them tick: how package managers resolve shared dependencies across packages, and how Node.js finds them at runtime.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="definition">Enter workspaces<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#definition" class="hash-link" aria-label="Direct link to Enter workspaces" title="Direct link to Enter workspaces" translate="no">​</a></h2>
<p>Workspaces are a feature in npm, Yarn, pnpm, and others that let you organize multiple JavaScript packages in a single project. And while each package acts as its own encapsulated unit, they can still depend on one another with no extra ceremony.</p>
<div style="float:right;max-width:50%"><p><img decoding="async" loading="lazy" alt="Diagram comparing dependency management with and without workspaces. Without workspaces two separate projects each install their own copy of their dependencies, and are isolated from one another. With workspaces the same two projects share their common dependencies, can call each other." src="https://wasp.sh/assets/images/intro-62ed0375f6ce1b4b8ca12905b2a65cdb.png" width="1526" height="1448" class="img_ev3q"></p></div>
<p>Two main behaviors make this work: shared dependencies (if two packages depend on <code>react</code>, they both use the same installed version) and cross-package imports (a <code>pkg-a</code> can just import <code>pkg-b</code> by name).</p>
<p>The building blocks are interesting and require a dive into how package managers and Node.js work. The feature also comes after a long evolution of tooling and JS development practices, so let's start with some history.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-brief-history">A brief history<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#a-brief-history" class="hash-link" aria-label="Direct link to A brief history" title="Direct link to A brief history" translate="no">​</a></h2>
<p>It seems like the second half of the 2010s was when the professionalization of JS development started to trickle down to the average developer. The rise of <a href="https://babeljs.io/" target="_blank" rel="noopener noreferrer" class="">Babel</a> and <a href="https://webpack.js.org/" target="_blank" rel="noopener noreferrer" class="">Webpack</a> gave us a modern development experience, one that more closely resembled what other languages had enjoyed for years. We finally had a proper module system, and the ability to target newer language versions, while still supporting older runtimes.</p>
<p><img decoding="async" loading="lazy" src="https://wasp.sh/assets/images/timeline-1df44e79015fc28405342ed769263f3b.png" width="2179" height="617" class="img_ev3q"></p>
<p>Our history of workspaces starts in 2015, when the 6to5 project <a href="https://babeljs.io/blog/2015/02/15/not-born-to-die" target="_blank" rel="noopener noreferrer" class="">realized</a> they didn't want to work only on ES6 code, but also on everything that came after it. They renamed themselves to Babel and started working on <a href="https://babeljs.io/blog/2015/10/29/6.0.0#modularization" target="_blank" rel="noopener noreferrer" class="">modularizing the codebase</a>, knowing they'd need well-separated concerns to keep up with the ever-evolving language.</p>
<p>Babel maintainers wanted to distribute the new version as a collection of plugins you could pick and choose, but couldn't afford to manage each one independently. So when they split all their code into different packages, they did so in the shape of <a href="https://github.com/babel/babel/blob/main/doc/design/monorepo.md" target="_blank" rel="noopener noreferrer" class="">a monorepo</a>. That decision kickstarted the development of what would eventually be called workspaces.</p>
<p>Their internal tooling was soon released as <a href="https://github.com/lerna/lerna/blob/v1.0.0/README.md" target="_blank" rel="noopener noreferrer" class="">Lerna</a>. Lerna's main job was to allow packages in a monorepo to seamlessly <code>require()</code> one another as if they were already published, instead of having to edit, publish, and reinstall each one independently.</p>
<p>A few months later in 2017, <a href="https://classic.yarnpkg.com/blog/2016/10/11/introducing-yarn/" target="_blank" rel="noopener noreferrer" class="">Yarn</a> came along. Yarn was created to solve some problematic behaviors of npm at the time, and dramatically improved performance and reliability. Alongside features like reproducible lockfiles and heavy caching, Yarn also introduced <a href="https://classic.yarnpkg.com/blog/2017/08/02/introducing-workspaces/" target="_blank" rel="noopener noreferrer" class="">workspaces as a first-class citizen</a>. This was no surprise, as Yarn was created by some of the same people who had worked on Babel and Lerna.</p>
<p>Soon, Yarn exploded in popularity, and in time, other package managers followed suit. pnpm added workspaces support in 2018, and npm followed in 2020. Nowadays, after a long evolution, workspaces are supported in all the mainstream package managers and widely used in both open and closed source projects.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="problems-and-solutions">Problems and solutions<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#problems-and-solutions" class="hash-link" aria-label="Direct link to Problems and solutions" title="Direct link to Problems and solutions" translate="no">​</a></h2>
<p>The exact definition of what a workspace is depends on the package manager you're using. <a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#definition" class="">The definition I gave above</a> is more of an observation of what the main package managers have in common than a prescriptive specification. There isn't a common "workspaces spec". Each package manager has its own take, with different configuration formats, commands, and even different ways to declare dependencies.</p>
<p>That said, there's a core of functionality you can expect across package managers: you declare a root folder that contains multiple sub-packages. Each sub-package is called a <em>workspace</em>, and the whole set is a <em>project</em><sup><a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#user-content-fn-yarn-glossary-f1ae0a" id="user-content-fnref-yarn-glossary-f1ae0a" data-footnote-ref="true" aria-describedby="footnote-label" class="anchorTargetStickyNavbar_Vzrq">1</a></sup>. From there, three features usually become available:</p>
<ul>
<li class="">
<p><strong>Each workspace is its own full package</strong>, as if it were an independent project. Each workspace has its own <code>package.json</code>, where it declares dependencies, configuration, and scripts without worrying about the insides of other packages.</p>
</li>
<li class="">
<p><strong>All dependencies are resolved together</strong> across the project, as if it were a single package. If two workspaces depend on the same package, it gets installed once and shared (assuming compatible version ranges).</p>
</li>
<li class="">
<p><strong>Workspaces can depend on each other by name</strong>, as if they were regular dependencies. In a project with many workspaces, <code>pkg-a</code> can import <code>pkg-b</code> and everything works.</p>
</li>
</ul>
<p>Workspaces give us the best of both worlds: the encapsulation of small packages with the convenience of shared dependencies.</p>
<p>The first feature is just the natural way of working with multiple packages, so it doesn't require much explanation. Let's look at the other two: why they're useful and how they're implemented. The exact implementation details depend on the package manager, so we'll use npm as a reference here, since it's the most widely used. The general concepts apply across the board.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-dependencies-of-the-whole-project-are-resolved-together">The dependencies of the whole project are resolved together<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#the-dependencies-of-the-whole-project-are-resolved-together" class="hash-link" aria-label="Direct link to The dependencies of the whole project are resolved together" title="Direct link to The dependencies of the whole project are resolved together" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem">The problem<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#the-problem" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h4>
<p>Imagine testing a project with many interconnected packages ...say, <a href="https://github.com/babel/babel/tree/main/packages" target="_blank" rel="noopener noreferrer" class="">157 of them</a>. You'd have to find out which packages depend on which others, run <code>npm install</code> in each folder, and deal with the ongoing headache of keeping dependency versions in sync. Worse: if each package had its own <code>node_modules</code>, you'd end up with tons of copies of the same dependencies installed in different places.</p>
<p>Multiple copies aren't just a waste of disk space, they can cause bugs. In JavaScript, the same class defined in two different files is considered <em>different</em>, even if the code is identical<sup><a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#user-content-fn-react-aside-f1ae0a" id="user-content-fnref-react-aside-f1ae0a" data-footnote-ref="true" aria-describedby="footnote-label" class="anchorTargetStickyNavbar_Vzrq">2</a></sup>:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockTitle_OeMC">foo/my-class.js</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token maybe-class-name">MyClass</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockTitle_OeMC">bar/my-class.js</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token maybe-class-name">MyClass</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockTitle_OeMC">main.js</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports operator" style="color:#393A34">*</span><span class="token imports"> </span><span class="token imports keyword module" style="color:#00009f">as</span><span class="token imports"> foo</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./foo/my-class"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports operator" style="color:#393A34">*</span><span class="token imports"> </span><span class="token imports keyword module" style="color:#00009f">as</span><span class="token imports"> bar</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"./bar/my-class"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> foo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access maybe-class-name" style="color:#d73a49">MyClass</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">instanceof</span><span class="token plain"> foo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">MyClass</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// true</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> foo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access maybe-class-name" style="color:#d73a49">MyClass</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">instanceof</span><span class="token plain"> bar</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">MyClass</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// false!</span><br></div></code></pre></div></div>
<p>For shared dependencies to work correctly, they need to be installed once, in a single place. That way, when different pieces of code import the same dependency, they get the same object.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-solution">The solution<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#the-solution" class="hash-link" aria-label="Direct link to The solution" title="Direct link to The solution" translate="no">​</a></h4>
<p>Most package managers already deduplicate dependencies for a single project. So to get this behaviour for free with workspaces, they cheat a bit: they treat all workspaces as dependencies of a single, "fake" root package. The existing resolution algorithm gets reused without changes.</p>
<p>You can see this in any workspaces lockfile. Here's a simplified example for a regular package with no workspaces:</p>
<div class="theme-tabs-container tabs-container tabList__CuJ"><ul role="tablist" aria-orientation="horizontal" class="tabs"><li role="tab" tabindex="0" aria-selected="true" class="tabs__item tabItem_LNqP tabs__item--active">Graph</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">Lockfile</li></ul><div class="margin-top--md"><div role="tabpanel" class="tabItem_Ymn6"></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">"my-simple-package"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">dependencies</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">left-pad</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ^1.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">right-pad</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ^1.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">"left-pad@^1.0.0"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">resolved</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 1.1.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">dependencies</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">core-pad</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ^1.1.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">"right-pad@^1.0.0"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">resolved</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 1.4.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">dependencies</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">core-pad</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ^1.4.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">"core-pad@^1.1.0,^1.4.0"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">resolved</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 1.4.5</span><br></div></code></pre></div></div></div></div></div>
<p>The package depends on both <code>left-pad</code> and <code>right-pad</code>, which both depend on <code>core-pad</code> with different version ranges. The package manager found a single version of <code>core-pad</code> that satisfies both and installed it once.</p>
<p>Here's the same scenario in a workspaces project:</p>
<div class="theme-tabs-container tabs-container tabList__CuJ"><ul role="tablist" aria-orientation="horizontal" class="tabs"><li role="tab" tabindex="0" aria-selected="true" class="tabs__item tabItem_LNqP tabs__item--active">Graph</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">Lockfile</li></ul><div class="margin-top--md"><div role="tabpanel" class="tabItem_Ymn6"></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">"##fake-project-root##"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">dependencies</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">workspace-a</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> local</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">workspace-b</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> local</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">"workspace-a@local"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">dependencies</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">left-pad</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ^1.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">"workspace-b@local"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">dependencies</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">right-pad</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ^1.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">"left-pad@^1.0.0"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">resolved</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 1.1.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">dependencies</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">core-pad</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ^1.1.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">"right-pad@^1.0.0"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">resolved</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 1.4.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">dependencies</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">core-pad</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ^1.4.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">"core-pad@^1.1.0,^1.4.0"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">resolved</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 1.4.5</span><br></div></code></pre></div></div></div></div></div>
<p>By pretending all workspaces are dependencies of a single root package, the package manager reuses its existing algorithm. It finds one version of <code>core-pad</code> that works for both, installs it once, and any workspace that imports it gets the same object.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="workspaces-can-depend-on-each-other-by-name">Workspaces can depend on each other by name<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#workspaces-can-depend-on-each-other-by-name" class="hash-link" aria-label="Direct link to Workspaces can depend on each other by name" title="Direct link to Workspaces can depend on each other by name" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem-1">The problem<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#the-problem-1" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h4>
<p>If you have two separate packages and want one to depend on the other, you have two options:</p>
<ul>
<li class="">
<p><strong>Publish the dependency</strong> to the registry, then install it in the other package. Straightforward, but if you're iterating quickly, the publish-install cycle kills your flow.</p>
</li>
<li class="">
<p><strong>Use a local file dependency.</strong> Edit the imports to point to the local path. This works, but it's brittle: if you move packages around or share the code with someone else, paths break. If the dependency has its own dependencies, you have to install them manually. And you need to remember to switch paths back before publishing.</p>
</li>
</ul>
<p>Neither is great. What you actually want is to say "my package depends on <code>pkg-b</code>" and have the package manager figure out that <code>pkg-b</code> is just in the folder next to you. That's exactly what workspaces do!</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-solution-1">The solution<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#the-solution-1" class="hash-link" aria-label="Direct link to The solution" title="Direct link to The solution" translate="no">​</a></h4>
<p>This seems straightforward to implement. The package manager knows where all workspaces are, so it could point to the right folder when it finds a matching dependency name. But there's a catch: the dependency resolution of the package manager (npm, Yarn) is separate from the import resolution of the runtime (Node.js).</p>
<p>If you've ever looked into your <code>node_modules</code> and found dependencies nested inside other dependencies instead of a flat structure, this is why. The package manager has no influence on how Node.js finds imports<sup><a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#user-content-fn-integrated-package-manager-f1ae0a" id="user-content-fnref-integrated-package-manager-f1ae0a" data-footnote-ref="true" aria-describedby="footnote-label" class="anchorTargetStickyNavbar_Vzrq">3</a></sup>, all it can do is lay them out on disk in a way that Node.js will understand.</p>
<h5 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-nodejs-finds-packages">How Node.js finds packages<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#how-nodejs-finds-packages" class="hash-link" aria-label="Direct link to How Node.js finds packages" title="Direct link to How Node.js finds packages" translate="no">​</a></h5>
<p>The high-level idea is simple:</p>
<ul>
<li class="">If the import is a relative or absolute path (<code>./</code>, <code>../</code>, or <code>/</code>), Node.js follows that path directly.</li>
<li class="">Otherwise, it looks for the import name in the nearest <code>node_modules</code> folder, starting from the current file's directory, walking up the folder tree until it finds a match or reaches the filesystem root.</li>
</ul>
<p>For example, if we want to import e.g. <code>left-pad</code>, Node.js checks these folders in order:</p>
<ul>
<li class="">From <code><b>~/projects/app/src/</b>utils.js</code></li>
</ul>
<ol>
<li class="">
<code><b>~/projects/app/src/</b>node_modules/left-pad</code>
</li>
<li class="">
<code><b>~/projects/app/</b>node_modules/left-pad</code>
</li>
<li class="">
<code><b>~/projects/</b>node_modules/left-pad</code>
</li>
<li class="">
<code><b>~/</b>node_modules/left-pad</code>
</li>
</ol>
<h5 class="anchor anchorTargetStickyNavbar_Vzrq" id="symlinks-tie-it-all-together">Symlinks tie it all together<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#symlinks-tie-it-all-together" class="hash-link" aria-label="Direct link to Symlinks tie it all together" title="Direct link to Symlinks tie it all together" translate="no">​</a></h5>
<p>We know we don't want multiple copies of the same workspace in different <code>node_modules</code> folders, because that would bring back all the problems we discussed. Instead, we can use <a href="https://en.wikipedia.org/wiki/Symbolic_link" target="_blank" rel="noopener noreferrer" class="">symlinks</a>. As a refresher: they are special files that just "redirect" to another file or folder when accessed.</p>
<p>So now, this just becomes a problem of how to arrange the symlinks in the <code>node_modules</code> folders for Node.js. And the solution: we'll create a symlink to each workspace in the top-level <code>node_modules</code> folder. Since Node.js walks up the directory tree looking for that folder, every workspace will eventually find the root one:</p>
<ul>
<li class=""><code>package-root/</code>
<ul>
<li class=""><code>node_modules/</code>
<ul>
<li class=""><code>sub-a</code> <em>(symlink to <code>sub-a</code>)</em></li>
<li class=""><code>sub-b</code> <em>(symlink to <code>sub-b</code>)</em></li>
<li class=""><code>sub-c</code> <em>(symlink to <code>sub-c</code>)</em></li>
</ul>
</li>
<li class=""><code>sub-a/</code>
<ul>
<li class=""><code>package.json</code></li>
<li class="">...</li>
</ul>
</li>
<li class=""><code>sub-b/</code>
<ul>
<li class=""><code>package.json</code></li>
<li class="">...</li>
</ul>
</li>
<li class=""><code>sub-c/</code>
<ul>
<li class=""><code>package.json</code></li>
<li class="">...</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>Here's an example:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockTitle_OeMC">package-root/sub-a/index.js</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> foo </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sub-b"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">foo</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockTitle_OeMC">package-root/sub-b/index.js</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">export</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">foo</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Hello from sub-b!"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>How does Node.js resolve the import in <code>sub-a/index.js</code>?</p>
<ul>
<li class=""><code>package-root/sub-a/node_modules/sub-b</code><br>
<!-- -->→ doesn't exist</li>
<li class=""><code>package-root/node_modules/sub-b</code><br>
<!-- -->→ symlink to <code>package-root/sub-b/</code>, follow it</li>
<li class=""><code>package-root/sub-b</code><br>
<!-- -->→ found it!</li>
</ul>
<p>By placing symlinks in the top-level <code>node_modules</code>, any workspace can import any other workspace by name, and Node.js finds it correctly.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-not-to-use-workspaces">When (not) to use workspaces<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#when-not-to-use-workspaces" class="hash-link" aria-label="Direct link to When (not) to use workspaces" title="Direct link to When (not) to use workspaces" translate="no">​</a></h2>
<p>While workspaces are pretty useful, they are not a silver bullet. That is, you shouldn't go link every project in your <code>~/dev</code> folder just yet. Instead, they should be treated as a scoped feature, with a clear sweet spot.</p>
<p>In general, workspaces work best for projects that:</p>
<ul>
<li class="">Consist of multiple packages</li>
<li class="">Will be developed in tandem</li>
<li class="">Share many dependencies</li>
<li class="">Call each other frequently</li>
</ul>
<p>That's why workspaces are mostly used in monorepos: almost by definition, they satisfy all of these criteria. Wasp uses them outside the monorepo context, but that's because we generate packages for each part of your app, that are tightly coupled and evolve together.</p>
<p>On the other hand, I'd discourage using workspaces when:</p>
<ul>
<li class="">You don't have a good reason to split a single codebase or repo into multiple packages. Workspaces solve problems, but it's better if you don't have those problems in the first place.</li>
<li class="">Your packages are mostly independent and don't share many dependencies. I wouldn't use workspaces to link all the random libraries in my <code>~/dev</code> folder. It could lead to random interference between dependencies, or implicit relationships.</li>
<li class="">You have cyclic dependencies. Without a clear story of which packages depend on which<sup><a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#user-content-fn-recommend-yarn-f1ae0a" id="user-content-fnref-recommend-yarn-f1ae0a" data-footnote-ref="true" aria-describedby="footnote-label" class="anchorTargetStickyNavbar_Vzrq">4</a></sup>, workspaces can allow any package depend on any other carelessly, and that can lead to a tangled web of dependencies.</li>
<li class="">Your project is split across multiple repositories. Workspaces would make them no longer self-contained. Unless you're very sure of your use case, either unlink the repos altogether or merge them into a single monorepo.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-does-wasp-care-about-workspaces">Why does Wasp care about workspaces?<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#why-does-wasp-care-about-workspaces" class="hash-link" aria-label="Direct link to Why does Wasp care about workspaces?" title="Direct link to Why does Wasp care about workspaces?" translate="no">​</a></h2>
<p>When you run the Wasp compiler, it generates app code into a <code>.wasp/</code> folder inside your project. And part of that generated code is three different packages: one for the SDK we generate from your Wasp spec, and a frontend and backend package that contain the final codebase for your deployed apps.</p>
<p>Those three packages are tightly coupled, with frequent imports across packages, and also to your own code. They also share many dependencies, and we want to avoid any chance of mismatches or duplication between them.</p>
<p>As part of our push for Wasp 1.0, we wanted to make this situation easier to work with, and we realized workspaces were a perfect fit for our problems. We worked on a project internally called <a href="https://github.com/wasp-lang/wasp/issues/3119" target="_blank" rel="noopener noreferrer" class="">Wasp Citizen</a>, so we could capitalize on workspaces to ease our dependency mismatch issues.</p>
<p>Since Wasp v0.19, this is at work in every Wasp project. Your app still works as a single-package project and you don't need to learn anything new; but internally, the generated packages are structured as workspaces. This translates to a better experience for you, fewer weird bugs for us, and quicker installs for everyone.</p>
<p>And with this knowledge, if you want, now you can add your own workspaces to your Wasp project too, and have them play nicely with the generated ones!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-off">Go off!<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#go-off" class="hash-link" aria-label="Direct link to Go off!" title="Direct link to Go off!" translate="no">​</a></h2>
<p>Workspaces are a powerful tool that help you organize your code, avoid dependency errors, and speed up your development workflow. I hope this post has given you a good understanding of how they work under the hood and when to use them. If you find yourself in a situation where workspaces could help you, give them a try!</p>
<p>Until next time, happy coding!</p>
<hr>
<!-- -->
<!-- -->
<section data-footnotes="true" class="footnotes"><h2 class="anchor anchorTargetStickyNavbar_Vzrq sr-only" id="footnote-label">Footnotes<a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#footnote-label" class="hash-link" aria-label="Direct link to Footnotes" title="Direct link to Footnotes" translate="no">​</a></h2>
<ol>
<li class="anchorTargetStickyNavbar_Vzrq" id="user-content-fn-yarn-glossary-f1ae0a">
<p>Yarn has a great <a href="https://yarnpkg.com/advanced/lexicon" target="_blank" rel="noopener noreferrer" class="">glossary</a> that you can check out if you want to get specific definitions for a package-manager-related term, and pointers to which feature they're a part of. <a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#user-content-fnref-yarn-glossary-f1ae0a" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li class="anchorTargetStickyNavbar_Vzrq" id="user-content-fn-react-aside-f1ae0a">
<p>This is one of the reasons React requires a single copy in your app. React relies on a single instance to track internal state (hooks, context, and reconciler). If you had two copies, components created with one wouldn't be recognized by the other, leading to cryptic errors. <a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#user-content-fnref-react-aside-f1ae0a" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
<li class="anchorTargetStickyNavbar_Vzrq" id="user-content-fn-integrated-package-manager-f1ae0a">
<p>Some newer runtimes have the package manager integrated directly into execution, so they can control both installation and resolution, and simply point to the right folder. For example, Deno doesn't even install dependencies in your project folder. Instead, it keeps a single global copy in an internal cache that it can programmatically reference. <a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#user-content-fnref-integrated-package-manager-f1ae0a" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref">↩</a></p>
</li>
<li class="anchorTargetStickyNavbar_Vzrq" id="user-content-fn-recommend-yarn-f1ae0a">
<p>Yarn has some great features that force you to be exhaustive about how your workspaces are structured. By default, you have to <a href="https://yarnpkg.com/features/workspaces#cross-references" target="_blank" rel="noopener noreferrer" class="">explicitly declare which other workspaces you depend on</a>. They also have a <a href="https://yarnpkg.com/features/constraints" target="_blank" rel="noopener noreferrer" class="">workspace constraints feature</a> that lets you enforce rules on which packages can depend on which others. This one has a steep learning curve, but it's worth it if you're working on a very big monorepo. <a href="https://wasp.sh/blog/2026/03/25/gentle-intro-npm-workspaces#user-content-fnref-recommend-yarn-f1ae0a" data-footnote-backref="" aria-label="Back to reference 4" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <category>wasp</category>
            <category>inside-wasp</category>
            <category>open-source</category>
            <category>wasp-citizen</category>
        </item>
        <item>
            <title><![CDATA[Claude Code for Fullstack Development: The 3 Things You Actually Need]]></title>
            <link>https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials</link>
            <guid>https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials</guid>
            <pubDate>Thu, 29 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Claude Code for fullstack development doesn't require complex workflows. Three essentials -— full-stack debugging visibility, LLM-friendly docs, and an opinionated framework -— are all you need.]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tldr">TL;DR<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#tldr" class="hash-link" aria-label="Direct link to TL;DR" title="Direct link to TL;DR" translate="no">​</a></h2>
<p>Claude Code for fullstack development doesn't require complex workflows. Three essentials are all you need:</p>
<ol>
<li class=""><strong>full-stack debugging visibility</strong> via background tasks and browser automation tools like Chrome DevTools MCP,</li>
<li class=""><strong>LLM-friendly documentation access</strong> via llms.txt (10x more context-efficient than MCP servers), and</li>
<li class=""><strong>an opinionated fullstack framework</strong> like Wasp that reduces boilerplate by 60-80% so AI can build more complex apps with greater accuracy.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-can-i-use-claude-code-effectively">How Can I Use Claude Code Effectively?<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#how-can-i-use-claude-code-effectively" class="hash-link" aria-label="Direct link to How Can I Use Claude Code Effectively?" title="Direct link to How Can I Use Claude Code Effectively?" translate="no">​</a></h2>
<p>There's a lot of hype around vibe coding with Claude Code.</p>
<p>The good news is that it's warranted. Claude Code can take care of some <a href="https://www.anthropic.com/news/claude-opus-4-5" target="_blank" rel="noopener noreferrer" class="">surprisingly complex coding tasks</a>.</p>
<p>The bad news is that it's over-hyped, with claims of amazing apps being vibe coded in a couple hours, or complex workflows that use 10 parallel sub-agents running in a loop to replace the work of 5 software engineers.</p>
<p>If you're not already a Claude Code power user, all this hype can leave you questioning how to use it effectively. <em>Am I missing out on productivity gains by not adopting this insane new workflow? Should I be using subagents, commands, skills, or MCP servers for my use case? If so, how?</em></p>
<p>I was asking myself these exact questions, so I did a good few weeks of research and testing, and reached the following conclusion:</p>
<p><strong>With just a few well-picked tools, and Claude Code's basic features, you have enough to "vibe code" great full-stack apps.</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="you-dont-need-all-its-features">You Don't Need All its Features<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#you-dont-need-all-its-features" class="hash-link" aria-label="Direct link to You Don't Need All its Features" title="Direct link to You Don't Need All its Features" translate="no">​</a></h3>
<blockquote>
<p><em>Weird seeing all these complex LLM workflows and tools. Meanwhile I run one continuous Claude convo per project... I've never used a subagent. Never used MCP…
And I have wildly good results ¯_(ツ)_/¯</em><br>
<!-- -->— <a href="https://x.com/chris_mccord/status/2016554559078883510" target="_blank" rel="noopener noreferrer" class="">Chris McCord, creator of the Phoenix framework</a></p>
</blockquote>
<p>While Claude Code's abilities are impressive, you quickly realize that there is a lot of feature overlap and stuff you just generally won't have to touch often if you get a few things right from the start. I'll explain what these are, and then go into more detail on each topic within this article.</p>
<!-- -->
<p><strong>1. Provide it with full-stack debugging visibility</strong></p>
<ul>
<li class="">This allows Claude to actually see and respond to what it's coding, rather than us having to copy-paste errors, or describe issues.</li>
</ul>
<p><strong>2. Give Claude Code access to up-to-date, LLM-friendly documentation</strong></p>
<ul>
<li class="">This is crucial when working with LLMs that may have outdated knowledge, hallucinate solutions, or get thrown off by "noisy" documentation that's not optimized for LLMs.</li>
</ul>
<p><strong>3. Use the right tech stack or framework</strong></p>
<ul>
<li class="">This is probably the most overlooked of these three approaches. By picking the right framework, you give AI clear patterns to follow, and remove a lot of complexity right from the start.</li>
</ul>
<p>With this foundation, you'll be able to build and deploy fullstack apps easily by mostly using Claude Code's default workflows, and just a couple custom commands/skills (I also packed these ideas into a simple Claude Code plugin you can install and use, but I'll talk more about that later).</p>
<p>This approach works because it provides guardrails and the right patterns for the agent to follow, so you can spend more time on business logic implementation, and less time working out specifications and technical details.</p>
<p>Essentially, you get to work with the agent on <em>what</em> you want, rather than having to explain <em>how</em> you want it, bringing that magic feeling of AI-assisted coding to complex, full-stack apps.</p>
<p>Let's dive in.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="giving-claude-code-full-stack-vision">Giving Claude Code Full-Stack Vision<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#giving-claude-code-full-stack-vision" class="hash-link" aria-label="Direct link to Giving Claude Code Full-Stack Vision" title="Direct link to Giving Claude Code Full-Stack Vision" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="establishing-a-tight-feedback-loop">Establishing a Tight Feedback Loop<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#establishing-a-tight-feedback-loop" class="hash-link" aria-label="Direct link to Establishing a Tight Feedback Loop" title="Direct link to Establishing a Tight Feedback Loop" translate="no">​</a></h3>
<p>Our typical AI-assisted coding workflow tends to look like this: we prompt, then wait, then look over the generated code (maybe), then check on things in the browser. If we've got some errors or some bad looking frontend designs, we copy and paste them and try to (better) explain what we want.</p>
<div style="display:flex;justify-content:center;margin:1rem 0"><figure style="width:100%;margin:0"><video width="100%" controls="" aria-label="An AI-assisted coding workflow without full-stack vision"><source src="/img/claude-code-fullstack/workflow-without-vision.mp4" type="video/mp4"></video><figcaption class="image-caption" style="font-style:italic;opacity:0.6;font-size:0.9rem">An AI-assisted coding workflow without 'full-stack vision'.</figcaption></figure></div>
<p>What's worse is we might have to do this a few times before we're satisfied, or get things working again.</p>
<p>By always keeping ourselves in the loop, <a href="https://www.anthropic.com/research/estimating-productivity-gains" target="_blank" rel="noopener noreferrer" class="">we're slowing things down a lot</a>. We should, at times, just get out of the way and let the agent improve the code with each iteration until its done, before checking the result.</p>
<p>But to do this, we need to give Claude Code a "set of eyes". Luckily, this is possible with the right features and tools, allowing it to see the results of the code it wrote, and quickly modify it autonomously if it encounters an error anywhere in the stack.</p>
<p>Let's check out how.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="running-the-dev-server-as-a-background-task">Running the Dev Server as a Background Task<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#running-the-dev-server-as-a-background-task" class="hash-link" aria-label="Direct link to Running the Dev Server as a Background Task" title="Direct link to Running the Dev Server as a Background Task" translate="no">​</a></h3>
<p>Claude Code introduced their <a href="https://code.claude.com/docs/en/interactive-mode#background-bash-commands" target="_blank" rel="noopener noreferrer" class="">background tasks feature</a> which lets it execute long-running tasks, like app development servers (e.g. <code>npm run dev</code>), on the side without blocking Claude's progress on other work. The nice part is that Claude can continue to read and respond to the output of this task while you work.</p>
<div style="display:flex;justify-content:center;margin:1rem 0"><figure style="width:100%;margin:0"><video width="100%" controls="" aria-label="Running the dev server as a background task"><source src="/img/claude-code-fullstack/background-tasks-demo.mp4" type="video/mp4"></video><figcaption class="image-caption" style="font-style:italic;opacity:0.6;font-size:0.9rem">Running the dev server as a background task.</figcaption></figure></div>
<p>To run commands in the background, you can either:</p>
<ul>
<li class="">Prompt Claude Code to run a command in the background, e.g. <code>"run my app's dev server in the background"</code></li>
<li class="">Press Ctrl+B to move a regular Bash tool invocation to the background, e.g. <code>"run the dev server"</code> + <code>Ctrl+b</code> as it starts</li>
</ul>
<p>Even though Claude can read the output of your background tasks, there are times you may want to check up on them yourself, and you still can do that. Just use the down arrow to highlight the background task message and click enter.</p>
<p>This is great because now Claude can react to issues that occur from building and serving your code. Unfortunately, it still can't react to errors that occur while your app is running in the browser.</p>
<p>But there's a cool way to solve this.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="browser-automation-tools">Browser Automation Tools<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#browser-automation-tools" class="hash-link" aria-label="Direct link to Browser Automation Tools" title="Direct link to Browser Automation Tools" translate="no">​</a></h3>
<p>The missing piece so far is giving Claude the ability to actually <em>see</em> the result of the code it wrote, most typically what the UI looks like.</p>
<p>Problems that only show up <strong>after the app is running in the browser</strong>, like design issues and runtime errors, aren't yet visible to the agent. So this tends to be where the human steps in to report back to: <em>"the button is misaligned"</em>, <em>"there's a 404 error when trying to login"</em>, <em>"the console says something about <code>undefined</code>"</em>.</p>
<p>But luckily, we can arm Claude Code with browser automation tools to solve for this. These tools allow for programmatic control of the browser, like loading pages, clicking buttons, inspecting elements, reading console logs, and even taking screenshots.</p>
<div style="display:flex;justify-content:center;margin:1rem 0"><figure style="width:100%;margin:0"><video width="100%" controls="" aria-label="Logging in and adding a task with browser automation"><source src="/img/claude-code-fullstack/browser-automation-login.mp4" type="video/mp4"></video><figcaption class="image-caption" style="font-style:italic;opacity:0.6;font-size:0.9rem">Logging in and adding a task with browser automation.</figcaption></figure></div>
<p>This closes the loop for the <em>entire</em> stack and gives Claude the autonomy to complete larger features tasks entirely on its own.</p>
<p><a href="https://developer.chrome.com/blog/chrome-devtools-mcp" target="_blank" rel="noopener noreferrer" class="">Chrome DevTools MCP server</a> is one of the best options currently out there, though there are many alternatives. It's easy to install, and specializes in browser debugging and performance insights.</p>
<p>To install it, run the following command in the terminal to add it to your current project:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">claude mcp </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> chrome-devtools </span><span class="token parameter variable" style="color:#36acaa">--scope</span><span class="token plain"> project npx chrome-devtools-mcp@latest</span><br></div></code></pre></div></div>
<p>Then start a new Claude session and give it a prompt like:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Verify in the browser that your change works as expected.</span><br></div></code></pre></div></div>
<p>You should see a separate Chrome instance open up being controlled by Claude. Go ahead and give it more tasks like:</p>
<ul>
<li class="">authenticating a test user</li>
<li class="">checking the lighthouse performance score of a site (e.g. how fast it loads)</li>
<li class="">giving you feedback on how to improve your app's design</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="tying-it-all-together">Tying it All Together<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#tying-it-all-together" class="hash-link" aria-label="Direct link to Tying it All Together" title="Direct link to Tying it All Together" translate="no">​</a></h3>
<div style="display:flex;justify-content:center;margin:1rem 0"><figure style="width:100%;margin:0"><video width="100%" controls="" aria-label="Adding a full-stack subtasks feature with full-stack vision workflow"><source src="/img/claude-code-fullstack/subtasks-fullstack-demo.mp4" type="video/mp4"></video><figcaption class="image-caption" style="font-style:italic;opacity:0.6;font-size:0.9rem">Adding a full-stack 'subtasks' feature with our 'full-stack vision' workflow.</figcaption></figure></div>
<p>Now, when you're implementing full-stack features in your app, ask Claude to verify the feature works correctly by checking the logs in the development server, as well as in the browser with the Chrome DevTools MCP.</p>
<p>Alternatively, if you want Claude to <em>always</em> automatically verify changes in the browser with the DevTools MCP without having to explicitly ask it to do so, you can <a href="https://code.claude.com/docs/en/memory#set-up-project-memory" target="_blank" rel="noopener noreferrer" class="">add a rule to Claude's memory in your <code>CLAUDE.md</code> file</a>.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="give-the-agent-access-to-the-right-docs">Give the Agent Access to the Right Docs<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#give-the-agent-access-to-the-right-docs" class="hash-link" aria-label="Direct link to Give the Agent Access to the Right Docs" title="Direct link to Give the Agent Access to the Right Docs" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-versioning-problem">The Versioning Problem<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#the-versioning-problem" class="hash-link" aria-label="Direct link to The Versioning Problem" title="Direct link to The Versioning Problem" translate="no">​</a></h3>
<p>You've probably experienced this: you're vibe coding using a library's API, and the AI confidently writes code that would have worked perfectly… two versions ago. Or it creates some crazy, over-engineered work-around to a known issue that has a simple solution in its documentation.</p>
<p>This is because the model's training data has a cut-off date. LLMs and tools like Claude have no way of knowing about the updated code patterns, unless you give it access to the current documentation.</p>
<p>But, as <a href="https://x.com/karpathy/status/1899876370492383450" target="_blank" rel="noopener noreferrer" class="">Andrej Karpathy observed</a>, most documentation contains content that's not relevant for LLMs:</p>
<blockquote>
<p><em>99% of libraries still have docs that basically render to some pretty .html static pages assuming a human will click through them. In 2025 the docs should be a your_project.md text file that is intended to go into the context window of an LLM.</em></p>
</blockquote>
<p>There's <a href="https://research.trychroma.com/context-rot" target="_blank" rel="noopener noreferrer" class="">research to back up his claim,</a> showing that LLM performance degrades when irrelevant content, like HTML syntax or verbose instructions for humans, is added to context as it distracts from the task and reduces accuracy.</p>
<p><strong>In other words, every unnecessary token in your context window makes the AI slightly worse at its job.</strong></p>
<p>So we need to be giving Claude Code the <em>right</em> kind of docs.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-solution-curated-doc-access">The Solution: Curated Doc Access<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#the-solution-curated-doc-access" class="hash-link" aria-label="Direct link to The Solution: Curated Doc Access" title="Direct link to The Solution: Curated Doc Access" translate="no">​</a></h3>
<p>The fix is simple: give the agent the ability to fetch and read the relevant, LLM-optimized documentation for the tools you're using.</p>
<p>Here are the two main ways developers are accomplishing this:</p>
<ul>
<li class=""><strong>MCP Servers</strong> — A standard for external systems to serve data to AI agents. Popular dev tools, such as <a href="https://vercel.com/docs/mcp/vercel-mcp" target="_blank" rel="noopener noreferrer" class="">Vercel</a>, make these available with doc searching tools.</li>
<li class=""><strong>llms.txt and doc maps</strong> — A standard for publishing LLM-friendly documentation at a well-known URL, e.g. <a href="https://wasp.sh/llms.txt" target="_blank" rel="noopener noreferrer" class="">https://wasp.sh/llms.txt</a>. The agent fetches structured docs optimized for LLM context windows.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="mcp-servers-for-docs-fetching">MCP Servers for Docs Fetching<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#mcp-servers-for-docs-fetching" class="hash-link" aria-label="Direct link to MCP Servers for Docs Fetching" title="Direct link to MCP Servers for Docs Fetching" translate="no">​</a></h3>
<p>The <a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener noreferrer" class="">Model Context Protocol (MCP)</a> is an open standard for connecting AI applications (i.e. agents, LLMs) to external systems. Claude Code can communicate with an MCP server to access specialized tools and information.</p>
<p>There are already tons of MCP servers for popular tools, like Supabase, Jira, Canva, Notion, and Vercel. Claude Code has <a href="https://code.claude.com/docs/en/mcp#popular-mcp-servers" target="_blank" rel="noopener noreferrer" class="">a docs section listing these and many more</a>, with instructions on how to install them if you’re interested.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="the Context7 MCP's `get-library-docs` tool for Material-UI " src="https://wasp.sh/img/claude-code-fullstack/mcp-servers-list.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">the Context7 MCP's `get-library-docs` tool</figcaption></figure></div>
<p>Developer Tool MCP servers like <a href="https://supabase.com/docs/guides/getting-started/mcp" target="_blank" rel="noopener noreferrer" class="">Supabase</a> and <a href="https://vercel.com/docs/ai-resources/vercel-mcp" target="_blank" rel="noopener noreferrer" class="">Vercel</a> have tools which will fetch documentation for the agent based on a query. There are some pros and cons to this approach, though.</p>
<p><strong>Pros</strong></p>
<ul>
<li class="">can do some pre-processing on the docs before sending them back</li>
<li class="">can version-check, filter, and fetch relevant snippets across multiple docs/guides</li>
<li class="">return structured outputs instead of raw text</li>
</ul>
<p><strong>Cons</strong></p>
<ul>
<li class="">the MCP server decides what is relevant, ignoring potentially useful information</li>
<li class="">all of its tools (not just doc fetching tools) get loaded into, and quickly fill up, the LLM’s context window, degrading the agent’s performance</li>
<li class="">more overhead for the agent: evaluate prompt → find the right MCP tool → call it → wait for response from MCP server → evaluate response → take action</li>
</ul>
<p>Because LLMs don’t have a real memory, they have to load information into context with every new session, such as the tools they can use. A single MCP server can add around 15-30 tools to the context, and with multiple servers you can easily consume 10-20% or more of your LLM’s context window before you even begin working.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Claude Code context window usage showing MCP server overhead" src="https://wasp.sh/img/claude-code-fullstack/context-usage-tip.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Checking the context usage in a Claude Code session.</figcaption></figure></div>
<p>If you want to see how much of your context window is already used up, you can run the <code>/context</code> slash command in an active Claude Code session.
The example above shows that 2.5% of the context window is used up by just one MCP server.</p>
<p>And as an LLM’s context window fills up, it’s performance degrades. Some devs even suggest starting a new session once the context is 75% full to avoid this, which is possible with the <code>/clear</code> command in Claude Code. You can also run the <code>/compact</code> command which creates a summary of your current session’s context and passes it along to the next session.</p>
<p>Luckily, if your main reason for using an MCP server is just for documentation searching, then an alternative open standard exists that may be a better fit.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="llm-friendly-docs-urls--llmstxt">LLM-friendly Docs URLs — LLMs.txt<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#llm-friendly-docs-urls--llmstxt" class="hash-link" aria-label="Direct link to LLM-friendly Docs URLs — LLMs.txt" title="Direct link to LLM-friendly Docs URLs — LLMs.txt" translate="no">​</a></h3>
<p><a href="https://llmstxt.org/" target="_blank" rel="noopener noreferrer" class="">LLMs.txt</a> has quickly become the standard for providing LLMs with context-friendly versions of websites at an <code>/llms.txt</code> path.</p>
<p>Try it out on some of your favorite developer tools URLs, such as:</p>
<ul>
<li class=""><a href="https://stripe.com/llms.txt" target="_blank" rel="noopener noreferrer" class="">stripe.com/llms.txt</a></li>
<li class=""><a href="https://supabase.com/llms.txt" target="_blank" rel="noopener noreferrer" class="">supabase.com/llms.txt</a></li>
<li class=""><a href="https://wasp.sh/llms.txt" target="_blank" rel="noopener noreferrer" class="">wasp.sh/llms.txt</a></li>
</ul>
<div style="display:flex;justify-content:center;margin:1rem 0"><figure style="width:100%;margin:0"><video width="100%" controls="" aria-label="Comparison of different llms.txt files"><source src="/img/claude-code-fullstack/llmstxt-comparison.mp4" type="video/mp4"></video><figcaption class="image-caption" style="font-style:italic;opacity:0.6;font-size:0.9rem">Some llms.txt provide lots of information up front, while others are simply maps to documentation.</figcaption></figure></div>
<p>Although llms.txt files might vary widely in the content they surface, they always follow the same format of a simple markdown file with the title of the website and some links. That’s it! This allows LLMs to get precise information without all the fluff.</p>
<p>Here are some of the pros and cons of using llms.txt for documentation fetching:</p>
<p><strong>Pros</strong></p>
<ul>
<li class="">curated list of the most relevant information for LLMs and agents</li>
<li class="">extremely simple; works with any agent that can fetch URLs</li>
<li class="">agents decide which links to follow and can see full source text</li>
<li class="">very context window efficient</li>
</ul>
<p><strong>Cons</strong></p>
<ul>
<li class="">raw docs / markdown files may transmit more information than needed</li>
<li class="">the agent could fetch incompatible links / files</li>
<li class="">the agent might need more guidance / rules on how to interact with the docs correctly</li>
</ul>
<p>In my opinion, I think that fetching docs via an <code>llms.txt</code> URL is the better approach as its more context efficient. For example, a typical documentation file is ~100 tokens versus 5,000-10,000 tokens for just one MCP server.</p>
<p><strong>That’s a 10x reduction in context usage.</strong></p>
<p>Claude Code is also great at navigating documentation maps to only fetch the most relevant information. Plus, you get the added benefit that <code>llms.txt</code> files are easier for humans to reference as well.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Example of an llms.txt file structure" src="https://wasp.sh/img/claude-code-fullstack/llmstxt-example.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>It's also the method that Claude Code uses for its <a href="https://simonwillison.net/2025/Oct/24/claude-code-docs-map/" target="_blank" rel="noopener noreferrer" class="">own doc fetching internally</a>. So when you ask it a question about its own features, it will first fetch its <a href="https://simonwillison.net/2025/Oct/24/claude-code-docs-map/" target="_blank" rel="noopener noreferrer" class="">documentation map markdown file URL</a> to find the correct guides.</p>
<p>So now that you know how to give Claude Code access to up-to-date documenation, the next question to answer is which tools might you need to provide the docs for?</p>
<p>And the most obvious answer is the <strong>tech stack or framework</strong> you’re building your full-stack app with.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="choosing-the-right-tech-stack--framework">Choosing the Right Tech Stack / Framework<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#choosing-the-right-tech-stack--framework" class="hash-link" aria-label="Direct link to Choosing the Right Tech Stack / Framework" title="Direct link to Choosing the Right Tech Stack / Framework" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="popular-choices">Popular Choices<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#popular-choices" class="hash-link" aria-label="Direct link to Popular Choices" title="Direct link to Popular Choices" translate="no">​</a></h3>
<p>This is probably the most overlooked of the 3 pillars.</p>
<p>Starting with a tech stack that AI can easily reason about will make the job of building the app you want significantly easier. There are a lot of options out there, but luckily there are many solid choices, such as:</p>
<ul>
<li class=""><a href="https://nextjs.org/docs" target="_blank" rel="noopener noreferrer" class="">NextJS</a> (React, NodeJS)</li>
<li class=""><a href="https://laravel.com/docs" target="_blank" rel="noopener noreferrer" class="">Laravel</a> (PHP)</li>
<li class=""><a href="https://rubyonrails.org/docs" target="_blank" rel="noopener noreferrer" class="">Ruby on Rails</a> (Rails)</li>
<li class=""><a href="https://wasp.sh/" target="_blank" rel="noopener noreferrer" class="">Wasp</a> (React, NodeJS, Prisma)</li>
</ul>
<p>In 2026, you'll be able to get very far with Claude Code and any of the frameworks listed above. But while these frameworks all offer good conventions, and are responsible for stitching together the most important parts of the stack, most of these still require some sort of additional integration.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-one-should-you-go-with">Which one should you go with?<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#which-one-should-you-go-with" class="hash-link" aria-label="Direct link to Which one should you go with?" title="Direct link to Which one should you go with?" translate="no">​</a></h3>
<p>For NextJS, which is more client-side focused, you have to chose and connect your own database layer. For Laravel and Rails, which unify backend and database, you need to decide which client to use and how it will talk to your backend.</p>
<p>That's why a lot of developers will reach for popular stacks/combos that include these frameworks, such as:</p>
<ul>
<li class="">NextJS + tRPC + Prisma + NextAuth (aka <a href="https://create.t3.gg/en/introduction" target="_blank" rel="noopener noreferrer" class="">T3 Stack</a>)</li>
<li class=""><a href="https://inertiajs.com/docs/introduction" target="_blank" rel="noopener noreferrer" class="">Laravel + Inertia.js + React</a></li>
<li class=""><a href="https://hotwired.dev/" target="_blank" rel="noopener noreferrer" class="">Rails + Hotwire</a></li>
</ul>
<p>You might have noticed as well that the T3 Stack is the only one to include an authentication library, NextAuth. That's because the backend-focused frameworks, Laravel and Rails, have an opinionated way of adding authentication to your app already, but NextJS does not.</p>
<p>Wasp, on the other hand, is the only one of the bunch that unifies all parts of the stack — client ↔ backend ↔ database — while also being opinionated on features.</p>
<p><strong>This is all great, but which one should you ultimately choose?</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-more-opinionated-the-better">The More Opinionated, The Better<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#the-more-opinionated-the-better" class="hash-link" aria-label="Direct link to The More Opinionated, The Better" title="Direct link to The More Opinionated, The Better" translate="no">​</a></h3>
<p>Well, the more opinionated the framework, the better it vibes with AI.</p>
<p>If a framework is <strong>opinionated</strong>, it means there's usually one obvious place to put code and one common pattern to follow. The AI doesn't have to guess. They've already made decisions ahead of time so you (and your agent) don't have to. They've encoded architectural wisdom into conventions, picked the libraries to use, the way authentication is wired, and how the app should be structured.</p>
<p><strong>So when there are fewer decisions to make, less boilerplate to write, and fewer tools to stitch together, the development process becomes more reliable.</strong></p>
<p>And as the app gets more complex, AI doesn't lose its focus, because the framework is handling a lot of underlying complexity.  You can also understand and inspect what is being generated more easily, and avoid building yourself into a messy corner.</p>
<p>Consider that <strong>opinionated frameworks can reduce boilerplate by 60-80%</strong>. Wasp's auth declaration, for example, replaces 500+ lines of typical authentication code with a 10 to 15-line config. That's 97% less code that needs to be generated and audited!</p>
<p>Of these frameworks, Wasp is definitely the most opinionated but it's also the newest kid on the block. After that, Laravel and Rails are probably tied for a close second, but they are built on PHP and Rails, respectively, and come with their own distinct ecosystems, so you'll most likely have to pair them with a frontend JavaScript framework like React, too. NextJS, is the most popular but least opinionated of the bunch, so it means there is more complexity and up-front choices you and your AI have to deal with, but this offers more flexibility in the long run.</p>
<p>So in the end, the choice you make largely depends on what you're trying to achieve, what you're comfortable with, and how much flexibility you need.</p>
<p>Just remember, the more a framework gives you structure and defaults, the easier it is for AI to generate code that fits correctly — and the less time you spend fixing confusing or inconsistent output.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-this-means-for-claude-code">What This Means for Claude Code<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#what-this-means-for-claude-code" class="hash-link" aria-label="Direct link to What This Means for Claude Code" title="Direct link to What This Means for Claude Code" translate="no">​</a></h3>
<p>Ok. So we've established that working with an opinionated framework means that it it manages a lot of the complexity for you.</p>
<p>But what does that practically mean when using it with Claude Code?</p>
<ul>
<li class=""><strong>Subagents for architecture planning?</strong> The framework already decided the architecture.</li>
<li class=""><strong>Elaborate plans for where code should live?</strong> The conventions tell you.</li>
<li class=""><strong>Back-and-forth to agree on patterns?</strong> The patterns are already decided.</li>
<li class=""><strong>Glueing the pieces of your app together?</strong> The frameworks manage this code.</li>
<li class=""><strong>Context that explains your app structure?</strong> It's embedded in the framework.</li>
</ul>
<p>In a sense, the framework acts as a large specification that both you and Claude already understand and agree on.</p>
<p><strong>Instead of multi-turn conversations to figure out HOW things should be built, you get to just say WHAT you want built.</strong></p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp visual representation of a full-stack app structure" src="https://wasp.sh/img/claude-code-fullstack/wasp-visual-representation.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Wasp Studio even gives you a visual representation of your full-stack app structure.</figcaption></figure></div>
<p>So when you tell Claude "add a new model for Comments" or "add a user account settings feature", Claude will know exactly what that means and where it all goes. And you also get the added benefit that the implementations follow best practices and are backed by the decisions of experienced professionals behind the framework, and is not just some <a href="https://x.com/karpathy/status/2015883857489522876" target="_blank" rel="noopener noreferrer" class="">LLM hastily implementing a feature on the wrong assumptions.</a></p>
<p>This isn't to say that you no longer have to do good planning, create a good spec, or Product Requirement Doc for your agent to follow. This can still be a really important step when vibe coding (or practicing "<a href="https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html" target="_blank" rel="noopener noreferrer" class="">spec-driven development</a>").</p>
<p>But it does mean that, with an opinionated full-stack framework, much less of your planning phases need to be devoted to discussing architectural and technical implementation details.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="this-approach-in-a-plugin">This Approach, In a Plugin<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#this-approach-in-a-plugin" class="hash-link" aria-label="Direct link to This Approach, In a Plugin" title="Direct link to This Approach, In a Plugin" translate="no">​</a></h2>
<p>If you want to put this theoretical approach I discussed above to the practical test, then I suggest you try out the <a href="https://github.com/wasp-lang/wasp-agent-plugins/tree/main/plugins/wasp" target="_blank" rel="noopener noreferrer" class="">Wasp plugin we created for Claude Code</a>.</p>
<p>We, the Wasp framework creators, maintain the plugin, so we've battle tested it with Wasp. Plus we're a very responsive community and we're listening to feedback and improving it all the time.</p>
<p>Here's how to get started:</p>
<ol>
<li class=""><strong>Install Wasp</strong></li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> i </span><span class="token parameter variable" style="color:#36acaa">-g</span><span class="token plain"> @wasp.sh/wasp-cli</span><br></div></code></pre></div></div>
<ol start="2">
<li class=""><strong>Install the Claude Code plugin</strong></li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># add the Wasp marketplace to Claude</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">claude plugin marketplace </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> wasp-lang/wasp-agent-plugins</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># install the plugin from the marketplace</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">claude plugin </span><span class="token function" style="color:#d73a49">install</span><span class="token plain"> wasp@wasp-agent-plugins </span><span class="token parameter variable" style="color:#36acaa">--scope</span><span class="token plain"> project</span><br></div></code></pre></div></div>
<ol start="3">
<li class=""><strong>Start a new Wasp project</strong></li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># create a new project</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">wasp new</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># change into the project root directory</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">your-wasp-project</span><span class="token operator" style="color:#393A34">&gt;</span><br></div></code></pre></div></div>
<ol start="4">
<li class=""><strong>Start a Claude Code session</strong> in your Wasp project directory</li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">claude</span><br></div></code></pre></div></div>
<ol start="5">
<li class=""><strong>Run the init command</strong> to set up the plugin</li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">/wasp:init</span><br></div></code></pre></div></div>
<p>From there, you can tell Claude to "start the dev server" and it will walk you through spinning up fullstack visibility as we outlined above. Or ask it to implement a Wasp feature and watch it fetch version-matched documentation guides for you!</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp Claude Code plugin in action" src="https://wasp.sh/img/claude-code-fullstack/plugin-in-action.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Wasp Claude Code plugin in action.</figcaption></figure></div>
<p>More importantly, Wasp as a framework pushes into territory that's even more AI-native than the rest, because of its central configuration file(s) where the app is defined.</p>
<p>This main config file is like your app's blueprint. Wasp takes these declarations, and manages the code for those features for you.</p>
<p>Take this example config file authentication snippet as reference:</p>
<div class="language-wasp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-wasp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token declaration-type keyword" style="color:#00009f">app</span><span class="token plain"> </span><span class="token class-name variable" style="color:#36acaa">TaskManager</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token dict-key plain" style="color:#393A34">wasp</span><span class="token plain">: </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token dict-key plain" style="color:#393A34">version</span><span class="token plain">: </span><span class="token string" style="color:#e3116c">"^0.21.0"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token dict-key plain" style="color:#393A34">title</span><span class="token plain">: </span><span class="token string" style="color:#e3116c">"Task Manager"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token dict-key plain" style="color:#393A34">auth</span><span class="token plain">: </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token dict-key plain" style="color:#393A34">userEntity</span><span class="token plain">: </span><span class="token class-name variable" style="color:#36acaa">User</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token dict-key plain" style="color:#393A34">methods</span><span class="token plain">: </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token dict-key plain" style="color:#393A34">email</span><span class="token plain">: </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token dict-key plain" style="color:#393A34">google</span><span class="token plain">: </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token dict-key plain" style="color:#393A34">onAuthFailedRedirectTo</span><span class="token plain">: </span><span class="token string" style="color:#e3116c">"/login"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>This is what an authentication implementation in Wasp looks like. That's it.</p>
<p>This 8-line config generates what would typically require 500-800 lines of code: components, session handling, password hashing, OAuth flows, and database schemas. Claude just needs to know what auth methods you want.</p>
<p>Claude doesn't need to worry about choosing what kind of auth implementation to use, or generating any of the glue code. It can just get straight to work building features.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-you-do-need-the-fancy-stuff">When You DO Need the Fancy Stuff<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#when-you-do-need-the-fancy-stuff" class="hash-link" aria-label="Direct link to When You DO Need the Fancy Stuff" title="Direct link to When You DO Need the Fancy Stuff" translate="no">​</a></h2>
<p>I've spent this whole article arguing that you can ignore a lot Claude Code features for fullstack app development. But I don't want to leave you with the impression that those features are useless.</p>
<p>Custom subagents, commands, and skills shine when you're doing the same task repeatedly with consistent criteria, e.g.:</p>
<ul>
<li class=""><strong>Testing</strong> — A dedicated test-runner subagent that knows your testing patterns, runs suites, analyzes failures, and suggests fixes.</li>
<li class=""><strong><a href="https://code.claude.com/docs/en/sub-agents#code-reviewer" target="_blank" rel="noopener noreferrer" class="">Code Reviews</a></strong> — A code review command or subagent that runs tests, fixes bugs, and reviews code after every development.</li>
<li class=""><strong>Running Scripts</strong> — Skills can be useful if you have deterministic tasks you run often, like a deployment script or using a cli tool to convert your blog images to webp. Define them in a Skill and link those scripts to them, and Claude Code will run them when it deems it fit for the task at hand.</li>
</ul>
<p>These are tasks where you want the same process followed every time and where a well-configured subagent with specific rules makes sense.</p>
<p>In most cases, reach for complexity only when the simpler approach stops working.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="now-build">Now Build!<a href="https://wasp.sh/blog/2026/01/29/claude-code-fullstack-development-essentials#now-build" class="hash-link" aria-label="Direct link to Now Build!" title="Direct link to Now Build!" translate="no">​</a></h2>
<p>With these three ingredients I think most fullstack app developers can get the vast majority of the work done without reaching for much more:</p>
<ol>
<li class="">An opinionated framework that handles architecture/boilerplate so Claude doesn't have to</li>
<li class="">Version-matched documentation so Claude has up-to-date implementation details</li>
<li class="">Full-stack visibility so Claude can see what's happening and fix it on its own</li>
</ol>
<p>With these in place, Claude Code's basic toolset—exploring, planning, reading, writing, running commands—is enough to build real, complex fullstack applications. The subagents, hooks, plugins, and complex configurations are there if you need them, but honestly most of the time, you won't.</p>]]></content:encoded>
            <author>vince@wasp-lang.dev (Vince Canger)</author>
            <category>wasp</category>
            <category>ai</category>
            <category>claude-code</category>
            <category>fullstack</category>
        </item>
        <item>
            <title><![CDATA[Wasp 2025: Year in Review]]></title>
            <link>https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review</link>
            <guid>https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review</guid>
            <pubDate>Tue, 30 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[2025 was both the biggest year for Wasp so far, but also one in which we were heads-down building the most. While we shipped consistently throughout the year (5 launch events, 20 blog posts & tutorials, and a steady stream of features) the real story is what happened beneath the surface: clearing the path to 1.0.]]></description>
            <content:encoded><![CDATA[<p>2025 was both the biggest year for Wasp so far, but also one in which we were heads-down building the most. While we shipped consistently throughout the year (5 launch events, 20 blog posts &amp; tutorials, and a steady stream of features) the real story is what happened beneath the surface: <strong>clearing the path to 1.0</strong>.</p>
<!-- -->
<p>This was the year in which we had to slow down in order to speed up. We got serious about maturity, transparency, and building the foundation for what Wasp needs to become. Still, we managed to sneak in a few cool new features, too.</p>
<p>We want to share with you what we achieved, what we learned, and what's coming next.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2025-in-numbers">2025 in Numbers<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#2025-in-numbers" class="hash-link" aria-label="Direct link to 2025 in Numbers" title="Direct link to 2025 in Numbers" translate="no">​</a></h2>
<ul>
<li class="">📦 <strong>13 releases</strong> (v0.16 → v0.20)</li>
<li class="">🚀 <strong>5 launch events</strong></li>
<li class="">⭐ <strong>+8,182 <a href="https://github.com/wasp-lang/wasp" target="_blank" rel="noopener noreferrer" class="">GitHub</a> stars</strong> across Wasp and Open SaaS</li>
<li class="">🔀 <strong>489 pull requests merged</strong></li>
<li class="">✅ <strong>284 issues closed</strong></li>
<li class="">👾 <strong>4,669 Discord members</strong></li>
<li class="">🐝 <strong>8 people</strong> working on Wasp full-time (welcome Franjo, Carlos &amp; Tole!)</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-shipped">What We Shipped<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#what-we-shipped" class="hash-link" aria-label="Direct link to What We Shipped" title="Direct link to What We Shipped" translate="no">​</a></h2>
<p>In 2025, we hosted <strong>five launch events</strong> - one each quarter, plus a special Xmas launch. You can see the details of each launch below:</p>
<ul>
<li class=""><a class="" href="https://wasp.sh/blog/2025/01/09/wasp-launch-week-8"><strong>Launch Week 8</strong></a> (January) - "Fixer Upper"</li>
<li class=""><a class="" href="https://wasp.sh/blog/2025/04/09/wasp-launch-week-9"><strong>Launch Week 9</strong></a> (April) - "The Road to 1.0"</li>
<li class=""><a class="" href="https://wasp.sh/blog/2025/07/07/wasp-launch-week-10"><strong>Launch Week 10</strong></a> (July) - "Public Exposure"</li>
<li class=""><a class="" href="https://wasp.sh/blog/2025/09/28/wasp-launch-week-11"><strong>Launch Week 11</strong></a> (September) - "Grinding the Grind"</li>
<li class=""><a class="" href="https://wasp.sh/blog/2025/12/17/wasp-xmas-launch"><strong>Wasp Xmas Launch</strong></a> (December) - React 19, Claude Code Plugin, Polar</li>
</ul>
<p>What we released falls into four major categories:</p>
<ul>
<li class=""><strong>Core &amp; Developer Experience</strong> - the overall architecture, CLI, and what/how Wasp uses under the hood</li>
<li class=""><strong>Integrations &amp; Ecosystem</strong> - how well Wasp plays with other tools and services (deployment, analytics, AI, ...)</li>
<li class=""><strong>Open SaaS</strong> - an open-source SaaS boilerplate starter, based on Wasp. Payments, admin dashboard, emails, and more</li>
<li class=""><strong>Content &amp; Education</strong> - tutorials, guides, showcases, and community engagement</li>
</ul>
<p>Let's now dive into the details of each category:</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="core--developer-experience">Core &amp; Developer Experience<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#core--developer-experience" class="hash-link" aria-label="Direct link to Core &amp; Developer Experience" title="Direct link to Core &amp; Developer Experience" translate="no">​</a></h3>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp Roadmap on GitHub" src="https://wasp.sh/img/year-review-2025/roadmap.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Wasp's roadmap to 1.0 is on GitHub</figcaption></figure></div>
<ul>
<li class=""><strong>React 19 &amp; stack upgrades</strong> - Updated to React 19, along with Node.js and Vite version bumps</li>
<li class=""><strong>Public development roadmap</strong> - Published our <a class="" href="https://wasp.sh/blog/2025/07/14/public-wasp-dev-roadmap">transparent path to 1.0</a>, with epics and milestones <a href="https://github.com/orgs/wasp-lang/projects/5" target="_blank" rel="noopener noreferrer" class="">tracked on GitHub</a></li>
<li class=""><strong>Deployment docs rehaul</strong> - New guides for <a class="" href="https://wasp.sh/docs/deployment/deployment-methods/self-hosted">Coolify, CapRover</a>, and improved deployment story overall</li>
<li class=""><strong><a class="" href="https://wasp.sh/docs/0.20/general/wasp-ts-config">TypeScript config</a> (experimental)</strong> - Define your app in <code>main.wasp.ts</code> instead of the DSL, with full editor support out of the box. Soon to become the default.</li>
<li class=""><strong>Quality of life improvements</strong> - Env variable validation with Zod, <a class="" href="https://wasp.sh/docs/deployment/local-testing"><code>wasp build start</code></a> for testing production builds locally, better TSConfig behavior</li>
</ul>
<p>While some of these look like single bullet points, the roadmap and TypeScript config alone represent months of focused work. Behind the scenes, we also <a class="" href="https://wasp.sh/blog/2025/07/18/faster-wasp-dev">overhauled our internal architecture</a> - modernizing how we build, test, and release Wasp, so we can move faster and ship with more confidence.</p>
<p>This groundwork is already enabling us to make Wasp behave more like a "standard" part of the JS ecosystem: respecting your existing tooling, playing nicely with other libraries (e.g. ShadCN), and just working the way you'd expect.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="integrations--ecosystem">Integrations &amp; Ecosystem<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#integrations--ecosystem" class="hash-link" aria-label="Direct link to Integrations &amp; Ecosystem" title="Direct link to Integrations &amp; Ecosystem" translate="no">​</a></h3>
<ul>
<li class=""><strong>Railway CLI Integration</strong> - One-command deployment to Railway with <code>wasp deploy railway launch</code></li>
<li class=""><strong>Claude Code Plugin</strong> - AI-assisted development with <a class="" href="https://wasp.sh/blog/2025/12/23/wasp-claude-code-plugin">deep Wasp integration</a></li>
<li class=""><strong>AI-friendly docs</strong> - Added .cursorrules and llms.txt with versioning, so AI tools always have the right docs for your Wasp version</li>
<li class=""><strong><a class="" href="https://wasp.sh/docs/auth/social-auth/slack">Slack Authentication</a></strong> - New auth provider, next to Google, GitHub, Discord, and others</li>
</ul>
<p>No tool can exist in isolation. And when we find a service/tool that fits with Wasp's philosophy, we go above and beyond to make it easy to use together.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="open-saas">Open SaaS<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#open-saas" class="hash-link" aria-label="Direct link to Open SaaS" title="Direct link to Open SaaS" translate="no">​</a></h3>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Open SaaS" src="https://wasp.sh/img/year-review-2025/open-saas.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>2025 is the year Open SaaS truly came into its own. What started as a simple wrapper around Wasp with common SaaS features has grown into a fully-fledged product. It's now one of the most common ways developers start their (Wasp) apps.</p>
<ul>
<li class=""><strong>Hit <a href="https://github.com/wasp-lang/open-saas" target="_blank" rel="noopener noreferrer" class="">13,000+ GitHub stars</a></strong> - One of the most popular React/Node.js SaaS starters</li>
<li class=""><strong>Complete redesign (v2.0)</strong> - Now with ShadCN UI, and a sleek new design by default</li>
<li class=""><strong>Railway Marketplace</strong> - Now part of <a href="https://railway.com/deploy/open-saas" target="_blank" rel="noopener noreferrer" class="">Railway's official marketplace</a>. One click deployment to Railway's hosting platform.</li>
<li class=""><strong>Polar integration</strong> - New billing provider, next to Stripe and LemonSqueezy</li>
</ul>
<p>Open SaaS now also has its own <a href="https://opensaas.sh/#roadmap" target="_blank" rel="noopener noreferrer" class="">roadmap</a>, developed in parallel with Wasp's roadmap. You're welcome to join, comment, and contribute - we'd love to have you!</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="content--education">Content &amp; Education<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#content--education" class="hash-link" aria-label="Direct link to Content &amp; Education" title="Direct link to Content &amp; Education" translate="no">​</a></h3>
<p>Launch Weeks are our most intense publishing periods, but we don't go quiet in between. We aim to keep you up to date with Wasp's progress while sharing what we're learning along the way.</p>
<p><strong>Most popular deep-dives:</strong></p>
<ul>
<li class=""><a class="" href="https://wasp.sh/blog/2025/10/07/how-we-test-a-web-framework">How we test a web framework</a></li>
<li class=""><a class="" href="https://wasp.sh/blog/2025/07/18/faster-wasp-dev">Cleaning up 5 years of tech debt in a full-stack JS framework</a></li>
</ul>
<p><strong>Most popular tutorials:</strong></p>
<ul>
<li class=""><a class="" href="https://wasp.sh/blog/2025/01/22/advanced-react-hook-form-zod-shadcn">Building Advanced React Forms Using React Hook Form, Zod and Shadcn</a></li>
<li class=""><a class="" href="https://wasp.sh/blog/2025/04/02/an-introduction-to-database-migrations">A Gentle Introduction to Database Migrations in Prisma</a></li>
<li class=""><a class="" href="https://wasp.sh/blog/2025/05/28/how-to-run-cron-jobs-in-postgress-without-extra-infrastructure">How to Run CRON Jobs in Postgres Without Extra Infrastructure</a></li>
<li class=""><a href="https://www.youtube.com/watch?v=-mWmIaJ6AJk" target="_blank" rel="noopener noreferrer" class="">Build an agent-powered SaaS with Mastra AI &amp; Wasp</a> (video)</li>
<li class=""><a href="https://www.youtube.com/watch?v=WYzEROo7reY" target="_blank" rel="noopener noreferrer" class="">Vibe Code a Full-stack App Effectively</a> (video)</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-learned">What We Learned<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#what-we-learned" class="hash-link" aria-label="Direct link to What We Learned" title="Direct link to What We Learned" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="developers-really-want-rails-for-js---productivity-and-portability">Developers <em>really</em> want "Rails for JS" - productivity and portability<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#developers-really-want-rails-for-js---productivity-and-portability" class="hash-link" aria-label="Direct link to developers-really-want-rails-for-js---productivity-and-portability" title="Direct link to developers-really-want-rails-for-js---productivity-and-portability" translate="no">​</a></h3>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Rails for JS" src="https://wasp.sh/img/year-review-2025/rails-for-js.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>The JavaScript ecosystem is incredibly powerful - its modularity means there's a package for everything. But that same modularity makes it exhausting to start and maintain a full-stack project. You have to choose your bundler, your router, your ORM, your auth library/service, your hosting platform... and then keep them all working together as they evolve.</p>
<p>We've heard this over and over again: developers want the productivity and portability of Rails, but in the JavaScript/TypeScript world they already know. This conviction has only grown stronger for us in 2025 - there's a real gap here, and we're building to fill it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="frameworks-matter-more-in-the-age-of-ai-not-less">Frameworks matter more in the age of AI, not less<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#frameworks-matter-more-in-the-age-of-ai-not-less" class="hash-link" aria-label="Direct link to Frameworks matter more in the age of AI, not less" title="Direct link to Frameworks matter more in the age of AI, not less" translate="no">​</a></h3>
<p>Here's something we've assumed for a while but haven't seen confirmed until recently: AI coding assistants work dramatically better with opinionated frameworks. When there's one right way to do something, AI can nail it. When there are fifteen ways to wire up authentication, it's another point of friction you or your LLM need to think about and make a decision.</p>
<p>We're literally seeing this as a choosing criteria now for Wasp - developers pick tools that make their AI assistants more effective. Good abstractions aren't just nice for humans anymore; they're essential for AI too. This has reinforced our belief that the right level of abstraction is one of the most valuable things a framework can provide.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="typescript-is-the-way">TypeScript is the way<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#typescript-is-the-way" class="hash-link" aria-label="Direct link to TypeScript is the way" title="Direct link to TypeScript is the way" translate="no">​</a></h3>
<p>We're moving from a DSL to TypeScript config. From "Wasp language" to "Wasp framework." Some of this is messaging, but it's actually a meaningful shift.</p>
<p>TypeScript gives an editor support for free - autocomplete, type checking, refactoring tools - without us having to build and maintain custom IDE plugins. It's a language developers already know. And it opens the door to more flexibility while keeping the benefits of a structured, declarative config.</p>
<p>This move reflects a broader lesson: meet developers where they are. Don't ask them to learn something new unless it provides clear, significant value. TypeScript config is just as expressive as our DSL (even more), but without the learning curve.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="developers-are-building-real-businesses-and-tools">Developers are building real businesses and tools<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#developers-are-building-real-businesses-and-tools" class="hash-link" aria-label="Direct link to Developers are building real businesses and tools" title="Direct link to Developers are building real businesses and tools" translate="no">​</a></h3>
<p>2025 made something crystal clear: Wasp has moved past the "toy/side project" phase. We're seeing developers build very real things - production apps serving actual users, internal tools at enterprises, startups that have raised funding, and yes, even exits.</p>
<p>This shift is meaningful. It's one thing to hear "I built a to-do app with Wasp" and quite another to hear "we're running our entire company on it." The maturity of what people are building now - and the trust they're placing in the framework - is something we don't take lightly. It's also the ultimate validation that we're on the right track.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-team">The Team<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#the-team" class="hash-link" aria-label="Direct link to The Team" title="Direct link to The Team" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img style="width:650px" alt="The Wasp Team" src="https://wasp.sh/img/year-review-2025/wasp-team.jpeg"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">The Waspeteers! This was at our annual retreat in Heidelberg we took in October. Banana juice with beer, anyone?</figcaption></figure></div>
<p>This is the team that made it all happen in 2025. Without their focus and dedication, none of this would be possible. This year we welcomed <a class="" href="https://wasp.sh/blog/2025/03/20/meet-franjo-engineer-at-wasp">Franjo</a>, <a class="" href="https://wasp.sh/blog/2025/04/07/meet-carlos-engineer-at-wasp">Carlos</a>, and another Matija (Tole). It is a unique privilege to be able to pick (and get picked by) your own team and get to design our team culture as we build out the vision we have for Wasp.</p>
<p>What we're building hasn't really been done before - a true full-stack, batteries-included framework for JavaScript. That means we're often in uncharted territory. Our work takes a lot of research and design before we can even start writing code. What keeps us going is the feedback and adoption we see from the community - knowing that what we're building actually helps people ship and achieve their dreams and goals.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-coming-next">What's Coming Next<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#whats-coming-next" class="hash-link" aria-label="Direct link to What's Coming Next" title="Direct link to What's Coming Next" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="laser-focus-on-10">Laser focus on 1.0<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#laser-focus-on-10" class="hash-link" aria-label="Direct link to Laser focus on 1.0" title="Direct link to Laser focus on 1.0" translate="no">​</a></h3>
<p>The path is clear and we're going full speed. Everything we did in 2025 - the roadmap, the internal refactors, the testing infrastructure - was setting the stage for this. Now we execute.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="doubling-down-on-ai">Doubling down on AI<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#doubling-down-on-ai" class="hash-link" aria-label="Direct link to Doubling down on AI" title="Direct link to Doubling down on AI" translate="no">​</a></h3>
<p>We're seeing exciting signal on how well Wasp works with AI coding assistants. Opinionated frameworks and AI are a natural fit, and we want to lean into that even more - better tooling, better docs, better integrations.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="polishing-the-dx--more-user-facing-features">Polishing the DX + more user-facing features<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#polishing-the-dx--more-user-facing-features" class="hash-link" aria-label="Direct link to Polishing the DX + more user-facing features" title="Direct link to Polishing the DX + more user-facing features" translate="no">​</a></h3>
<p>Having invested in leveling up the core of Wasp in 2025, we can now reap the benefits and move to the next stage - building cool features you'll see and use! We'll be doing a complete "DX audit", from creating a new project to building and testing it and finally deploying it to production.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="thank-you">Thank you!<a href="https://wasp.sh/blog/2025/12/30/wasp-2025-year-in-review#thank-you" class="hash-link" aria-label="Direct link to Thank you!" title="Direct link to Thank you!" translate="no">​</a></h2>
<p>None of this would have been possible without you - our community. You're the ones building with Wasp, pushing its limits, reporting bugs, suggesting features, and showing us what's possible. Seeing you get excited about what we're building, and then taking it even further than we imagined, is what keeps us going.</p>
<p>Thank you for being part of this journey. Here's to a buzzing 2026.</p>]]></content:encoded>
            <author>matija@wasp-lang.dev (Matija Sosic)</author>
            <category>wasp</category>
            <category>year-review</category>
            <category>launch-week</category>
        </item>
        <item>
            <title><![CDATA[The Claude Code Plugin for Wasp is Here 🔌]]></title>
            <link>https://wasp.sh/blog/2025/12/23/wasp-claude-code-plugin</link>
            <guid>https://wasp.sh/blog/2025/12/23/wasp-claude-code-plugin</guid>
            <pubDate>Tue, 23 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Get Even More Out of Claude Code with the Wasp Plugin]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="get-even-more-out-of-claude-code-with-the-wasp-plugin">Get Even More Out of Claude Code with the Wasp Plugin<a href="https://wasp.sh/blog/2025/12/23/wasp-claude-code-plugin#get-even-more-out-of-claude-code-with-the-wasp-plugin" class="hash-link" aria-label="Direct link to Get Even More Out of Claude Code with the Wasp Plugin" title="Direct link to Get Even More Out of Claude Code with the Wasp Plugin" translate="no">​</a></h2>
<p>Batteries-included frameworks are a great match for AI-assisted coding tools like <a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank" rel="noopener noreferrer" class="">Claude Code</a>. The frameworks themselves are opinionated and already take care of a lot of routine tasks and boilerplate, which makes the AI's output more deterministic and predictable.</p>
<p>In effect, frameworks like Wasp take care of a lot of grunt work for you and your AI assistants, shifting the focus to the more complex and creative parts of your app.</p>
<p>And now with Claude Code's plugin system, which bundles skills, commands, hooks, and rules, you can tweak things so that you're getting the absolute most out of Claude Code in your projects.</p>
<!-- -->
<p>But configuring, testing, and maintaining all these Claude Code features for your specific projects and tools can be a lot of work. Almost as much as just writing the code yourself (ironic, isn't it?)!</p>
<p>Luckily for you, the the Wasp Team has created the <a href="https://github.com/wasp-lang/wasp-agent-plugins/tree/main/plugins/wasp" target="_blank" rel="noopener noreferrer" class="">Wasp Claude Code plugin</a>, curated with the help of community and their own experience, that ensures you are getting the most out of Claude Code and Wasp when using them together.</p>
<div class="video-container"><iframe src="https://www.youtube.com/embed/beUTJYW65Bw?si=_CmEU1f3r1sTZW69" frameborder="1" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="turn-claude-into-a-wasp-expert">Turn Claude into a Wasp Expert<a href="https://wasp.sh/blog/2025/12/23/wasp-claude-code-plugin#turn-claude-into-a-wasp-expert" class="hash-link" aria-label="Direct link to Turn Claude into a Wasp Expert" title="Direct link to Turn Claude into a Wasp Expert" translate="no">​</a></h2>
<p>Claude Code knows it can't do everything perfectly, which is why it allows for user-defined commands, skills, hooks, and rules, which act as sources of knowledge and guardrails when working in your project.</p>
<p>We've done the tedious work for you, and bundled a bunch of the essentials into the Claude Code plugin for Wasp.</p>
<p>Here's how you can try it once you've got <a href="https://wasp.sh/" target="_blank" rel="noopener noreferrer" class="">Wasp</a> and <a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank" rel="noopener noreferrer" class="">Claude Code</a> installed:</p>
<ol>
<li class="">Add the Wasp x Claude Code Plugin Marketplace:</li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">claude plugin marketplace </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> wasp-lang/wasp-agent-plugins</span><br></div></code></pre></div></div>
<ol start="2">
<li class="">Install the plugin:</li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">claude plugin </span><span class="token function" style="color:#d73a49">install</span><span class="token plain"> wasp@wasp-agent-plugins </span><span class="token parameter variable" style="color:#36acaa">--scope</span><span class="token plain"> project</span><br></div></code></pre></div></div>
<ol start="3">
<li class="">Once installed, initialize the plugin in a Claude Code session and follow the instructions:</li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:var(--wasp-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:var(--wasp-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">/wasp:init</span><br></div></code></pre></div></div>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp plugin initialization" src="https://wasp.sh/img/cc-plugin/plugin-init.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>With the plugin installed, Claude turns into a true Wasp expert by:</p>
<ol>
<li class=""><strong>Using the right documentation</strong> — Automatically fetches the correct Wasp docs for your project's version during development and debugging</li>
<li class=""><strong>Avoiding common mistakes</strong> — Provides Wasp-specific tips, patterns, and best practices so Claude doesn't hallucinate or use outdated approaches</li>
<li class=""><strong>Guided workflows</strong> — Skills and commands so Claude can walk you through setting up Wasp's batteries-included features: auth, email, database, deploying, etc.</li>
<li class=""><strong>Full debugging visibility</strong> — Start managed databases, dev servers, and connect browser console access so Claude has full development and debugging visibility across the entire stack</li>
</ol>
<p>The result: Claude actually understands Wasp instead of guessing and can get the most out of Wasp features to help you spend less time managing boilerplate or debugging errors, and more time building and shipping app features.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="give-claude-the-wheel-or-let-it-be-your-backseat-driver">Give Claude the Wheel, Or Let it Be Your Backseat Driver<a href="https://wasp.sh/blog/2025/12/23/wasp-claude-code-plugin#give-claude-the-wheel-or-let-it-be-your-backseat-driver" class="hash-link" aria-label="Direct link to Give Claude the Wheel, Or Let it Be Your Backseat Driver" title="Direct link to Give Claude the Wheel, Or Let it Be Your Backseat Driver" translate="no">​</a></h2>
<p>One of the most important features of the plugin is that it gives Claude everything it needs to get your Wasp app setup and running locally, with full visibility into what's going on with your app.</p>
<p>For example, Claude can use the Plugin's skills to easily setup, run and monitor Wasp's:</p>
<ul>
<li class="">managed Postgres DB server,</li>
<li class="">local development servers, and</li>
<li class="">a Chrome instance</li>
</ul>
<p>By leveraging these Wasp features, you give Claude <em>instant</em> access to logs across the entire stack and can drastically reduce development and debugging feedback loops.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Claude Code fetching Wasp documentation" src="https://wasp.sh/img/cc-plugin/fetching-docs.webp"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>On top of that, we've added hooks to ensure Claude checks your project's Wasp version and always pulls the correct LLM-friendly docs before performing important Wasp-related tasks.</p>
<p>Here are some examples of things you can ask Claude to help with:</p>
<ul>
<li class=""><em>"Add <code>Google authentication</code> to my app"</em></li>
<li class=""><em>"Migrate the database from <code>SQLite</code> to <code>PostgreSQL</code> and start it for me"</em></li>
<li class=""><em>"Deploy my app to <code>Railway</code> for me"</em></li>
<li class=""><em>"Help me add <code>ShadCN UI</code> to my app to build a dashboard"</em></li>
<li class=""><em>"Start a new SaaS app using <code>Wasp's SaaS starter template</code>"</em></li>
<li class=""><em>"Why isn't my <code>recurring job</code> working?"</em></li>
</ul>
<p>With the Wasp plugin, Claude Code will know exactly how to take care of these tasks in your Wasp app, and can take full control of implementation, or guide you through the process.</p>
<p>We've also loaded the plugin with straight-forward slash commands, such as <code>/wasp:help</code> so that you can always quickly reference what the plugin can do.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-next">What's Next<a href="https://wasp.sh/blog/2025/12/23/wasp-claude-code-plugin#whats-next" class="hash-link" aria-label="Direct link to What's Next" title="Direct link to What's Next" translate="no">​</a></h2>
<p>The focus for the first iteration of the plugin was to give Claude good fundamentals Wasp knowledge.</p>
<p>For the future, we'll expand it with better guardrails and more skills and workflows to really speed up development. We'll also be adding a separate plugin for <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a>, our open-source SaaS starter built on Wasp, to get you shipping SaaS apps easier and quicker.</p>
<p>And if you have any feedback or ideas on how to improve the plugin, <a href="https://github.com/wasp-lang/wasp-agent-plugins/issues" target="_blank" rel="noopener noreferrer" class="">let us know</a>.</p>]]></content:encoded>
            <author>vince@wasp-lang.dev (Vince Canger)</author>
            <category>wasp</category>
            <category>ai</category>
            <category>claude-code</category>
        </item>
        <item>
            <title><![CDATA[Wasp Xmas Launch - React 19, Claude Code Plugin, Polar! 🎄🎁]]></title>
            <link>https://wasp.sh/blog/2025/12/17/wasp-xmas-launch</link>
            <guid>https://wasp.sh/blog/2025/12/17/wasp-xmas-launch</guid>
            <pubDate>Wed, 17 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<ImgWithCaption]]></description>
            <content:encoded><![CDATA[<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Wasp Xmas Launch" src="https://wasp.sh/img/xmas-launch/xmas-launch-banner.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>You know that feeling when you're waiting for a package to arrive and the tracking says "delivery delayed until January"? Yeah, we think that sucks, too (thanks so much, Temu).</p>
<p>We've been working on some cool stuff that we're really excited about, and we thought - why make you wait until our next launch week in January when we can give you early Xmas presents instead?</p>
<!-- -->
<p><strong>So here's what we're doing: On December 23rd, we're unwrapping 4 new features for the Wasp community. Consider it our gift to you for an amazing 2026! 🎄</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="join-us-for-the-xmas-party-">Join Us for the Xmas Party! 🎉<a href="https://wasp.sh/blog/2025/12/17/wasp-xmas-launch#join-us-for-the-xmas-party-" class="hash-link" aria-label="Direct link to Join Us for the Xmas Party! 🎉" title="Direct link to Join Us for the Xmas Party! 🎉" translate="no">​</a></h2>
<p>What's a launch without a party? We're hosting a cozy <a href="https://discord.gg/NQ9RNkyk?event=1450815808325423114" target="_blank" rel="noopener noreferrer" class="">Discord call on <strong>December 23rd at 7 AM PT / 10 AM ET / 4 PM CET</strong></a> to unwrap all the gifts together!</p>
<p>We'll be:</p>
<ul>
<li class="">Doing live demos of all 4 new features</li>
<li class="">Answering your questions</li>
<li class="">Sharing what's coming in 2026</li>
<li class="">Having a good time with the community</li>
</ul>
<p>Make sure to <a href="https://discord.gg/NQ9RNkyk?event=1450815808325423114" target="_blank" rel="noopener noreferrer" class="">join our Discord and RSVP to the event</a> - we'd love to see you there! Bring your hot cocoa ☕</p>
<p>And now, let's start unpacking! As the olden song goes, <em>"On the Xmas Launch Day, my true full-stack batteries included web framework gave me"</em>:</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-gift-1-react-19-support">🎁 Gift #1: React 19 Support<a href="https://wasp.sh/blog/2025/12/17/wasp-xmas-launch#-gift-1-react-19-support" class="hash-link" aria-label="Direct link to 🎁 Gift #1: React 19 Support" title="Direct link to 🎁 Gift #1: React 19 Support" translate="no">​</a></h2>
<p>It's here! We're bringing React 19 to Wasp so you can take advantage of all the latest and greatest features from the React team.</p>
<p>People have been asking for this for a while now, and we're excited to finally deliver it:</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="User excited about React 19 support" src="https://wasp.sh/img/xmas-launch/react19-testimonial.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">It's Christmas, so no better time to make your wishes come true!</figcaption></figure></div>
<p>With React 19 support, you'll get access to some exciting improvements - new Actions and form APIs, a bunch of new hooks, ref as a prop, and <a href="https://react.dev/blog/2024/12/05/react-19" target="_blank" rel="noopener noreferrer" class="">more</a>!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-gift-2-claude-code-plugin-for-wasp">🎁 Gift #2: Claude Code Plugin for Wasp<a href="https://wasp.sh/blog/2025/12/17/wasp-xmas-launch#-gift-2-claude-code-plugin-for-wasp" class="hash-link" aria-label="Direct link to 🎁 Gift #2: Claude Code Plugin for Wasp" title="Direct link to 🎁 Gift #2: Claude Code Plugin for Wasp" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Claude Code Wasp plugin" src="https://wasp.sh/img/xmas-launch/cc-wasp-plugin.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">The Wasp plugin for Claude Code - making AI an expert Wasp developer!</figcaption></figure></div>
<p>You're gonna love this one. We keep hearing from so many developers what a great fit Claude Code combined with Wasp is, so we decided to take things one step further - introducing official <strong>Claude Code plugin for Wasp</strong>!</p>
<p>If you haven't heard of Claude Code plugins yet - it's a way to extend Claude with custom functionality. Think of it as giving Claude superpowers for specific frameworks and workflows.</p>
<p>Here's what it does:</p>
<ul>
<li class=""><strong>Knows Wasp inside-out</strong> - Plugs in Wasp best practices and points Claude to the right documentation (even accounting for your Wasp version!)</li>
<li class=""><strong>Guides you through common tasks</strong> - Adding auth, email sending, deployment, and more</li>
<li class=""><strong>Embedded workflows</strong> - Common patterns and advice baked right in, so Claude can help you build the "Wasp way"</li>
</ul>
<p>It's like having a Wasp expert sitting next to you while you code. AI-assisted development just got a whole lot better for Wasp developers! 🤖</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-gift-3-polar-integration-in-open-saas">🎁 Gift #3: Polar Integration in Open SaaS<a href="https://wasp.sh/blog/2025/12/17/wasp-xmas-launch#-gift-3-polar-integration-in-open-saas" class="hash-link" aria-label="Direct link to 🎁 Gift #3: Polar Integration in Open SaaS" title="Direct link to 🎁 Gift #3: Polar Integration in Open SaaS" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Support for Polar!" src="https://wasp.sh/img/xmas-launch/polar-code-example.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Ooh yeah - make it snow! 🐻‍❄️</figcaption></figure></div>
<p><a href="https://polar.sh/" target="_blank" rel="noopener noreferrer" class="">Polar</a> is now the hottest way to add billing to your SaaS. Think of it like Wasp, but for payment systems - it's extremely fast to add and does a ton of heavy lifting for you (subscriptions, usage-based billing, and MoR). It's also 100% open-source, so no wonder we like it!</p>
<p>We're integrating Polar directly into <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a>, making it even simpler to start accepting payments and building your SaaS business.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="-gift-4-wasp-news">🎁 Gift #4: Wasp News<a href="https://wasp.sh/blog/2025/12/17/wasp-xmas-launch#-gift-4-wasp-news" class="hash-link" aria-label="Direct link to 🎁 Gift #4: Wasp News" title="Direct link to 🎁 Gift #4: Wasp News" translate="no">​</a></h2>
<p>Stay in the loop without leaving your terminal! We're introducing Wasp News - a new feature that brings you important updates, tips, and announcements right in your CLI. Never miss a beat with what's happening in the Wasp ecosystem.</p>
<p>All you have to do is run <code>wasp news</code> in your terminal and you'll be up to date!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="see-you-on-the-23rd-">See You on the 23rd! 📅<a href="https://wasp.sh/blog/2025/12/17/wasp-xmas-launch#see-you-on-the-23rd-" class="hash-link" aria-label="Direct link to See You on the 23rd! 📅" title="Direct link to See You on the 23rd! 📅" translate="no">​</a></h2>
<p><strong>December 23rd</strong> is the day! We'll be sharing:</p>
<ul>
<li class="">Full feature announcements with all the details</li>
<li class="">Video demos and tutorials</li>
<li class="">Fun stuff on Twitter/X</li>
</ul>
<p>Make sure you're following us so you don't miss the unwrapping:</p>
<ul>
<li class=""><a href="https://twitter.com/WaspLang" target="_blank" rel="noopener noreferrer" class="">Twitter/X</a> - We'll be tweeting all day</li>
<li class=""><a href="https://discord.gg/NQ9RNkyk?event=1450815808325423114" target="_blank" rel="noopener noreferrer" class="">Discord</a> - Join the celebration with the community</li>
<li class=""><a href="https://github.com/wasp-lang/wasp" target="_blank" rel="noopener noreferrer" class="">GitHub</a> - Where all the magic happens</li>
</ul>
<p>That's all for now - we don't want to give away too much! Keep an eye out for the full announcement on <strong>December 23rd</strong>.</p>
<p>Stay cozy, keep building, and get ready to unwrap some presents! 🎁🐝</p>]]></content:encoded>
            <author>matija@wasp-lang.dev (Matija Sosic)</author>
            <category>wasp</category>
            <category>launch</category>
            <category>update</category>
        </item>
        <item>
            <title><![CDATA[Wasp Design-AI-thon: Winner Announcement 🏆 🎉]]></title>
            <link>https://wasp.sh/blog/2025/11/10/design-ai-thon-winners</link>
            <guid>https://wasp.sh/blog/2025/11/10/design-ai-thon-winners</guid>
            <pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<ImgWithCaption]]></description>
            <content:encoded><![CDATA[<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Boi celebrating the design-ai-thon winners!" src="https://wasp.sh/img/design-ai-thon-winners/design-ai-thon-banner.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>Aaaand the results are in! After 10 days of intense vibes-fueled creative energy, <strong>we're thrilled to announce the winners of Wasp's first ever Design-AI-thon</strong>.</p>
<p>We have to admit - judging was <em>tough</em>. We asked you to reimagine Wasp's website, and you absolutely delivered. Some of you went full Figma wizard mode, others shipped actual working code, and one person even brought in a full vibe-coded OS (more on that later 👀).</p>
<!-- -->
<p>Thank you to everyone who participated! Whether you submitted or just followed along, your enthusiasm made this event special. Now, without further ado, let's get to the winners across our three categories: Best Overall Project, Most Fun/Innovative, and Most Technically Impressive.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="best-overall-project-">Best Overall Project 🥇<a href="https://wasp.sh/blog/2025/11/10/design-ai-thon-winners#best-overall-project-" class="hash-link" aria-label="Direct link to Best Overall Project 🥇" title="Direct link to Best Overall Project 🥇" translate="no">​</a></h2>
<p><strong>Winner: Zeke</strong></p>
<video width="100%" controls=""><source src="/img/design-ai-thon-winners/zeke-entry.mp4" type="video/mp4"></video>
<p>When we said "no rules," Zeke took it to heart and delivered a complete overhaul of Wasp's visual identity. We're talking full redesign - new layout, refreshed copy, the works. But what really made this submission stand out wasn't just the end result though it looked great, it was the <em>journey</em>.</p>
<p>Zeke didn't just drop a Figma file and call it a day. He documented his entire thought process, breaking down every design decision, every copy change, and the reasoning behind it all. He even created 6 different drawings of boi!</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Six different drawings of Wasp's boi mascot by Zeke" src="https://wasp.sh/img/design-ai-thon-winners/zeke-bois.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">Boinanza.</figcaption></figure></div>
<p>The attention to detail combined with the comprehensive approach made this an easy pick for Best Overall Project. Zeke clearly put serious time and energy into understanding what Wasp needed, and it shows.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="most-fun--innovative-">Most Fun / Innovative 🎨<a href="https://wasp.sh/blog/2025/11/10/design-ai-thon-winners#most-fun--innovative-" class="hash-link" aria-label="Direct link to Most Fun / Innovative 🎨" title="Direct link to Most Fun / Innovative 🎨" translate="no">​</a></h2>
<p><strong>Winner: Étienne Gagnier</strong></p>
<p><strong>Figma design</strong>: <a href="https://ide-tidy-39147958.figma.site/" target="_blank" rel="noopener noreferrer" class="">link</a></p>
<video width="100%" controls=""><source src="/img/design-ai-thon-winners/etienne-fun.mp4" type="video/mp4"></video>
<p>Remember when we said we wanted something you don't see every day? Etienne delivered exactly that. Imagine if Wasp's website was designed like a developer's notebook, complete with that hand-drawn, sketch-style aesthetic.</p>
<p>It's playful, it's memorable, and it's <em>fun</em>. In a world where most dev tool websites look like they came from the same template, Etienne's design stands out by leaning into personality over polish. The notebook-style approach feels authentic and creative - like you're peeking into someone's actual design process.</p>
<p>This is the kind of out-of-the-box thinking we were hoping to see. It's a design that makes you smile, and honestly, that's exactly the vibe Wasp is all about (next to building web apps).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="most-technically-impressive-">Most Technically Impressive 💻<a href="https://wasp.sh/blog/2025/11/10/design-ai-thon-winners#most-technically-impressive-" class="hash-link" aria-label="Direct link to Most Technically Impressive 💻" title="Direct link to Most Technically Impressive 💻" translate="no">​</a></h2>
<p><strong>Winner: <a href="https://github.com/bO-05" target="_blank" rel="noopener noreferrer" class="">Adam (bO-05)</a></strong></p>
<p><strong>See it in action</strong>: <a href="https://wasp-clone-1.vercel.app/" target="_blank" rel="noopener noreferrer" class="">link</a></p>
<video width="100%" controls=""><source src="/img/design-ai-thon-winners/adam-entry.mp4" type="video/mp4"></video>
<p>Remember that vibe-coded OS we mentioned earlier? Yeah, this is it. Adam didn't just redesign Wasp's website - he built an <em>entire interactive desktop OS playground</em> where you can explore Wasp like you're using an actual operating system.</p>
<p>We're talking a fully functional virtual desktop complete with file management, a working terminal, notes app, and even an AI-powered chat assistant. Want to save your work? There's optional cloud sync with authentication. Adam literally shipped a whole OS experience for a website redesign competition.</p>
<p>The technical execution here is wild, but what really sealed the deal was how it all comes together with this futuristic, high-vibe aesthetic. It's not just technically impressive - it's <em>cool</em>. You could spend 20 minutes just playing around with it, which is exactly what I did.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="thank-you">Thank You!<a href="https://wasp.sh/blog/2025/11/10/design-ai-thon-winners#thank-you" class="hash-link" aria-label="Direct link to Thank You!" title="Direct link to Thank You!" translate="no">​</a></h2>
<p>A huge thank you to everyone who submitted - the quality and creativity across the board was incredible. It was genuinely tough to narrow it down to just three winners. Each submission brought something special, and we loved seeing how you interpreted Wasp's personality in your own unique way.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-prizes">The Prizes<a href="https://wasp.sh/blog/2025/11/10/design-ai-thon-winners#the-prizes" class="hash-link" aria-label="Direct link to The Prizes" title="Direct link to The Prizes" translate="no">​</a></h2>
<p>Our three winners will each be receiving one of these beautifully designed pieces of hardware:</p>
<ul>
<li class=""><a href="https://teenage.engineering/store/ob-4-ochre" target="_blank" rel="noopener noreferrer" class="">OB-4 Bluetooth Loudspeaker by teenage engineering</a></li>
<li class=""><a href="https://usetrmnl.com/" target="_blank" rel="noopener noreferrer" class="">TRMNL - E-ink companion for your desk</a></li>
<li class=""><a href="https://play.date/shop/playdate/" target="_blank" rel="noopener noreferrer" class="">Playdate - A tiny handheld game system. With The Crank.</a></li>
</ul>
<p>Congrats again to Zeke, Etienne, and Adam!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="inspired-to-build-your-next-app">Inspired to build your next app?<a href="https://wasp.sh/blog/2025/11/10/design-ai-thon-winners#inspired-to-build-your-next-app" class="hash-link" aria-label="Direct link to Inspired to build your next app?" title="Direct link to Inspired to build your next app?" translate="no">​</a></h2>
<p>Want to start building with Wasp? Check out <a href="https://opensaas.sh/" target="_blank" rel="noopener noreferrer" class="">Open SaaS</a> - a free, open-source SaaS starter kit built with Wasp that includes authentication, payments, email, analytics, and more out of the box.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-next">What's Next?<a href="https://wasp.sh/blog/2025/11/10/design-ai-thon-winners#whats-next" class="hash-link" aria-label="Direct link to What's Next?" title="Direct link to What's Next?" translate="no">​</a></h2>
<p>If you want to stay in the loop for future events, challenges, and launch weeks, join us on <a href="https://discord.com/invite/rzdnErX" target="_blank" rel="noopener noreferrer" class="">Discord</a> or follow us on <a href="https://twitter.com/WaspLang" target="_blank" rel="noopener noreferrer" class="">Twitter/X</a>.</p>
<p>Until next time - keep b(uzz)|(ild)ing! 🐝</p>]]></content:encoded>
            <author>matija@wasp-lang.dev (Matija Sosic)</author>
            <category>hackathon</category>
        </item>
        <item>
            <title><![CDATA[Wasp Launch Week 11 Design-AI-thon]]></title>
            <link>https://wasp.sh/blog/2025/10/08/design-ai-thon</link>
            <guid>https://wasp.sh/blog/2025/10/08/design-ai-thon</guid>
            <pubDate>Wed, 08 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<ImgWithCaption]]></description>
            <content:encoded><![CDATA[<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Launch Week 11 design-ai-thon is here" src="https://wasp.sh/img/design-ai-thon/banner.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>As promised at the start of this Launch Week, we're bringing our hackathon back! And this time, it's a bit special - <strong>we're asking you to give a revamp to Wasp's website</strong>! Our design is kinda dated and looks like it was done by a backend engineer (looking at myself), and we think it's about time we did something about it!</p>
<!-- -->
<p>We want to come up with something <em>fun</em> and not what you see every day, which reflects our style and personality.</p>
<p>The only rule is: there are no rules!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="key-things-to-know">Key things to know<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#key-things-to-know" class="hash-link" aria-label="Direct link to Key things to know" title="Direct link to Key things to know" translate="no">​</a></h2>
<ul>
<li class="">You have 10 days to <strong>create a redesigned mock-up of <a class="" href="https://wasp.sh/">Wasp's website</a></strong>
<ul>
<li class="">Starting <strong>10:00 am PT Friday, Oct 10th, 2025</strong></li>
<li class="">The submission deadline is <strong>11:59 pm Sunday, midnight PT, Oct 19th, 2025</strong></li>
</ul>
</li>
<li class="">Use any tool or format - code, Figma design, drawing. Anything.</li>
<li class="">You're welcome to adapt the copy on the website. If you have an idea for a tagline you think would work better (e.g. "Laravel for JS"), we'd love to see it!</li>
</ul>
<p>You can find Wasp's existing website code and use it as a starting point <a href="https://github.com/wasp-lang/wasp/blob/release/web/src/pages/index.jsx" target="_blank" rel="noopener noreferrer" class="">here</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">FAQ<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#faq" class="hash-link" aria-label="Direct link to FAQ" title="Direct link to FAQ" translate="no">​</a></h2>
<p>Here are answers to some questions you might have. If there is something we haven't covered, ping us on <a href="https://discord.com/invite/rzdnErX" target="_blank" rel="noopener noreferrer" class="">Discord</a>!</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-should-i-create-what-level-of-fidelity-should-it-be">What should I create? What level of fidelity should it be?<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#what-should-i-create-what-level-of-fidelity-should-it-be" class="hash-link" aria-label="Direct link to What should I create? What level of fidelity should it be?" title="Direct link to What should I create? What level of fidelity should it be?" translate="no">​</a></h3>
<p>We leave this part up to you. Whatever you think might be a good way to represent your idea on how to upgrade Wasp's page design and/or branding. It could be a page you actually coded and deployed, a Figma/Illustrator design, a drawing or a video in which you present your vision.</p>
<p>Maybe even a poem. Ok, probably not, but would be interesting to see someone give it a try.</p>
<p>To get your juices flowing, here's an example we played around with internally :</p>
<div class="video-container"><iframe src="https://www.youtube.com/embed/5UWX6XkkbL4?si=sQPROo5PGfFWKmn2" frameborder="1" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-much-of-the-website-should-i-mock-up">How much of the website should I mock up?<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#how-much-of-the-website-should-i-mock-up" class="hash-link" aria-label="Direct link to How much of the website should I mock up?" title="Direct link to How much of the website should I mock up?" translate="no">​</a></h3>
<p>You can go as far as you want. As a minimum, we recommend focusing on the Above-The-Fold aka Hero section of the page. That is the portion of the page you see on your screen when it loads for the first time, before you scroll down.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-tools-should-i-use-can-i-use-ai">Which tools should I use? Can I use AI?<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#which-tools-should-i-use-can-i-use-ai" class="hash-link" aria-label="Direct link to Which tools should I use? Can I use AI?" title="Direct link to Which tools should I use? Can I use AI?" translate="no">​</a></h3>
<p>Of course! As the title says, it's a design-AI-thon. We believe that AI has made better design more accessible to anyone (even backend engineers, although that's a tough one) and would love to see it in action!</p>
<p>You're of course also free to go old-school and do it Figma/Sketch/Photoshop/Illustrator or any other design tool out there.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="prizes">Prizes<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#prizes" class="hash-link" aria-label="Direct link to Prizes" title="Direct link to Prizes" translate="no">​</a></h2>
<p>There are 3 categories, and there will be prizes for:</p>
<ul>
<li class="">Best overall project</li>
<li class="">Most fun / innovative</li>
<li class="">Most technically impressive</li>
</ul>
<p>For the prizes themselves, we'll give away three beautifully designed pieces of hardware. We think that is just right for the theme of this competition, and will inspire you to channel your inner Jony Ive or Dieter Rams.</p>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img alt="Design-AI-thon prizes" src="https://wasp.sh/img/design-ai-thon/prizes.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em">I have a very strong urge to cancel this design-ai-thon and ship all the prizes to myself.</figcaption></figure></div>
<ul>
<li class="">Prize #1: <a href="https://teenage.engineering/store/ob-4-ochre" target="_blank" rel="noopener noreferrer" class="">OB-4 Bluetooth Loudspeaker by teenage engineering</a></li>
<li class="">Prize #2: <a href="https://usetrmnl.com/" target="_blank" rel="noopener noreferrer" class="">TRMNL - E-ink companion for your desk</a></li>
<li class="">Prize #3: <a href="https://play.date/shop/playdate/" target="_blank" rel="noopener noreferrer" class="">Playdate - A tiny handheld game system. With The Crank.</a></li>
</ul>
<p>Special thanks goes to folks at TRMNL for offering to sponsor this event by gifting a device! Seems like <a href="https://x.com/MatijaSosic/status/1974116112548909200" target="_blank" rel="noopener noreferrer" class="">me tweeting a lot of stuff</a> eventually pays off!</p>
<p><em><strong>A lil' disclaimer</strong>: If for some reason we can't get our hands on one of the prizes, or can't ship it to your location, we'll swap it out for something just as cool (promise!).</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="submission">Submission<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#submission" class="hash-link" aria-label="Direct link to Submission" title="Direct link to Submission" translate="no">​</a></h2>
<p>You should submit your project using the <a href="https://e44cy1h4s0q.typeform.com/to/ZjN0Dr0j" target="_blank" rel="noopener noreferrer" class="">submission form</a> before 11:59 pm PT, Sunday, October 19th, 2025.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="judging">Judging<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#judging" class="hash-link" aria-label="Direct link to Judging" title="Direct link to Judging" translate="no">​</a></h2>
<p>The Wasp team will judge the winners for each category. We will be looking for:</p>
<ul>
<li class="">Creativity &amp; thinking out of the box</li>
<li class="">How well it showcases Wasp’s playful personality</li>
<li class="">Visually pleasing</li>
<li class="">Attention to detail</li>
<li class="">Fun!</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="rules">Rules<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#rules" class="hash-link" aria-label="Direct link to Rules" title="Direct link to Rules" translate="no">​</a></h2>
<ul>
<li class="">Multiple submissions are allowed. You can do as many as you want.</li>
<li class="">You must submit before the deadline (no late entries)</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="additional-info">Additional info<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#additional-info" class="hash-link" aria-label="Direct link to Additional info" title="Direct link to Additional info" translate="no">​</a></h2>
<ul>
<li class="">By making a submission, you grant Wasp permission to use screenshots, code snippets, and/or links to your project on our Twitter, blog, website, email updated, and in the Wasp discord server.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="see-you-there">See you there!<a href="https://wasp.sh/blog/2025/10/08/design-ai-thon#see-you-there" class="hash-link" aria-label="Direct link to See you there!" title="Direct link to See you there!" translate="no">​</a></h2>
<div class="figure-container" style="display:flex;justify-content:center"><figure><img style="width:500px" alt="See you there!" src="https://wasp.sh/img/design-ai-thon/boi-painter.png"><figcaption class="image-caption" style="font-style:italic;opacity:0.8;font-size:1.1rem;margin-top:0.2em"></figcaption></figure></div>
<p>That's - it have fun. Find your inner artist, pair it with AI and let sparks fly. We can't wait to see what you come up with!</p>]]></content:encoded>
            <author>matija@wasp-lang.dev (Matija Sosic)</author>
            <category>launch-week</category>
            <category>update</category>
        </item>
    </channel>
</rss>