How to Build a Full-Stack Authenticated App in 30 Minutes with Wasp

31 views 0 likes 0 comments 25 minutesOriginalTutorial

Stop wrestling with boilerplate code. This hands-on tutorial shows you how to use Wasp—a declarative full-stack framework—to rapidly build a TodoList app with user authentication, database modeling, and type-safe RPC communication, all in under 30 minutes.

#Full-stack #Wasp #React #Node.js #Prisma #TypeScript #Hands-onTutorial #RapidDevelopment
How to Build a Full-Stack Authenticated App in 30 Minutes with Wasp

How to Build a Full-Stack Authenticated App in 30 Minutes with Wasp

Honestly, every time I get a request to "build an admin dashboard" or "spin up an internal tool," I dread it—not because the tech is hard, but because the setup is fragmented and tedious.

You start a React frontend, wire up an Express backend, set up Prisma for the database, manually implement JWT auth, configure CORS, and write glue code to align frontend and backend types... Before you even write a single line of business logic, you've already spent a day scaffolding. Don't even get me started on Dockerfiles and CI/CD pipelines.

If you've ever felt the pain of "spending 80% of the time configuring environments and only 20% writing actual features," this tutorial is for you.

We'll use Wasp, a declarative full-stack framework, to build a complete TodoList application from scratch. It will include: user registration/login authentication, database modeling, frontend-to-backend RPC communication, and dynamic data rendering. You won't need to manually write Express routes, handle CORS, or define API type interfaces—Wasp generates everything for you.

By the end of this guide, you'll have a deployable full-stack app skeleton that will cut your development time in half for future projects.


Prerequisites

Before we begin, ensure your environment meets the following requirements:

  • Node.js >= 18.0 (recommended to manage versions via nvm)
  • npm or yarn package manager
  • SQLite or PostgreSQL (we'll use SQLite by default for zero-config local development)
  • Basic familiarity with React components and TypeScript typing
  • OS: macOS, Linux, or Windows (WSL) all work fine

Why Node 18+? Wasp relies on modern Node.js APIs under the hood. Older versions will cause the CLI tool to fail. Upgrade first to avoid unnecessary debugging later.


Step 1: Install the Wasp CLI

Wasp's core is a compiler: you write declarative configurations and business logic, and it scaffolds the complete full-stack project for you. Start by installing the CLI:

bash 复制代码
npm install -g @wasp.sh/wasp-cli@latest

Verify the installation:

bash 复制代码
wasp --version

If you see a version number, you're good to go. We install it globally so the wasp command is accessible from any subdirectory in your project workflow.


Step 2: Initialize Your Project

Wasp provides an out-of-the-box project template. Generate your project skeleton with:

bash 复制代码
wasp new todo-app
cd todo-app

After running this, you'll get a clean, well-structured directory. The most important file here is main.wasp (or main.wasp.ts)—this is Wasp's declarative core. All routes, pages, operations, and authentication configurations are centralized here.

Why keep React and Express files separate? Wasp enforces separation of concerns: declarative files define the architecture, while business files contain the logic. The benefit? When you need a new page or endpoint, you just update the declarative file, and Wasp automatically handles the underlying wiring.


Step 3: Define Your Database Schema

Wasp integrates Prisma as its ORM, so database models are defined in schema.prisma. Open db/schema.prisma in your project and update it with the following:

prisma 复制代码
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  username  String   @unique
  tasks     Task[]
}

model Task {
  id          Int     @id @default(autoincrement())
  description String
  isDone      Boolean @default(false)
  user        User    @relation(fields: [userId], references: [id])
  userId      Int
}

This does two things: defines User and Task tables and establishes a foreign key relationship between them. I chose SQLite because it's zero-config and perfect for local development. For production, simply change the provider to postgresql, update the connection string, and you're done—your business code won't need a single change. This is the power of Prisma, which Wasp leverages natively.

After updating the schema, run the database migration:

bash 复制代码
cd ext/db
npx prisma migrate dev
npx prisma generate

Step 4: Configure Declarative Architecture (main.wasp)

This is the heart of Wasp. Open main.wasp and paste the following configuration:

wasp 复制代码
app todo-app {
  wasp: { version: "^0.16.0" },
  title: "Todo App"
}

route RootRoute { path: "/", to: MainPage }
page MainPage {
  authRequired: true,
  component: import MainPage from "@ext/MainPage"
}

query getTasks {
  fn: import { getTasks } from "@ext/queries",
  entities: [Task]
}

action createTask {
  fn: import { createTask } from "@ext/actions",
  entities: [Task]
}

auth {
  userEntity: User,
  methods: {
    usernameAndPassword: {}
  },
  onAuthFailedRedirectTo: "/login"
}

authRoute LoginPage { path: "/login", to: LoginPageComponent }
page LoginPageComponent {
  component: import { LoginPage } from "@wasp/auth"
}

What does this declarative code do?

  1. route maps URLs to pages.
  2. page declares the UI component. authRequired: true means only authenticated users can access it—no custom middleware needed; Wasp handles it automatically.
  3. query and action define frontend-to-backend RPC methods. The entities: [Task] directive is crucial: it tells Wasp that this operation depends on the Task entity. When Task data changes, Wasp automatically invalidates caches and refetches data. This keeps your UI perfectly in sync without manual refetch calls.
  4. auth declares the authentication method. Wasp will generate the login page, registration flow, and session management out of the box—zero custom auth code required.

