A declarative blueprint assembling itself into full-stack machinery while a gray-blue British shorthair cat looks on

Wasp: The Batteries-Included Full-Stack Framework That Writes Its Own Plumbing

The Gray Cat
The Gray Cat
0 views

Building a full-stack web app in JavaScript usually starts the same way: spin up a React frontend, scaffold a Node backend, pick an ORM, wire up an API layer, then spend days bolting on authentication, background jobs, and email before you write a single line of the thing you actually wanted to build. Wasp exists to delete that ceremony. It's a Rails-like, batteries-included framework for JavaScript and TypeScript that lets you describe the high-level shape of your app — its routes, pages, auth methods, data operations, and jobs — in one concise spec, and then leans on a real compiler to generate all the glue code between React, Node.js/Express, and Prisma.

The trick that sets Wasp apart from the meta-framework crowd is that it isn't just a library you import. It's a compiler with its own spec file. You write the business logic; Wasp writes the plumbing: routing, the RPC layer, auth flows, type definitions, cache invalidation, and deployment config. It's a great fit for SaaS products, internal tools, and side projects where you want a real backend out of the box, and it has leaned hard into being friendly to AI coding agents — the small declarative surface means agents have clear guardrails and burn far fewer tokens.

Why Wasp Feels Different

Most full-stack JavaScript stacks are assembled. Wasp is declared. Instead of stitching together a dozen libraries and praying they agree on conventions, you hand Wasp a spec describing what your app needs, and it produces a coherent, type-safe application around your own code. A few capabilities come standard:

  • Full-stack authentication — username/password, email with verification, and OAuth providers like Google, GitHub, Discord, Microsoft, Slack, and Keycloak, with declarative protected routes.
  • Operations (Queries and Actions) — a zero-API RPC layer that lets the client call server functions like local functions, fully type-safe, with no REST or GraphQL boilerplate.
  • Automatic cache invalidation — declared entity dependencies drive React Query cache updates for you.
  • Automatic CRUD — generate the five standard operations plus matching React hooks from a single declaration.
  • Background and recurring jobs — cron-scheduled work via a PgBoss executor.
  • Full-stack type safety — types flow from your Prisma model through the server operation and into your React component automatically.
  • Single-command deploymentwasp deploy ships to Fly.io or Railway, and you own the generated code, so there's no lock-in.

Getting It Onto Your Machine

A quick but important note: Wasp is a compiler, not a plain npm dependency. The bare wasp package on npm is an unrelated project, so you don't npm install wasp into your app. Instead you install the Wasp CLI globally. It runs on macOS, Linux, and Windows via WSL.

npm install -g @wasp.sh/wasp-cli@latest
yarn global add @wasp.sh/wasp-cli@latest

Once the CLI is on your path, you scaffold a new project and start the dev server:

wasp new my-app
cd my-app
wasp start

That single wasp start compiles your spec, generates the glue code, runs migrations, and brings up both the React client and the Node server together.

Describing An App In One File

The heart of a Wasp project is its spec. As of v0.24.0, you author it in TypeScript as main.wasp.ts using the @wasp.sh/spec package, which brings proper editor support, imports, and type checking. Your data models live separately in a standard schema.prisma file. Here's the smallest interesting spec — an app with username-and-password auth:

import { app } from "@wasp.sh/spec"

export default app({
  name: "RecipeApp",
  wasp: { version: "^0.24" },
  title: "My Recipes",
  auth: {
    methods: { usernameAndPassword: {} },
    onAuthFailedRedirectTo: "/login",
    userEntity: "User",
  },
})

You haven't written a login form, a session handler, or a redirect guard, yet you now have all three. The compiler reads this spec alongside your React, Node, and Prisma code and generates the signup and login flows, session handling, and protected-route redirects. Add another method to the methods object — say google: {} — and the OAuth plumbing appears too. This is the whole philosophy in miniature: declare the intent, let the compiler produce the implementation.

Calling The Server Without An API

Wasp's most addictive feature is its Operations layer. Operations are plain Node.js functions that run on the server but are invoked from the client as if they were local. There are two flavors: Queries read data and benefit from automatic cache invalidation, while Actions perform writes and side effects. You never define a REST endpoint or a GraphQL resolver.

Because of full-stack type safety, a query that returns Recipe[] makes the data typed as Recipe[] in your component with zero manual typing. You write the server function:

import { type GetRecipes } from "wasp/server/operations"
import { type Recipe } from "wasp/entities"

export const getRecipes: GetRecipes<void, Recipe[]> = async (_args, context) => {
  return context.entities.Recipe.findMany({
    orderBy: { id: "desc" },
  })
}

Then you consume it on the client with a generated hook. The recipes value below is already typed as Recipe[], with loading and error states handled by the underlying React Query integration:

import { useQuery, getRecipes } from "wasp/client/operations"

