# Wasp 0.26 Full Documentation
This is the full documentation for the latest version of Wasp.
For other versions, see the links below.
## Full Documentation by Version
- [latest (currently 0.26)](https://wasp.sh/llms-full.txt)
- [0.26](https://wasp.sh/llms-full-0.26.txt)
- [0.25](https://wasp.sh/llms-full-0.25.txt)
- [0.24](https://wasp.sh/llms-full-0.24.txt)
- [0.23](https://wasp.sh/llms-full-0.23.txt)
- [0.22](https://wasp.sh/llms-full-0.22.txt)
- [0.21](https://wasp.sh/llms-full-0.21.txt)
- [0.20](https://wasp.sh/llms-full-0.20.txt)
- [0.19](https://wasp.sh/llms-full-0.19.txt)
- [0.18](https://wasp.sh/llms-full-0.18.txt)
- [0.17](https://wasp.sh/llms-full-0.17.txt)
- [0.16](https://wasp.sh/llms-full-0.16.txt)
- [0.15](https://wasp.sh/llms-full-0.15.txt)
- [0.14](https://wasp.sh/llms-full-0.14.txt)
- [0.13](https://wasp.sh/llms-full-0.13.txt)
- [0.12](https://wasp.sh/llms-full-0.12.txt)
- [0.11.8](https://wasp.sh/llms-full-0.11.8.txt)
---
# Docs
## Getting started / What's Wasp?
:::note
If you are looking for the installation instructions, check out the [Quick Start](https://wasp.sh/docs/quick-start) section.
:::
We will give a brief overview of what Wasp is, how it works on a high level and when to use it.
### Wasp is a tool to build modern web applications
It is an opinionated way of building **full-stack web applications**. It takes care of all three major parts of a web application: **client** (front-end), **server** (back-end) and **database**.
#### Works well with your existing stack
Wasp is not trying to do everything at once but rather focuses on the complexity that arises from connecting all the parts of the stack (client, server, database, deployment).
Wasp is using **React**, **Node.js** and **Prisma** under the hood and relies on them to define web components and server queries and actions.
#### Wasp's secret sauce
At the core is the Wasp compiler which takes the Wasp Spec and your Javascript code and outputs the client app, server app and deployment code.

How the magic happens ๐
The cool thing about having a compiler that understands your code is that it can do a lot of things for you.
Define your app in the Wasp file and get:
- login and signup with Auth UI components,
- full-stack type safety,
- e-mail sending,
- async processing jobs,
- React Query powered data fetching,
- security best practices,
- and more.
You don't need to write any code for these features, Wasp will take care of it for you ๐คฏ And what's even better, Wasp also maintains the code for you, so you don't have to worry about keeping up with the latest security best practices. As Wasp updates, so does your app.
### So what does the code look like?
Let's say you want to build a web app that allows users to **create and share their favorite recipes**.
Let's start with the `main.wasp.ts` file: it is the central spec file of your app, where you describe the app from the high level.
Let's give our app a title and let's immediately turn on the full-stack authentication via username and password:
```ts title="main.wasp.ts"
import { app } from "@wasp.sh/spec"
export default app({
name: "RecipeApp",
wasp: { version: "^0.26" },
title: "My Recipes",
head: [""],
auth: {
methods: { usernameAndPassword: {} },
onAuthFailedRedirectTo: "/login",
userEntity: "User",
},
// ...
})
```
Let's then add the data models for your recipes. Wasp understands and uses the models from the `schema.prisma` file. We will want to have Users and Users can own Recipes:
```prisma title="schema.prisma"
...
// Data models are defined using Prisma Schema Language.
model User {
id Int @id @default(autoincrement())
recipes Recipe[]
}
model Recipe {
id Int @id @default(autoincrement())
title String
description String?
userId Int
user User @relation(fields: [userId], references: [id])
}
```
Next, let's define how to do something with these data models!
We do that by defining Operations, in this case, a Query `getRecipes` and Action `addRecipe`, which are in their essence Node.js functions that execute on the server and can, thanks to Wasp, very easily be called from the client.
First, we define these Operations in our `main.wasp.ts` file, so Wasp knows about them and can "beef them up":
```ts title="main.wasp.ts"
import { action, app, query } from "@wasp.sh/spec"
import { getRecipes, addRecipe } from "./src/recipe/operations" with { type: "ref" }
export default app({
// ...
spec: [
// ...
// Queries have automatic cache invalidation and are type-safe.
query(getRecipes, { entities: ["Recipe"] }),
// Actions are type-safe and can be used to perform side-effects.
action(addRecipe, { entities: ["Recipe"] }),
],
})
```
... and then implement them in our Javascript (or TypeScript) code (we show just the query here, using TypeScript):
```ts title="src/recipe/operations.ts"
// Wasp generates the types for you.
import { type GetRecipes } from "wasp/server/operations";
import { type Recipe } from "wasp/entities";
export const getRecipes: GetRecipes<{}, Recipe[]> = async (_args, context) => {
return context.entities.Recipe.findMany( // Prisma query
{ where: { user: { id: context.user.id } } }
);
};
export const addRecipe ...
```
Now we can very easily use these in our React components!
For the end, let's create a home page of our app.
First, we define it in `main.wasp.ts`:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { HomePage } from "./src/pages/HomePage" with { type: "ref" }
export default app({
// ...
spec: [
// ...
route("HomeRoute", "/", page(HomePage, {
authRequired: true, // Will send user to /login if not authenticated.
})
),
],
})
```
and then implement it as a React component in JS/TS (that calls the Operations we previously defined):
```tsx title="src/pages/HomePage.tsx"
import { useQuery, getRecipes } from "wasp/client/operations"
import { type User } from "wasp/entities"
export function HomePage({ user }: { user: User }) {
// Due to full-stack type safety, `recipes` will be of type `Recipe[]` here.
const { data: recipes, isLoading } = useQuery(getRecipes) // Calling our query here!
if (isLoading) {
return
Loading...
}
return (
Recipes
{recipes
? recipes.map((recipe) => (
{recipe.title}
{recipe.description}
))
: "No recipes defined yet!"}
)
}
```
And voila! We are listing all the recipes in our app ๐
This was just a quick example to give you a taste of what Wasp is. For step by step tour through the most important Wasp features, check out the [Todo App tutorial](https://wasp.sh/docs/tutorial/create).
:::note
Above we skipped defining `/login` and `/signup` pages to keep the example a bit shorter, but those are very simple to do by using Wasp's Auth UI feature.
:::
### When to use Wasp
Wasp addresses the same core problems that typical web app frameworks are addressing, and it in big part [looks, swims and quacks](https://en.wikipedia.org/wiki/Duck_test) like a web app framework.
#### Best used for
- building full-stack web apps (like e.g. Airbnb or Asana)
- quickly starting a web app with industry best practices
- to be used alongside modern web dev stack (React and Node.js are currently supported)
#### Avoid using Wasp for
- building static/presentational websites
- to be used as a no-code solution
- to be a solve-it-all tool in a single language
### Wasp is a spec-driven framework
Wasp does not match typical expectations of a web app framework: it is not just a set of libraries. You describe your app in the `main.wasp.ts` spec file, and the compiler uses that spec together with your React, Node.js, and Prisma code to generate the application structure and glue code.
This spec-driven approach lets Wasp focus on one purpose: **building modern web applications with 10x less code and less stack-specific knowledge**.
## Getting started / Quick start
### Installation
Welcome, new Waspeteer ๐!
Let's create and run our first Wasp app in 3 short steps:
1. **To install Wasp on Linux / OSX / WSL (Windows), open your terminal and run:**
```shell
npm i -g @wasp.sh/wasp-cli@latest
```
โน๏ธ Wasp requires Node.js and npm, which are usually installed together: check below for [more details](#requirements).
2. **Then, create a new app by running:**
```shell
wasp new
```
3. **Finally, run the app:**
```shell
cd
wasp start
```
That's it ๐ You have successfully created a new full-stack web app and Wasp is serving both frontend and backend for you. Open the client URL that Wasp printed on startup (usually , unless it's taken) to see it.
But don't stop there! Turn your coding agent into a Wasp framework expert in the next step.
4. **Install Agent Plugin / Skills:**
**Claude Code:**
```bash
claude plugin marketplace add wasp-lang/wasp-agent-plugins
claude plugin install wasp@wasp-agent-plugins --scope project
```
**Other Agents (Cursor, Codex, Gemini, Copilot, OpenCode, etc.):**
```bash
npx skills add wasp-lang/wasp-agent-plugins
```
**Initialize the plugin / skills:** Invoke the `/wasp-plugin-init` skill to add Wasp knowledge to your agent's memory file (e.g. `CLAUDE.md`, `AGENTS.md`):
```bash
Run the '/wasp-plugin-init' skill.
```
For more info check out the [Wasp Agent Plugin / Skills](https://wasp.sh/docs/getting-started/agent-integration) page.
:::note[Something Unclear?]
Check [More Details](#more-details) section below if anything went wrong with the installation, or if you have additional questions.
:::
:::tip[Having trouble running Wasp?]
If you get stuck with a weird error while developing with Wasp, try running `wasp clean` - this is the Wasp equivalent of "turning it off and on again"! Do however let us know about the issue on our GitHub repo or Discord server.
:::
#### What next?
- [ ] ๐ **Check out the [Todo App tutorial](https://wasp.sh/docs/tutorial/create), which will take you through all the core features of Wasp!** ๐
- [ ] Join us on [Discord](https://discord.gg/rzdnErX)! Any feedback or questions you have, we are there for you.
- [ ] Follow Wasp development by subscribing to our newsletter: . We usually send 1 per month, and [Matija](https://github.com/matijaSos) does his best to unleash his creativity to make them engaging and fun to read \:D!
---
### More details
#### Requirements
You must have Node.js (and NPM) installed on your machine and available in `PATH`.
A version of Node.js must be >= 24.22.1.
If you need it, we recommend using [nvm](https://github.com/nvm-sh/nvm) for managing your Node.js installation version(s).
A quick guide on installing/using nvm
Install nvm via your OS package manager (`apt`, `pacman`, `homebrew`, ...) or via the [nvm](https://github.com/nvm-sh/nvm#install--update-script) install script.
Then, install a version of Node.js that you need:
```shell
nvm install 24
```
Finally, whenever you need to ensure a specific version of Node.js is used, run:
```shell
nvm use 24
```
to set the Node.js version for the current shell session.
You can run
```shell
node -v
```
to check the version of Node.js currently being used in this shell session.
Check NVM repo for more details: .
#### Installation {#detailed-installation}
**Linux / macOS (npm)**
Open your terminal and run:
```shell
npm i -g @wasp.sh/wasp-cli@latest
```
:::note[Looking for the old installer script?]
Check out the **[Legacy installer guide](https://wasp.sh/docs/guides/legacy/installer)** for instructions on how to keep using it or how to migrate to npm-based installation.
:::
**Windows**
With Wasp for Windows, we are almost there: Wasp is successfully compiling and running on Windows but there is a bug or two stopping it from fully working. Check it out [here](https://github.com/wasp-lang/wasp/issues/48) if you are interested in helping.
In the meantime, the best way to start using Wasp on Windows is by using [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). Once you set up Ubuntu on WSL, just follow Linux instructions for installing Wasp. You can refer to this [article](https://wasp.sh/blog/2023/11/21/guide-windows-development-wasp-wsl) if you prefer a step by step guide to using Wasp in WSL environment. If you need further help, reach out to us on [Discord](https://discord.gg/rzdnErX) - we have some community members using WSL that might be able to help you.
:::caution
If you are using WSL2, make sure that your Wasp project is not on the Windows file system, but instead on the Linux file system. Otherwise, Wasp won't be able to detect file changes, due to the [issue in WSL2](https://github.com/microsoft/WSL/issues/4739). If you use VS Code, install the [WSL extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) and open the project from inside WSL (run `code .` in the project directory). Without it, TypeScript support in VS Code won't work on WSL2.
:::
**From source**
If the other methods are not working for you or your OS is not supported, you can try building Wasp from the source.
To install from source, you need to clone the [wasp repo](https://github.com/wasp-lang/wasp), install [Cabal](https://cabal.readthedocs.io/en/stable/getting-started.html) on your machine and then run `cabal install` from the `waspc/` dir.
If you have never built Wasp before, this might take some time due to `cabal` downloading dependencies for the first time.
Check [waspc/](https://github.com/wasp-lang/wasp/tree/main/waspc) for more details on building Wasp from the source.
## Getting started / Starter templates
We created a few starter templates to help you get started with Wasp. Check out the list [below](#available-templates).
### Using a Template
Run `wasp new` to run the interactive mode for creating a new Wasp project.
It will ask you for the project name, and then for the template to use:
```
$ wasp new
Enter the project name (e.g. my-project) โธ MyFirstProject
Choose a starter template
[1] basic (default)
A basic starter template designed to help you get up and running quickly.
It features examples covering the most common use cases.
[2] minimal
A minimal starter template that features just a single page.
[3] saas
Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more.
Check out https://opensaas.sh/ for more details.
โธ 1
๐ --- Creating your project from the "basic" template... -------------------------
Created a new Wasp app in `./MyFirstProject`.
To run your new app, do:
cd MyFirstProject
wasp db migrate-dev
wasp start
```
### Available Templates
When you have a good idea for a new product, you don't want to waste your time on setting up common things like authentication, database, etc. That's why we created a few starter templates to help you get started with Wasp.
#### OpenSaaS.sh template

Everything a SaaS needs! Comes with Auth, ChatGPT API, Tailwind, Stripe payments and more. Check out for more details.
**Features:** Stripe Payments, OpenAI GPT API, Google Auth, SendGrid, Tailwind, & Cron Jobs
Use this template:
```
wasp new -t saas
```
#### Minimal Template
A minimal starter template that features just a single page. Perfect for starting from scratch with the bare essentials.
Use this template:
```
wasp new -t minimal
```
## Getting started / Agent integration
Wasp provides an official plugin for coding agents that transforms them into Wasp framework experts.
The plugin gives your agent curated access to Wasp docs, workflows, and best practices so it can develop full-stack web apps (React, Node.js, Prisma) more effectively.
The plugin / skills work with just about all the popular coding agents, such as [Claude Code](https://claude.com/product/claude-code), [Cursor](https://www.cursor.com/), [Codex](https://openai.com/codex/), [Gemini CLI](https://geminicli.com/), [GitHub Copilot](https://github.com/features/copilot/cli), [OpenCode](https://opencode.ai/), and more.
### Features
- **Wasp Documentation** โ Ensures your agent always accesses LLM-friendly Wasp docs in sync with your current project's Wasp version.
- **Wasp Knowledge** โ Imports Wasp best practices and conventions into your project's memory file (e.g. `CLAUDE.md`, `AGENTS.md`).
- **Feature Configuration** โ Easily add Wasp features like authentication, database, email, styling (Tailwind, shadcn/ui), and other integrations through your agent.
- **Deployment Guidance** โ Your agent will guide you through deploying your Wasp app to Railway or Fly.io via the Wasp CLI, or manually to your favorite cloud provider.
### Installation
#### Claude Code
First, add the Wasp plugin marketplace:
```bash
claude plugin marketplace add wasp-lang/wasp-agent-plugins
```
Then install the Wasp plugin:
```bash
claude plugin install wasp@wasp-agent-plugins --scope project
```
:::tip
We recommend installing with `project` scope so the settings are committed to git (via `settings.json`). Use `local` scope if you prefer settings that aren't committed (via `settings.local.json`).
:::
#### Other Agents (Cursor, Codex, Gemini, Copilot, OpenCode, etc.)
Run the following command and select all the skills:
```bash
npx skills add wasp-lang/wasp-agent-plugins
```
### Setup
After installing, initialize the plugin in an active agent session by explicitly invoking the `/wasp-plugin-init` skill:
```
Run the '/wasp-plugin-init' skill.
```
This adds Wasp knowledge to your project's `CLAUDE.md` or `AGENTS.md` file.
Next, start the development server as a background task so your agent has full insight into the running app while developing:
```
Run the 'start-dev-server' skill.
```
To see all available features and skills:
```
/wasp-plugin-help
```
### Learn More
Check out the [Wasp Agent Plugins repository](https://github.com/wasp-lang/wasp-agent-plugins) for more details.
## Tutorial / 1. Creating a new project
:::info
You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the [QuickStart](https://wasp.sh/docs/quick-start) guide!
:::
In this section, we'll guide you through the process of creating a simple Todo app with Wasp. In the process, we'll take you through the most important and useful features of Wasp.

If you get stuck at any point (or just want to chat), reach out to us on [Discord](https://discord.gg/rzdnErX) and we will help you!
You can find the complete code of the app we're about to build [here](https://github.com/wasp-lang/wasp/tree/release/examples/tutorials/TodoApp).
### Creating a Project
To setup a new Wasp project, run the following command in your terminal:
```sh
wasp new TodoApp -t minimal
```
We are using the `minimal` template because we're going to implement the app from scratch, instead of the more full-featured default template.
Enter the newly created directory and start the development server:
```sh
cd TodoApp
wasp start
```
`wasp start` will take a bit of time to start the server the first time you run it in a new project.
You will see log messages from the client, server, and database setting themselves up. When everything is ready, open the client URL that Wasp printed on startup in your browser. It's `http://localhost:3000` unless that port is taken. You will see a simple placeholder page:

Wasp has generated for you the full front-end and back-end code of the app! Next, we'll take a closer look at how the project is structured.
### A note on supported languages
Wasp supports both JavaScript and TypeScript out of the box, but you are free to choose between or mix JavaScript and TypeScript as you see fit.
We'll provide you with both JavaScript and TypeScript code in this tutorial. Code blocks will have a toggle to switch between vanilla JavaScript and TypeScript.
Try it out:
:::note[Welcome to TypeScript!]
You are now reading the TypeScript version of the docs. The site will remember your preference as you switch pages.
You'll have a chance to change the language on every code snippet - both the snippets and the text will update accordingly.
:::
## Tutorial / 2. Project structure
After creating a new Wasp project, your project should look like this:
```python
.
โโโ AGENTS.md # Instructions for AI coding agents.
โโโ CLAUDE.md # Symlink to AGENTS.md.
โโโ main.wasp.ts # Your Wasp Spec goes here.
โโโ package.json # Your dependencies and project info go here.
โโโ public # Your static files (e.g., images, favicon) go here.
โย ย โโโ favicon.ico
โโโ schema.prisma # Your database models go here.
โโโ src # Your source code (TS/React/Node.js) goes here.
โย ย โโโ Main.css
โย ย โโโ MainPage.tsx
โย ย โโโ assets
โย ย โย ย โโโ wasp-logo-rounded.svg
โย ย โโโ vite-env.d.ts
โโโ tsconfig.json
โโโ tsconfig.src.json
โโโ tsconfig.wasp.json
โโโ vite.config.ts
```
By *your code*, we mean the *"the code you write"*, as opposed to the code generated by Wasp. Wasp allows you to organize and structure your code however you think is best - there's no need to separate client files and server files into different directories.
We'd normally recommend organizing code by features (i.e., vertically). However, since this tutorial contains only a handful of files, there's no need for fancy organization. We'll keep it simple by placing everything in the root `src` directory.
Many other files (e.g., `tsconfig.json`, `tsconfig.src.json`, `tsconfig.wasp.json`, `vite-env.d.ts`, etc.) help Wasp and the IDE improve your development experience with autocompletion, IntelliSense, and error reporting.
The `vite.config.ts` file is used to configure [Vite](https://vitejs.dev/guide/), Wasp's build tool of choice. We won't be customizing the Vite setup in this tutorial, so you can safely ignore the file. Still, if you ever end up wanting more control over Vite, you'll find everything you need to know in [custom Vite config docs](https://wasp.sh/docs/advanced/client-customization/custom-vite-config).
The `schema.prisma` file is where you define your database schema using [Prisma](https://www.prisma.io/). We'll cover this a bit later in the tutorial.
The most important file in the project is `main.wasp.ts`. Wasp uses the configuration within it to perform its magic. Based on what you write, it generates a bunch of code for your database, server-client communication, React routing, and more.
Let's take a closer look at `main.wasp.ts`
### `main.wasp.ts`
`main.wasp.ts` is your app's Wasp file. It defines the app's central components and helps Wasp to do a lot of the legwork for you.
The file exports your app's top-level configuration and a collection of specifications. Each one defines a Route, Page, Query, Action, or other features provided by Wasp.
The default `main.wasp.ts` file generated with `wasp new` on the previous page looks like this:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
// This is a reference to your MainPage component.
// Read more about how Wasp references your code in the section below.
import { MainPage } from "./src/MainPage" with { type: "ref" }
export default app({
name: "TodoApp",
// Pins the version of Wasp to use.
wasp: { version: "^0.26" },
title: "TodoApp", // Used as the browser tab title.
head: [""],
// Add your specs here so Wasp knows to register them.
spec: [
route("RootRoute", "/", page(MainPage)),
],
})
```
#### Referencing code from `src`
When `main.wasp.ts` needs to point to your React components or Node.js functions, it uses imports like this:
```ts
import { MainPage } from "./src/MainPage" with { type: "ref" }
```
Notice the `with { type: "ref" }` part at the end of the import statement. This tells Wasp to treat the import as a reference to your app's code, without running the imported code. For more details and examples, see [reference imports](https://wasp.sh/docs/features/spec#reference-imports).
#### Specifications
This Wasp app uses three specifications:
- [**app**](https://wasp.sh/docs/api/@wasp.sh/spec/functions/app): Top-level configuration information about your app.
- [**route**](https://wasp.sh/docs/api/@wasp.sh/spec/functions/route): Describes which path each page should be accessible from.
- [**page**](https://wasp.sh/docs/api/@wasp.sh/spec/functions/page): Defines a web page and the React component that gets rendered when the page is loaded.
In the next section, we'll explore how **route** and **page** work together to build your web app.
## Tutorial / 3. Pages and routes
In the default `main.wasp.ts` file created by `wasp new`, there is a **page** and a **route** spec:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { MainPage } from "./src/MainPage" with { type: "ref" }
export default app({
// ...
spec: [
// We specify that the React implementation of the page is exported from
// `src/MainPage.tsx`. Reference imports must point to files inside `src`.
route("RootRoute", "/", page(MainPage)),
],
})
```
Together, these specifications tell Wasp that when a user navigates to `/`, it should render the `MainPage` component from `src/MainPage.tsx`.
### The MainPage Component
Let's take a look at the React component referenced by the page spec:
```tsx title="src/MainPage.tsx"
import Logo from "./assets/wasp-logo-rounded.svg";
import "./Main.css";
export function MainPage() {
// ...
}
```
This is a regular functional React component. It also imports some CSS and a logo from the `assets` folder.
That is all the code you need! Wasp takes care of everything else necessary to define, build, and run the web app.
:::tip[Keep Wasp start running]
`wasp start` automatically picks up the changes you make, regenerates the code, and restarts the app. So keep it running in the background.
It also improves your experience by tracking the working directory and ensuring the generated code/types are up to date with your changes.
:::
:::caution[LSP Problems]
If you are using TypeScript, your editor may sometimes report type and import errors even while `wasp start` is running.
This happens when the TypeScript Language Server gets out of sync with the current code. If you're using VS Code, you can manually restart the language server by opening the command palette and selecting *"TypeScript: Restart TS Server."* Open the command palette with:
- `Ctrl` + `Shift` + `P` if you're on Windows or Linux.
- `Cmd` + `Shift` + `P` if you're on a Mac.
:::
### Adding a Second Page
To add more pages, you can add another route to your spec. You can even add parameters to the URL path, using [dynamic segments](https://wasp.sh/docs/features/pages-and-routes#dynamic-segments). Let's test this out by adding a new page:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { HelloPage } from "./src/HelloPage" with { type: "ref" }
export default app({
// ...
spec: [
route("HelloRoute", "/hello/:name", page(HelloPage)),
],
})
```
When a user visits `/hello/their-name`, Wasp renders the component exported from `src/HelloPage.tsx` and you can use the `useParams` hook from `react-router` to access the `name` parameter:
```tsx title="src/HelloPage.tsx"
import { useParams } from "react-router";
export const HelloPage = () => {
const { name } = useParams<"name">();
return
Here's {name}!
;
};
```
Now you can visit `/hello/johnny` and see "Here's johnny!"
:::tip[Type-safe links]
Since you are using Typescript, you can benefit from using Wasp's type-safe `Link` component and the `routes` object. Check out [Navigating between pages](https://wasp.sh/docs/features/pages-and-routes#link-component) for more details.
:::
### Cleaning Up
Now that you've seen how Wasp deals with Routes and Pages, it's finally time to build the Todo app.
Start by cleaning up the starter project and removing unnecessary code and files.
First, remove most of the code from the `MainPage` component:
```tsx title="src/MainPage.tsx"
export const MainPage = () => {
return
Hello world!
;
};
```
At this point, the main page should look like this:

You can now delete redundant files: `src/Main.css`, `src/assets/wasp-logo-rounded.svg`, and `src/HelloPage.tsx` (we won't need this page for the rest of the tutorial).
Since `src/HelloPage.tsx` no longer exists, remove its route from the `main.wasp.ts` file.
Your Wasp file should now look like this:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { MainPage } from "./src/MainPage" with { type: "ref" }
export default app({
name: "TodoApp",
wasp: { version: "^0.26" },
title: "TodoApp",
head: [""],
spec: [
route("RootRoute", "/", page(MainPage)),
],
})
```
Excellent work!
You now have a basic understanding of Wasp and are ready to start building your TodoApp. We'll implement the app's core features in the following sections.
## Tutorial / 4. Database entities
Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.
Wasp uses Prisma to talk to the database, and you define Entities by defining Prisma models in the `schema.prisma` file.
Since our Todo app is all about tasks, we'll define a Task entity by adding a Task model in the `schema.prisma` file:
```prisma title="schema.prisma"
// ...
model Task {
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
}
```
:::note
Read more about how Wasp Entities work in the [Entities](https://wasp.sh/docs/features/data/entities) section or how Wasp uses the `schema.prisma` file in the [Prisma schema file](https://wasp.sh/docs/features/data/prisma-file) section.
:::
To update the database schema to include this entity, stop the `wasp start` process, if it's running, and run:
```sh
wasp db migrate-dev
```
You'll need to do this any time you change an entity's definition. It instructs Prisma to create a new database migration and apply it to the database.
To take a look at the database and the new `Task` entity, run:
```sh
wasp db studio
```
This will open a new page in your browser to view and edit the data in your database.

Click on the `Task` entity and check out its fields! We don't have any data in our database yet, but we are about to change that.
## Tutorial / 5. Querying the database
We want to know which tasks we need to do, so let's list them!
The primary way of working with Entities in Wasp is with [Queries and Actions](https://wasp.sh/docs/features/data/operations/overview), collectively known as ***Operations***.
Queries are used to read an entity, while Actions are used to create, modify, and delete entities. Since we want to list the tasks, we'll want to use a Query.
To list the tasks, you must:
1. Create a Query that fetches the tasks from the database.
2. Update the `MainPage.tsx` to use that Query and display the results.
### Defining the Query
We'll create a new Query called `getTasks`. We'll need to declare the Query in the Wasp file and write its implementation in TS.
#### Specifying a Query
We need to add a **query** specification to `main.wasp.ts` so that Wasp knows it exists:
```ts title="main.wasp.ts"
import { app, page, query, route } from "@wasp.sh/spec"
import { MainPage } from "./src/MainPage" with { type: "ref" }
import { getTasks } from "./src/queries" with { type: "ref" }
export default app({
// ...
spec: [
route("RootRoute", "/", page(MainPage)),
// Tell Wasp that this query reads from the `Task` entity. Wasp will
// automatically update the results of this query when tasks are modified.
query(getTasks, { entities: ["Task"] }),
],
})
```
:::note
To generate the types used in the next section, make sure that `wasp start` is still running.
:::
#### Implementing a Query
Next, create a new file called `src/queries.ts` and define the TypeScript function we've just imported in our `query` spec:
```ts title="src/queries.ts"
import type { Task } from "wasp/entities";
import type { GetTasks } from "wasp/server/operations";
export const getTasks: GetTasks = async (args, context) => {
return context.entities.Task.findMany({
orderBy: { id: "asc" },
});
};
```
Wasp automatically generates the types `GetTasks` and `Task` based on the contents of `main.wasp.ts`:
- `Task` is a type corresponding to the `Task` entity you defined in `schema.prisma`.
- `GetTasks` is a generic type Wasp automatically generated based on the `getTasks` Query you defined in `main.wasp.ts`.
You can use these types to specify the Query's input and output types. This Query doesn't expect any arguments (its input type is `void`), but it does return an array of tasks (its output type is `Task[]`).
Annotating the Queries is optional, but highly recommended because doing so enables **full-stack type safety**. We'll see what this means in the next step.
Query function parameters:
- `args: object`
The arguments the caller passes to the Query.
- `context`
An object with extra information injected by Wasp. Its type depends on the Query specification.
Since the Query spec in `main.wasp.ts` says that the `getTasks` Query uses the `Task` entity, Wasp injected a [Prisma client](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud) for the `Task` entity as `context.entities.Task` - we used it above to fetch all the tasks from the database.
:::info
Queries and Actions are NodeJS functions executed on the server.
:::
### Invoking the Query On the Frontend
While we implement Queries on the server, Wasp generates client-side functions that automatically take care of serialization, network calls, and cache invalidation, allowing you to call the server code like it's a regular function.
This makes it easy for us to use the `getTasks` Query we just created in our React component:
```tsx title="src/MainPage.tsx"
import type { Task } from "wasp/entities";
import { getTasks, useQuery } from "wasp/client/operations";
export const MainPage = () => {
const { data: tasks, isLoading, error } = useQuery(getTasks);
return (
);
};
```
Most of this code is regular React, the only exception being the three special `wasp` imports:
- `getTasks` - The client-side Query function Wasp generated based on the `getTasks` spec in `main.wasp.ts`.
- `useQuery` - Wasp's [useQuery](https://wasp.sh/docs/features/data/operations/queries#the-usequery-hook-1) React hook, which is based on [react-query](https://github.com/tannerlinsley/react-query)'s hook with the same name.
- `Task` - The type for the Task entity defined in `schema.prisma`.
Notice how you don't need to annotate the type of the Query's return value: Wasp uses the types you defined while implementing the Query for the generated client-side function. This is **full-stack type safety**: the types on the client always match the types on the server.
We could have called the Query directly using `getTasks()`, but the `useQuery` hook makes it reactive: React will re-render the component every time the Query changes. Remember that Wasp automatically refreshes Queries whenever the data is modified.
With these changes, you should be seeing the text "No tasks" on the screen:

We'll create a form to add tasks in the next step ๐ช
## Tutorial / 6. Modifying data
In the previous section, you learned about using Queries to fetch data. Let's now learn about Actions so you can add and update tasks in the database.
In this section, you will create:
1. A Wasp Action that creates a new task.
2. A React form that calls that Action when the user creates a task.
### Creating a New Action
Creating an Action is very similar to creating a Query.
#### Specifying an Action
We must first declare the Action in `main.wasp.ts`:
```ts title="main.wasp.ts"
import { action, app, page, query, route } from "@wasp.sh/spec"
import { MainPage } from "./src/MainPage" with { type: "ref" }
import { getTasks } from "./src/queries" with { type: "ref" }
import { createTask } from "./src/actions" with { type: "ref" }
export default app({
// ...
spec: [
route("RootRoute", "/", page(MainPage)),
query(getTasks, { entities: ["Task"] }),
action(createTask, { entities: ["Task"] }),
],
})
```
#### Implementing an Action
Let's now define a TypeScript function for our `createTask` Action:
```ts title="src/actions.ts"
import type { Task } from "wasp/entities";
import type { CreateTask } from "wasp/server/operations";
type CreateTaskPayload = Pick;
export const createTask: CreateTask = async (
args,
context,
) => {
return context.entities.Task.create({
data: { description: args.description },
});
};
```
Once again, we've annotated the Action with the `CreateTask` and `Task` types generated by Wasp. Just like with queries, defining the types on the implementation makes them available on the frontend, giving us **full-stack type safety**.
:::tip
We put the function in a new file `src/actions.ts`, but we could have put it anywhere we wanted! There are no limitations here, as long as the reference import in `main.wasp.ts` points to it and the file is located within the `src` directory.
:::
### Invoking the Action on the Client
Start by defining a form for creating new tasks.
```tsx title="src/MainPage.tsx"
import type { SubmitEvent } from "react";
import type { Task } from "wasp/entities";
import {
createTask,
getTasks,
useQuery,
} from "wasp/client/operations";
// ... MainPage, TaskView, TasksList ...
const NewTaskForm = () => {
const handleSubmit = async (event: SubmitEvent) => {
event.preventDefault();
try {
const target = event.target as HTMLFormElement;
const description = target.description.value;
target.reset();
await createTask({ description });
} catch (err: any) {
window.alert("Error: " + err.message);
}
};
return (
);
};
```
Unlike Queries, you can call Actions directly (without wrapping them in a hook) because they don't need reactivity. The rest is just regular React code.
Finally, because we've previously annotated the Action's server implementation with the correct type, Wasp knows that the `createTask` Action expects a value of type `{ description: string }` (try changing the argument and reading the error message). Wasp also knows that a call to the `createTask` Action returns a `Task` but we are not using it in this example.
All that's left now is adding this form to the page component:
```tsx title="src/MainPage.tsx"
import type { SubmitEvent } from "react";
import type { Task } from "wasp/entities";
import { createTask, getTasks, useQuery } from "wasp/client/operations";
export const MainPage = () => {
const { data: tasks, isLoading, error } = useQuery(getTasks);
return (
);
};
// ... TaskView, TasksList, NewTaskForm ...
```
Great work!
You now have a form for creating new tasks.
Try creating a "Build a Todo App in Wasp" task and see it appear in the list below. The task is created on the server and saved in the database.
Try refreshing the page or opening it in another browser. You'll see the tasks are still there!

:::note[Automatic Query Invalidation]
When you create a new task, the list of tasks is automatically updated to display the new task, even though you haven't written any code that does that! Wasp handles these automatic updates under the hood.
When you declared the `getTasks` and `createTask` operations, you specified that they both use the `Task` entity. So when `createTask` is called, Wasp knows that the data `getTasks` fetches may have changed and automatically updates it in the background. This means that **out of the box, Wasp keeps all your queries in sync with any changes made through Actions**.
This behavior is convenient as a default but can cause poor performance in large apps. While there is no mechanism for overriding this behavior yet, it is something that we plan to include in Wasp in the future. This feature is tracked [here](https://github.com/wasp-lang/wasp/issues/63).
:::
### A Second Action
Our Todo app isn't finished if you can't mark a task as done.
We'll create a new Action to update a task's status and call it from React whenever a task's checkbox is toggled.
Since we've already created one task together, try to create this one yourself. It should be an Action named `updateTask` that receives the task's `id` and its `isDone` status. You can see our implementation below.
Solution
Declaring the Action in `main.wasp.ts`:
```ts title="main.wasp.ts"
import { action, app, page, query, route } from "@wasp.sh/spec"
import { MainPage } from "./src/MainPage" with { type: "ref" }
import { getTasks } from "./src/queries" with { type: "ref" }
import { createTask, updateTask } from "./src/actions" with { type: "ref" }
export default app({
// ...
spec: [
// ... existing routes and queries
action(createTask, { entities: ["Task"] }),
action(updateTask, { entities: ["Task"] }),
],
})
```
Implementing the Action on the server:
```ts title="src/actions.ts"
import type { Task } from "wasp/entities";
import type { CreateTask, UpdateTask } from "wasp/server/operations";
// ...
type UpdateTaskPayload = Pick;
export const updateTask: UpdateTask = async (
{ id, isDone },
context,
) => {
return context.entities.Task.update({
where: { id },
data: {
isDone: isDone,
},
});
};
```
You can now call `updateTask` from the React component:
```tsx title="src/MainPage.tsx"
import type { ChangeEvent, SubmitEvent } from "react";
import type { Task } from "wasp/entities";
import {
updateTask,
createTask,
getTasks,
useQuery,
} from "wasp/client/operations";
// ... MainPage ...
const TaskView = ({ task }: { task: Task }) => {
const handleIsDoneChange = async (event: ChangeEvent) => {
try {
await updateTask({
id: task.id,
isDone: event.target.checked,
});
} catch (error: any) {
window.alert("Error while updating task: " + error.message);
}
};
return (
{task.description}
);
};
// ... TasksList, NewTaskForm ...
```
Awesome! You can now mark this task as done.
It's time to make one final addition to your app: supporting multiple users.
## Tutorial / 7. Adding authentication
Most modern apps need a way to create and authenticate users. Wasp makes this as easy as possible with its first-class auth support.
To add users to your app, you must:
- [ ] Create a `User` Entity.
- [ ] Tell Wasp to use the *Username and Password* authentication.
- [ ] Add login and signup pages.
- [ ] Update the main page to require authentication.
- [ ] Add a relation between `User` and `Task` entities.
- [ ] Modify your Queries and Actions so users can only see and modify their tasks.
- [ ] Add a logout button.
### Creating a User Entity
Since Wasp manages authentication, it will create [the auth related entities](https://wasp.sh/docs/features/auth/entities) for you in the background. Nothing to do here!
You must only add the `User` Entity to keep track of who owns which tasks:
```prisma title="schema.prisma"
// ...
model User {
id Int @id @default(autoincrement())
}
```
### Adding Auth to the Project
Next, tell Wasp to use full-stack [authentication](https://wasp.sh/docs/features/auth/overview):
```ts title="main.wasp.ts"
import { action, app, page, query, route } from "@wasp.sh/spec"
import { MainPage } from "./src/MainPage" with { type: "ref" }
import { getTasks } from "./src/queries" with { type: "ref" }
import { createTask, updateTask } from "./src/actions" with { type: "ref" }
export default app({
// ...
auth: {
// Tells Wasp which entity to use for storing users.
userEntity: "User",
methods: {
// Enable username and password auth.
usernameAndPassword: {},
},
// We'll see how this is used in a bit.
onAuthFailedRedirectTo: "/login",
},
spec: [
route("RootRoute", "/", page(MainPage)),
query(getTasks, { entities: ["Task"] }),
action(createTask, { entities: ["Task"] }),
action(updateTask, { entities: ["Task"] }),
],
})
```
Don't forget to update the database schema by running:
```sh
wasp db migrate-dev
```
By doing this, Wasp will create:
- [Auth UI](https://wasp.sh/docs/features/auth/ui) with login and signup forms.
- A `logout()` action.
- A React hook `useAuth()`.
- `context.user` for use in Queries and Actions.
:::info
Wasp also supports authentication using [Google](https://wasp.sh/docs/features/auth/social-auth/google), [GitHub](https://wasp.sh/docs/features/auth/social-auth/github), and [email](https://wasp.sh/docs/features/auth/email/overview), with more on the way!
:::
### Adding Login and Signup Pages
Wasp creates the login and signup forms for us, but we still need to define the pages to display those forms on. We'll start by declaring the pages in the Wasp file:
```ts title="main.wasp.ts"
import { action, app, page, query, route } from "@wasp.sh/spec"
import { MainPage } from "./src/MainPage" with { type: "ref" }
import { SignupPage } from "./src/SignupPage" with { type: "ref" }
import { LoginPage } from "./src/LoginPage" with { type: "ref" }
import { getTasks } from "./src/queries" with { type: "ref" }
import { createTask, updateTask } from "./src/actions" with { type: "ref" }
export default app({
// ...
spec: [
route("RootRoute", "/", page(MainPage)),
route("SignupRoute", "/signup", page(SignupPage)),
route("LoginRoute", "/login", page(LoginPage)),
// ... existing queries and actions
],
})
```
Great, Wasp now knows these pages exist!
Here's the React code for the pages you've just imported:
```tsx title="src/LoginPage.tsx"
import { Link } from "react-router";
import { LoginForm } from "wasp/client/auth";
export const LoginPage = () => {
return (
I don't have an account yet (go to signup).
);
};
```
The signup page is very similar to the login page:
```tsx title="src/SignupPage.tsx"
import { Link } from "react-router";
import { SignupForm } from "wasp/client/auth";
export const SignupPage = () => {
return (
I already have an account (go to login).
);
};
```
:::tip[Type-safe links]
Since you are using Typescript, you can benefit from using Wasp's type-safe `Link` component and the `routes` object. Check out [Navigating between pages](https://wasp.sh/docs/features/pages-and-routes#link-component) for more details.
:::
### Update the Main Page to Require Auth
We don't want users who are not logged in to access the main page, because they won't be able to create any tasks. So let's make the page private by requiring the user to be logged in:
```ts title="main.wasp.ts"
import { action, app, page, query, route } from "@wasp.sh/spec"
import { MainPage } from "./src/MainPage" with { type: "ref" }
import { SignupPage } from "./src/SignupPage" with { type: "ref" }
import { LoginPage } from "./src/LoginPage" with { type: "ref" }
import { getTasks } from "./src/queries" with { type: "ref" }
import { createTask, updateTask } from "./src/actions" with { type: "ref" }
export default app({
// ...
spec: [
route("RootRoute", "/", page(MainPage, {
authRequired: true,
})),
// ... existing routes, queries, and actions
],
})
```
Now that auth is required for this page, unauthenticated users will be redirected to `/login`, as we specified with `auth.onAuthFailedRedirectTo`.
Additionally, when `authRequired` is `true`, the page's React component will be provided a `user` object as prop.
```tsx title="src/MainPage.tsx"
import type { AuthUser } from "wasp/auth";
// ... existing imports
export const MainPage = ({ user }: { user: AuthUser }) => {
const { data: tasks, isLoading, error } = useQuery(getTasks);
// ...
};
```
Ok, time to test this out. Navigate to the main page (`/`) of the app. You'll get redirected to `/login`, where you'll be asked to authenticate.
Since we just added users, you don't have an account yet. Go to the signup page and create one. You'll be sent back to the main page where you will now be able to see the TODO list!
Let's check out what the database looks like. Start the Prisma Studio:
```shell
wasp db studio
```

You'll notice that we now have a `User` entity in the database alongside the `Task` entity.
However, you will notice that if you try logging in as different users and creating some tasks, all users share the same tasks. That's because you haven't yet updated the queries and actions to have per-user tasks. Let's do that next.
You might notice some extra Prisma models like `Auth`, `AuthIdentity` and `Session` that Wasp created for you. You don't need to care about these right now, but if you are curious, you can read more about them [here](https://wasp.sh/docs/features/auth/entities).
### Defining a User-Task Relation
First, let's define a one-to-many relation between users and tasks (check the [Prisma docs on relations](https://www.prisma.io/docs/orm/prisma-schema/data-model/relations)):
```prisma title="schema.prisma"
// ...
model User {
id Int @id @default(autoincrement())
tasks Task[]
}
model Task {
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User? @relation(fields: [userId], references: [id])
userId Int?
}
```
As always, you must migrate the database after changing the Entities:
```sh
wasp db migrate-dev
```
:::note
We made `user` and `userId` in `Task` optional (via `?`) because that allows us to keep the existing tasks, which don't have a user assigned, in the database.
This isn't recommended because it allows an unwanted state in the database (what is the purpose of the task not belonging to anybody?) and normally we would not make these fields optional.
Instead, we would do a data migration to take care of those tasks, even if it means just deleting them all. However, for this tutorial, for the sake of simplicity, we will stick with this.
:::
### Updating Operations to Check Authentication
Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks:
```ts title="src/queries.ts"
import type { Task } from "wasp/entities";
import { HttpError } from "wasp/server";
import type { GetTasks } from "wasp/server/operations";
export const getTasks: GetTasks = async (args, context) => {
if (!context.user) {
throw new HttpError(401);
}
return context.entities.Task.findMany({
where: { user: { id: context.user.id } },
orderBy: { id: "asc" },
});
};
```
```ts title="src/actions.ts"
import type { Task } from "wasp/entities";
import { HttpError } from "wasp/server";
import type { CreateTask, UpdateTask } from "wasp/server/operations";
type CreateTaskPayload = Pick;
export const createTask: CreateTask = async (
args,
context,
) => {
if (!context.user) {
throw new HttpError(401);
}
return context.entities.Task.create({
data: {
description: args.description,
user: { connect: { id: context.user.id } },
},
});
};
type UpdateTaskPayload = Pick;
export const updateTask: UpdateTask<
UpdateTaskPayload,
{ count: number }
> = async (args, context) => {
if (!context.user) {
throw new HttpError(401);
}
return context.entities.Task.updateMany({
where: { id: args.id, user: { id: context.user.id } },
data: { isDone: args.isDone },
});
};
```
:::note
Due to how Prisma works, we had to convert `update` to `updateMany` in `updateTask` action to be able to specify the user id in `where`.
:::
With these changes, each user should have a list of tasks that only they can see and edit.
Try playing around, adding a few users and some tasks for each of them. Then open the DB studio:
```sh
wasp db studio
```

You will see that each user has their tasks, just as we specified in our code!
### Logout Button
Last, but not least, let's add the logout functionality:
```tsx title="src/MainPage.tsx"
import type { AuthUser } from "wasp/auth";
import { logout } from "wasp/client/auth";
// ... existing imports
export const MainPage = ({ user }: { user: AuthUser }) => {
// ...
return (
{/* ... */}
);
};
```
This is it, we have a working authentication system, and our Todo app is multi-user!
### What's Next?
We did it ๐ You've followed along with this tutorial to create a basic Todo app with Wasp.
You can find the complete code for the TS version of the tutorial [here](https://github.com/wasp-lang/wasp/tree/release/examples/tutorials/TodoAppTs).
You should be ready to learn about more complicated features and go more in-depth with the features already covered. Scroll through the sidebar on the left side of the page to see every feature Wasp has to offer. Or, let your imagination run wild and start building your app! โจ
Looking for inspiration?
- Get a jump start on your next project with [Starter Templates](https://wasp.sh/docs/getting-started/starter-templates).
- Check out our [official examples](https://github.com/wasp-lang/wasp/tree/release/examples).
- Make a real-time app with [Web Sockets](https://wasp.sh/docs/features/websockets).
:::note
If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on [Discord](https://discord.gg/rzdnErX) or create an issue on [Github](https://github.com/wasp-lang/wasp), so we can learn which features to add/improve next ๐
If you would like to contribute or help to build a feature, let us know! You can find more details on contributing [here](https://wasp.sh/docs/contributing).
:::
Oh, and do [**subscribe to our newsletter**](https://wasp.sh/#signup)! We usually send one per month, and Matija does his best to unleash his creativity to make them engaging and fun to read \:D!
## Features / Wasp Spec (main.wasp.ts)
You define and configure the high level of your app (pages, routes, queries, actions, auth, ...) in a `main.wasp.ts` file in the root of your project. We call this file the **Wasp Spec**.
You write the Wasp Spec in TypeScript, so you get out-of-the-box support in all editors, full type checking, and the flexibility of a real programming language while configuring your app.
### A quick example
```ts title="main.wasp.ts"
import { app, page, query, route } from "@wasp.sh/spec";
import { MainPage } from "./src/MainPage" with { type: "ref" };
import { getTasks } from "./src/queries" with { type: "ref" };
export default app({
name: "todoApp",
wasp: { version: "^0.26" },
title: "ToDo App",
head: [""],
spec: [
route("MainRoute", "/", page(MainPage, { authRequired: true })),
query(getTasks, { entities: ["Task"] }),
],
});
```
You build your app by:
1. Importing the building blocks (`app`, `page`, `route`, `query`, ...) from `@wasp.sh/spec`.
2. Importing your own components and functions adding the import attribute `with { type: "ref" }`.
3. Calling `app({ ... })` with your app's configuration, listing all the pages, routes, queries, actions, etc. in the `spec` property.
4. Exporting the result as the **default export** of the file.
`spec` is short for specification: the pages, routes, queries, actions, APIs, jobs and CRUDs that make up your app.
### `wasp install`
The `@wasp.sh/spec` package doesn't exist on npm, but it is generated by Wasp per project, so we can customize it to fit the needs of your app. We do this through the `wasp install` command, which installs your app's dependencies and sets up the generated Wasp Spec package. You'll have to run it at least once after creating a new Wasp project, but you might also need to run it again later on when the generated spec needs to be updated.
If the Spec needs to be regenerated, Wasp will tell you to run `wasp install` before being able to start the app. Usually, this might happen when upgrading Wasp versions, running `wasp clean`, or removing the `node_modules` folder.
### Referencing your app's code
Anywhere the Wasp Spec expects your app's function or component (like a page's `component` or a query's `fn`), you can provide it in one of two ways:
#### Reference imports
**Recommended**
Import the value with the regular syntax, adding `with { type: "ref" }`. Use it when importing components or functions from `src/` so Wasp can connect them to pages, actions, queries, and other specifications.
```ts title="main.wasp.ts"
import { MainPage } from "./src/MainPage" with { type: "ref" };
import { getTasks } from "./src/queries" with { type: "ref" };
export default app({
spec: [page(MainPage), query(getTasks)],
});
```
The import paths are relative to the `*.wasp.ts` file they're written in (see [multiple spec files](#splitting-your-spec-into-multiple-files)):
```ts title="src/auth/auth.wasp.ts"
import { LoginPage } from "./LoginPage" with { type: "ref" };
export const auth = [page(LoginPage)];
```
:::note[Limitations]
Reference imports have some limitations:
- They only work from `*.wasp.ts` files.
- The referenced files must be inside the `src` directory.
- You can't re-export something as a reference import (`export { X } from "./X" with { type: "ref" }`). Import it first, then re-export it if needed.
- Namespace imports (`import * as something from './src/something' with { type: "ref" }`) aren't supported. Use named or default imports instead.
The vast majority of Wasp apps won't run into these limitations, so we recommend using reference imports by default.
:::
#### `ref` helper {#reference-objects}
Use `ref(...)` when a direct reference import is not practical. Import `ref` from `@wasp.sh/spec`, then pass it an import object with `import` (or `importDefault`) and `from`:
```ts title="main.wasp.ts"
import { ref } from "@wasp.sh/spec";
export default app({
// ...
spec: [
page(ref({ importDefault: "MainPage", from: "./src/MainPage" })),
query(ref({ import: "getTasks", from: "./src/queries" })),
// You can rename a named import with `alias`:
query(ref({ import: "getTasks", alias: "getAllTasks", from: "./src/queries" })),
],
});
```
The `from` path is relative to the `*.wasp.ts` file where you call `ref(...)` and must resolve inside your project's `src` directory.
:::note[Limitation]
You can't re-export `ref` from `@wasp.sh/spec` (`export { ref } from "@wasp.sh/spec"`). Import it first, then re-export it if needed.
:::
### Useful patterns
#### Splitting your spec into multiple files
For larger apps you don't have to keep everything in `main.wasp.ts`. You can move related specifications into their own `*.wasp.ts` files and combine them in `main.wasp.ts`. This works well for vertical slices, like keeping a feature's page, route, query, and action specifications in that feature's folder.
Each feature file exports it's own `Spec`:
```ts title="src/auth/auth.wasp.ts"
import { page, route, type Spec } from "@wasp.sh/spec";
import { LoginPage } from "./LoginPage" with { type: "ref" };
import { SignupPage } from "./SignupPage" with { type: "ref" };
export const authSpec: Spec = [
route("SignupRoute", "/signup", page(SignupPage)),
route("LoginRoute", "/login", page(LoginPage)),
];
```
The `Spec` annotation gives TypeScript enough information to validate the specification in its own file before it's added to the `main.wasp.ts`.
Then `main.wasp.ts` imports it and joins in into the `spec`:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec";
import { MainPage } from "./src/MainPage" with { type: "ref" };
import { authSpec } from "./src/auth/auth.wasp";
export default app({
name: "todoApp",
wasp: { version: "^0.26" },
title: "ToDo App",
head: [""],
spec: [
route("MainRoute", "/", page(MainPage, { authRequired: true })),
authSpec,
],
});
```
All spec files should have the `.wasp.ts` extension, so they are included in the `tsconfig.wasp.json` and type-checked.
#### Detecting production mode
Wasp sets the `NODE_ENV` environment variable based on which command you use to run Wasp:
- `"development"` during `wasp start` (and some other commands that compile the project, like `wasp db migrate-dev`).
- `"production"` during `wasp build`.
Because the Wasp Spec is just TypeScript, you can read this variable to switch config values per environment:
```ts title="main.wasp.ts"
const isProd = process.env.NODE_ENV === "production";
export default app({
//...
emailSender: {
provider: isProd ? "SMTP" : "Dummy",
defaultFrom: { email: "hi@example.com" },
},
});
```
### Reference
[API reference](https://wasp.sh/docs/api/@wasp.sh/spec)
#### [@wasp.sh/spec ยป](https://wasp.sh/docs/api/@wasp.sh/spec)
[A complete overview of all the available configuration options in the Wasp Spec.](https://wasp.sh/docs/api/@wasp.sh/spec)
## Features / Pages and routes
A **page** is a React component that Wasp renders as a full screen of your app. A **route** connects a URL path to a page. You declare both in your `main.wasp.ts` file and Wasp generates all the wiring for you, including [lazy-loading each page's code](#lazy-loaded-routes) by default. Internally, we use the industry-standard [React Router](https://reactrouter.com/8.0.1/home) to handle routing.
### Declaring a page and a route
To show a page at a URL, call `route` with a unique name, the URL path, and the page to render. Wrap your React component with `page` to turn it into a page:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { MainPage } from "./src/MainPage" with { type: "ref" }
import { AboutPage } from "./src/AboutPage" with { type: "ref" }
export default app({
// ...
spec: [
route("RootRoute", "/", page(MainPage)),
route("AboutRoute", "/about", page(AboutPage)),
],
})
```
The component is a regular React component, and it doesn't need to be exported in any particular way:
```tsx title="src/AboutPage.tsx"
export function AboutPage() {
return (
About us
We build things with Wasp.
);
}
```
With this example, visiting `/about` would show your `AboutPage` component. Every route needs a unique name (in this case, `"AboutRoute"`), which you can use to build type-safe links between pages, as shown in [Navigating between pages](#link-component).
Wasp collects every route in the `spec` array and turns them into a single React Router configuration. If you have many pages, you can [split the routes across several `*.wasp.ts` files](https://wasp.sh/docs/features/spec#splitting-your-spec-into-multiple-files).
### Reading values from the URL {#dynamic-segments}
Most apps have pages whose content depends on the URL, like `/photo/42`. Instead of declaring a route for every possible photo, put a **dynamic segment** in the path and read its value in the page.
#### Parameter segments
Use `:paramName` in a route path to match any value in that segment. Access the matched value in your page component with the `useParams` hook from `react-router`:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { PhotoPage } from "./src/PhotoPage" with { type: "ref" }
export default app({
// ...
spec: [
route("PhotoRoute", "/photo/:photoId", page(PhotoPage)),
],
})
```
```tsx title="src/PhotoPage.tsx"
import { useParams } from "react-router";
export function PhotoPage() {
const { photoId } = useParams<"photoId">();
return
Viewing photo {photoId}
;
}
```
Read more in the [React Router docs on dynamic segments](https://reactrouter.com/8.0.1/start/data/routing#dynamic-segments).
#### Optional segments
Append `?` to a path segment to make it optional. The route matches whether or not the segment is present:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { PhotoPage } from "./src/PhotoPage" with { type: "ref" }
export default app({
// ...
spec: [
route("PhotoRoute", "/photo/:photoId/edit?", page(PhotoPage)),
],
})
```
```tsx title="src/PhotoPage.tsx"
import { useParams, useLocation } from "react-router";
export function PhotoPage() {
const { photoId } = useParams<"photoId">();
const { pathname } = useLocation();
const isEditing = pathname.endsWith("/edit");
return (
);
}
```
Read more in the [React Router docs on optional segments](https://reactrouter.com/8.0.1/start/data/routing#optional-segments).
#### Splats
Use `/*` at the end of a route path to match any remaining path segments. Access the matched portion with the `'*'` param:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { FilesPage } from "./src/FilesPage" with { type: "ref" }
export default app({
// ...
spec: [
route("FilesRoute", "/files/*", page(FilesPage)),
],
})
```
```tsx title="src/FilesPage.tsx"
import { useParams } from "react-router";
export function FilesPage() {
const { "*": filePath } = useParams<"*">();
// Visiting /files/docs/report.txt โ filePath = "docs/report.txt"
return
File: {filePath}
;
}
```
Read more in the [React Router docs on splats](https://reactrouter.com/8.0.1/start/data/routing#splats).
#### Query strings and hashes
You don't need to declare query strings (`?sortBy=date`) or hashes (`#comments`) in the route path. Any page can read them with the `useSearchParams` and `useLocation` hooks from `react-router`:
```tsx title="src/TasksPage.tsx"
import { useSearchParams } from "react-router";
export function TasksPage() {
const [searchParams] = useSearchParams();
const sortBy = searchParams.get("sortBy") ?? "name";
// Visiting /tasks?sortBy=date โ sortBy = "date"
return
Tasks sorted by {sortBy}
;
}
```
### Navigating between pages {#link-component}
To link from one page to another, use the `Link` component from `wasp/client/router`. It behaves the same as [React Router's `Link`](https://reactrouter.com/8.0.1/api/components/Link), with added types for route paths and parameters. If you give it a route path that doesn't exist, or a route with wrong parameters, you'll get a type error.
```tsx title="src/components/PhotoCard.tsx"
import { Link } from "wasp/client/router";
export function PhotoCard({ photoId }: { photoId: string }) {
return (
Photo {photoId}
);
}
```
To build your own component on top of `Link` and keep the type safety, use the `LinkProps` type:
```tsx title="src/components/ButtonLink.tsx"
import { Link, type LinkProps } from "wasp/client/router";
export function ButtonLink({ className, ...linkProps }: LinkProps) {
return ;
}
```
When you need to navigate from your own code instead of as a link (for example after a form submits), build the URL with the `routes` object. `routes` has one entry per route name from your spec, and you can pass it to React Router's `useNavigate` hook:
```tsx title="src/NewPhotoForm.tsx"
import { useNavigate } from "react-router";
import { routes } from "wasp/client/router";
export function NewPhotoForm() {
const navigate = useNavigate();
async function handleSubmit() {
const photoId = await uploadPhoto(); // your upload logic
navigate(routes.PhotoRoute.build({ params: { photoId } }));
}
// ...
}
```
#### Reacting to navigation state with `NavLink`
Use `NavLink` when the current page should be highlighted, or when you want to show a spinner during a pending transition. It behaves the same as [React Router's `NavLink`](https://reactrouter.com/8.0.1/api/components/NavLink), with added types for route paths and parameters. It is similar to [`Link`](#link-component), but `className`, `style`, and `children` can be render-prop functions that receive `{ isActive, isPending, isTransitioning }`.
```tsx title="src/Navigation.tsx"
import { NavLink } from "wasp/client/router";
export function Navigation() {
return (
);
}
```
### Restricting a page to logged-in users
If your app uses [authentication](https://wasp.sh/docs/features/auth/overview), you can mark a page with `authRequired: true`. Wasp then shows the page only to logged-in users and redirects everyone else to the path defined in `auth.onAuthFailedRedirectTo`:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { DashboardPage } from "./src/DashboardPage" with { type: "ref" }
export default app({
// ...
auth: {
// ...
onAuthFailedRedirectTo: "/login",
},
spec: [
route("DashboardRoute", "/dashboard", page(DashboardPage, { authRequired: true })),
],
})
```
A page with `authRequired` receives the logged-in user as the `user` prop:
```tsx title="src/DashboardPage.tsx"
import type { AuthUser } from "wasp/auth";
export function DashboardPage({ user }: { user: AuthUser }) {
return
Hello, user {user.id}
;
}
```
While Wasp checks whether the user is logged in, it shows a loading indicator instead of the page. Read more about the `user` object and about reading the current user from pages that are open to everyone in [Accessing the logged-in user](https://wasp.sh/docs/features/auth/overview#accessing-the-logged-in-user).
### Sharing a layout between pages
By default, Wasp renders each page on its own. To give every page a shared header, footer, or set of providers, declare a **root component** in the `client` config and render React Router's `Outlet` where the current page should go:
```ts title="main.wasp.ts"
import { app } from "@wasp.sh/spec"
import { Root } from "./src/Root" with { type: "ref" }
export default app({
// ...
client: {
rootComponent: Root,
},
})
```
```tsx title="src/Root.tsx"
import { Outlet } from "react-router";
export function Root() {
return (
My App
);
}
```
See [Root Component](https://wasp.sh/docs/advanced/client-customization/client-config#root-component) for more details and examples.
### Setting the page metadata
The [`head` field of your `app` config](https://wasp.sh/docs/advanced/client-customization/customizing-app) applies to every page.
But if you want to add metadata for only one of your pages, you can render `` elements inside the component. React moves them into the document's `` for you:
```tsx title="src/PhotoPage.tsx"
import { useParams } from "react-router";
export function PhotoPage() {
const { photoId } = useParams<"photoId">();
return (
<>
Viewing photo {photoId}
>
);
}
```
Read more in the [React docs on ``](https://react.dev/reference/react-dom/components/meta), and in the [SEO & GEO](https://wasp.sh/docs/advanced/seo) page.
### Showing a page for not found URLs
When a visitor opens a URL that no route matches, Wasp shows a generic error screen. To show your own "not found" page instead, add a route with a path of `/*`. The router picks the most specific matching route, so this route only renders when nothing else matches:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { NotFoundPage } from "./src/NotFoundPage" with { type: "ref" }
export default app({
// ...
spec: [
// ... your other routes
route("NotFoundRoute", "/*", page(NotFoundPage)),
],
})
```
### Lazy-loaded routes
By default, Wasp splits and lazy-loads all pages. This means that, for example, while the user is in the `/about` page, all the other pages' code is not loaded. And, when the user navigates to another page, that bundle of code is downloaded on-demand. This reduces the amount of data your users need to download and execute, and thus provides a faster initial load experience, similar to classic HTML sites. This is especially useful for apps with many routes.
In most apps, you won't notice lazy loading at all. The page's code is usually small and downloads quickly. But when a page has a lot of code, or your users are on slow connections, navigating to it can feel sluggish: the previous page stays on screen until the new page's code arrives. If a page needs to render instantly, like one users open right after landing on your app, set the `lazy: false` option on its `route` spec to include it in the initial bundle:
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { DashboardPage } from "./src/DashboardPage" with { type: "ref" }
export default app({
// ...
spec: [
// This route's page will be included in the initial bundle
route("DashboardRoute", "/dashboard", page(DashboardPage), { lazy: false }),
],
})
```
:::caution
Disabling lazy loading means that this page's code will always be downloaded by the user's browser ahead of time. This will increase the initial load time of your app, especially if the page has a lot of code.
:::
### Prerendered routes
You can prerender specific routes at build time by setting the `prerender` property. This generates static HTML that is served immediately, giving faster load times and better SEO.
```ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec"
import { LandingPage } from "./src/LandingPage" with { type: "ref" }
export default app({
// ...
spec: [
route("LandingRoute", "/", page(LandingPage), { prerender: true }),
],
})
```
See the [Prerendering](https://wasp.sh/docs/advanced/prerendering) page for the full documentation.
### API Reference
#### `page` and `route` specifications
[API reference](https://wasp.sh/docs/api/@wasp.sh/spec/functions/page)
#### [page ยป](https://wasp.sh/docs/api/@wasp.sh/spec/functions/page)
[All the options for declaring a page in the Wasp spec.](https://wasp.sh/docs/api/@wasp.sh/spec/functions/page)
[API reference](https://wasp.sh/docs/api/@wasp.sh/spec/functions/route)
#### [route ยป](https://wasp.sh/docs/api/@wasp.sh/spec/functions/route)
[All the options for declaring a route in the Wasp spec.](https://wasp.sh/docs/api/@wasp.sh/spec/functions/route)
#### `Link` Component
The `Link` component accepts the following props:
- `to` required
- A valid Wasp Route path from your `main.wasp.ts` file.
In the case of optional static segments, you must provide one of the possible paths which include or exclude the optional segment. For example, if the path is `/task/:id/details?`, you must provide either `/task/:id/details` or `/task/:id`.
- `params: { [name: string]: string | number }` required (if the path contains params)
- An object with keys and values for each param in the path.
- For example, if the path is `/task/:id`, then the `params` prop must be `{ id: 1 }`. Wasp supports required and optional params.
- `search: string[][] | Record | string | URLSearchParams`
- The search params, in any shape the [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams) constructor accepts.
- For example, the object `{ sortBy: 'date' }` becomes `?sortBy=date`.
- `hash: string`
- All other props that the `react-router`'s [Link](https://reactrouter.com/8.0.1/api/components/Link) component accepts
#### `NavLink` Component
The `NavLink` component accepts the same `to`, `params`, `search`, and `hash` props as the [`Link` component](#link-component), plus:
- All other props that the `react-router`'s [NavLink](https://reactrouter.com/8.0.1/api/components/NavLink) component accepts
The most useful ones are:
- `className`, `style`, and `children`
- Besides regular values, these can be functions. `NavLink` calls them with an object describing the link's state and uses what they return. The object has these fields:
- `isActive`: the link points to the current page.
- `isPending`: the user clicked the link, and the page is still loading.
- `isTransitioning`: a [view transition](https://reactrouter.com/8.0.1/how-to/view-transitions) to the page is in progress.
- `end: boolean`
- By default, a link is also active on nested paths. For example, a link to `/tasks` is active on `/tasks/123`. Set `end` to only make it active on its exact path.
- `caseSensitive: boolean`
- By default, the link ignores letter case when checking if it's active. Set `caseSensitive` to take case into account.
#### `routes` Object
The `routes` object contains a function for each route in your app.
```ts title="router.tsx"
export const routes = {
// RootRoute has a path like "/"
RootRoute: {
build: (options?: {
search?: string[][] | Record | string | URLSearchParams
hash?: string
}) => // ...
},
// DetailRoute has a path like "/task/:id/:userId?"
DetailRoute: {
build: (
options: {
params: { id: ParamValue; userId?: ParamValue; },
search?: string[][] | Record | string | URLSearchParams
hash?: string
}
) => // ...
},
// OptionalRoute has a path like "/task/:id/details?"
OptionalRoute: {
build: (
options: {
path: "/task/:id/details" | "/task/:id",
params: { id: ParamValue },
search?: string[][] | Record | string | URLSearchParams
hash?: string
}
) => // ...
},
// CatchAllRoute has a path like "/pages/*"
CatchAllRoute: {
build: (
options: {
params: { "*": ParamValue },
search?: string[][] | Record | string | URLSearchParams
hash?: string
}
) => // ...
},
}
```
The `params` object is required if the route contains params. The `search` and `hash` parameters are optional.
#### React Router API
Inside page components, you can use most navigation-related hooks and components from `react-router`, such as [`useParams`](https://reactrouter.com/8.0.1/api/hooks/useParams), [`useSearchParams`](https://reactrouter.com/8.0.1/api/hooks/useSearchParams), [`useLocation`](https://reactrouter.com/8.0.1/api/hooks/useLocation), [`useNavigate`](https://reactrouter.com/8.0.1/api/hooks/useNavigate), and [`Outlet`](https://reactrouter.com/8.0.1/api/components/Outlet).
Wasp defines the routes for you, so there's no way to attach React Router [loaders](https://reactrouter.com/8.0.1/start/data/data-loading) or [actions](https://reactrouter.com/8.0.1/start/data/actions) to them. APIs that depend on those, like `useLoaderData`, `useActionData`, `useFetcher`, or submitting a `