Step 5: Implement Frontend UI and Business Logic

Frontend UI: MainPage.tsx

Create src/MainPage.tsx in the project root:

tsx 复制代码
import { useQuery, useAction } from "wasp/client";
import { getTasks } from "wasp/client/operations";
import { createTask } from "wasp/client/operations";
import { useState } from "react";

export default function MainPage() {
  const { data: tasks, isLoading } = useQuery(getTasks);
  const [description, setDescription] = useState("");
  const handleCreate = useAction(createTask);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (description.trim()) {
      await handleCreate({ description });
      setDescription("");
    }
  };

  if (isLoading) return <div>Loading...</div>;

  return (
    <div style={{ maxWidth: 400, margin: "40px auto", padding: 20 }}>
      <h2>My Todo List</h2>
      <form onSubmit={handleSubmit}>
        <input
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          placeholder="New task..."
          style={{ width: "calc(100% - 80px)", padding: 8, marginRight: 8 }}
        />
        <button type="submit" style={{ padding: "8px 16px" }}>Add</button>
      </form>
      <ul style={{ listStyle: "none", padding: 0, marginTop: 20 }}>
        {tasks?.map((task) => (
          <li key={task.id} style={{ padding: 8, borderBottom: "1px solid #eee" }}>
            <span style={{ textDecoration: task.isDone ? "line-through" : "none" }}>
              {task.description}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Notice the useQuery and useAction hooks. They are Wasp's abstractions over TanStack Query. Why not use fetch or axios directly? Because Wasp guarantees end-to-end type safety. The function signatures you define on the server automatically generate full TypeScript types on the client. Passing the wrong parameters? The compiler will catch it immediately, saving you from runtime bugs.

Backend Logic: Queries and Actions

Create src/queries.ts:

ts 复制代码
import { Task } from "wasp/entities";

export const getTasks = async (_args: void, context: any): Promise<Task[]> => {
  if (!context.user) throw new Error("Unauthorized");
  return context.entities.Task.findMany({
    where: { userId: context.user.id },
    orderBy: { id: "desc" },
  });
};

Create src/actions.ts:

ts 复制代码
import { Task } from "wasp/entities";

export const createTask = async (
  args: { description: string },
  context: any
): Promise<Task> => {
  if (!context.user) throw new Error("Unauthorized");
  return context.entities.Task.create({
    data: {
      description: args.description,
      userId: context.user.id,
    },
  });
};

The key here is context.user. Wasp automatically injects the authenticated user's information at runtime. You can use it directly in your queries and actions without manually parsing tokens from headers. This is a direct benefit of declarative authentication.


Step 6: Run and Verify Your Application

With everything in place, start the development server:

bash 复制代码
wasp start

Wasp will automatically spin up both the frontend dev server and the backend server, opening your browser to the registration page.

Verification Steps:

  1. On first visit, you'll be redirected to /login. Click "Sign up" to register a new account.
  2. After logging in, you'll land on the main page. Enter a task description and click "Add".
  3. The new task will appear instantly in the list (no page refresh required).
  4. Close and reopen the browser, then navigate directly to /. You'll be redirected to the login page again—Wasp manages authentication sessions automatically.

At this point, your full-stack app is running. Frontend-to-backend communication uses Wasp's auto-generated RPC layer, database queries run through Prisma, and auth is handled by built-in session management. You didn't write a single line of glue code.


Common Pitfalls & Troubleshooting

  1. CLI Version Mismatch: The wasp: { version: "^0.16.0" } in main.wasp must match your installed CLI's major version. If you get a version error, run npm i -g @wasp.sh/wasp-cli@latest.
  2. Prisma Migration Failures: Always run npx prisma migrate dev after modifying schema.prisma. If conflicts arise during local development, you can safely delete the dev.db file and start fresh.
  3. TypeScript Errors: Wasp auto-generates type definitions in the .wasp/out/sdk folder upon startup. If your IDE shows red squiggles initially, wait for wasp start to fully initialize, then restart your TypeScript language server. This is normal and not a code issue.
  4. Hot Reload Limits: Modifying main.wasp requires restarting wasp start (or waiting for the Wasp compiler to auto-regenerate). Modifying .tsx / .ts business files supports hot reloading out of the box.

Conclusion & Next Steps

Let's recap what we accomplished:

  1. Installed the Wasp CLI and generated a project skeleton with wasp new.
  2. Defined a database schema using Prisma and ran migrations.
  3. Configured routes, pages, RPC operations, and authentication declaratively in main.wasp.
  4. Wrote the frontend UI and backend query/action functions.
  5. Launched the server and verified the full user journey: registration, login, task creation, and data rendering.

Throughout this process, we never manually configured Express routes, wrote JWT middleware, handled CORS, or defined RPC interfaces. Wasp's declarative compiler handled it all, allowing you to focus purely on business logic.

Recommended Next Steps:

  • Add google: {} to auth.methods in main.wasp to integrate Google OAuth.
  • Utilize Wasp's email features to send notifications.
  • Implement a deleteTask action for full CRUD functionality.
  • Deploy to Render or Fly.io using the wasp deploy command.

While Wasp is currently in beta, its core features are highly stable. If you're tired of scaffolding full-stack projects from scratch, this framework is definitely worth trying. Join the Wasp Discord community or open a GitHub issue if you run into questions.

Happy coding!

Last Updated:

Comments (0)

Post Comment

Loading...
0/500

No comments yet, be the first to comment!