export const RecipeList = () => {
  const { data: recipes, isLoading } = useQuery(getRecipes)

  if (isLoading) return <p>Loading…</p>

  return (
    <ul>
      {recipes?.map((recipe) => (
        <li key={recipe.id}>{recipe.title}</li>
      ))}
    </ul>
  )
}

The context.entities object hands you the relevant Prisma models scoped to the operation, so you get database access without importing or configuring a client yourself.

Generating CRUD From A Declaration

When an entity needs the usual five operations, writing them by hand is busywork. Wasp's automatic CRUD generates getAll, get, create, update, and delete on the server plus matching React hooks on the client, all from one declaration in the spec:

import { app, crud } from "@wasp.sh/spec"
import { createTask } from "./src/tasks" with { type: "ref" }

export default app({
  spec: [
    crud("Tasks", "Task", {
      getAll: {},
      get: {},
      create: { overrideFn: createTask },
      update: {},
      delete: {},
    }),
  ],
})

Each operation can be left as {} to use the default implementation, or pointed at your own function with overrideFn when you need custom validation or authorization. On the client, the generated hooks read naturally:

import { Tasks } from "wasp/client/crud"

export const MainPage = () => {
  const { data: tasks } = Tasks.getAll.useQuery()
  const createTask = Tasks.create.useAction()

  return (
    <ul>
      {tasks?.map((task) => (
        <li key={task.id}>{task.description}</li>
      ))}
    </ul>
  )
}

That's a fully working data layer for an entity in roughly ten lines of spec, with the cache invalidation already handled when create runs.

Scheduling Work That Runs On Its Own

Real apps need things to happen on a timer — digest emails, cleanup, syncs. Wasp models this with declarative jobs backed by a PgBoss executor. You declare the job and its cron schedule in the spec:

import { app, job } from "@wasp.sh/spec"
import { mySpecialJob } from "./src/workers/bar" with { type: "ref" }

export default app({
  spec: [
    job(mySpecialJob, {
      executor: "PgBoss",
      schedule: { cron: "0 * * * *", args: { name: "Johnny" } },
    }),
  ],
})

The worker itself is an ordinary, fully typed function. It receives the job arguments plus a context carrying your database entities, exactly like an operation:

import { type MySpecialJob } from "wasp/server/jobs"
import { type Task } from "wasp/entities"

type Input = { name: string }
type Output = { tasks: Task[] }

export const mySpecialJob: MySpecialJob<Input, Output> = async ({ name }, context) => {
  const tasks = await context.entities.Task.findMany({})
  return { tasks }
}

With the cron schedule in place, this runs automatically on the hour — no separate scheduler service to provision, no manual invocation. Drop the schedule and you have an on-demand background job you can trigger from an action instead.

Shipping It Without The Yak-Shaving

Because Wasp generates a real React + Node + Postgres application, deploying it is a single command. There's first-class support for Fly.io and Railway:

wasp deploy fly deploy
wasp deploy railway launch

For CI/CD you can target an existing project, which makes Wasp comfortable inside a GitHub Actions pipeline. And since you own the generated code, nothing stops you from deploying the underlying React, Node, and Postgres pieces manually to any host you like. If you want to skip even more of the early scaffolding, the team maintains OpenSaaS, an open-source, MIT-licensed SaaS starter built on Wasp that ships auth, Stripe and Lemon Squeezy payments, an admin dashboard, an Astro blog, S3 file uploads, email, and Playwright end-to-end tests out of the box.

A Few Honest Caveats

Wasp is still pre-1.0 — v0.24.0 is officially Beta — and that comes with the usual trade-offs. Minor versions can ship breaking changes; the move from the old custom .wasp DSL to the new TypeScript main.wasp.ts spec is a recent example, so plan for some migration work on upgrades. The stack is opinionated: you get React, Node/Express, and Prisma, which is wonderful if that's your stack and a dealbreaker if you wanted Vue, Svelte, or a different ORM. The community and ecosystem are smaller than Next.js, so there are fewer third-party integrations and fewer answers waiting for you online. And because there's a compiler in the middle, debugging occasionally means peeking at the generated code and learning "the Wasp way." None of these are fatal, but they're worth knowing going in.

The Takeaway

Wasp's bet is that most of a full-stack app is plumbing nobody enjoys writing, and that plumbing can be inferred from a short, declarative spec. In practice that bet pays off: auth, a type-safe API layer, CRUD, jobs, email, and deployment all fall out of a single TypeScript file plus your own React and Node code. If you're starting a SaaS product or an internal tool and you'd rather spend week one on features than on glue — or you want a stack that's easy for an AI agent to reason about — Wasp is well worth a wasp new. It's young, it's opinionated, and it's pre-1.0, but it delivers something genuinely rare in the JavaScript world: a framework that writes its own boilerplate so you don't have to.