Leveraging Wasp for full-stack development
Tags: wasp, webdev
Published: 2024-06-06 17:26:03

**Written by [Wisdom Ekpotu](https://blog.logrocket.com/author/wisdomekpotu/)✏️**

[Wasp](https://wasp-lang.dev/), or Web Application Specification, is a declarative full-stack framework introduced in 2020 with the primary goal of addressing the complexities inherent in modern web development. Traditionally, developers had to be concerned about setting up authentication, managing databases, client-to-server connections, etc., but repetitive tasks take time away from actually developing the software. 

Wasp takes care of this so that developers can write less code and focus more on the business logic of their applications. Developers can define their application structure and functionalities within a simple configuration file (`main.wasp`) using a domain-specific language (DSL) that Wasp understands. 

Wasp then generates the boilerplate code for the frontend (React), backend (Node.js), and data access layer (Prisma). This reduces the cognitive load, minimizes the risk of inconsistencies, and promotes better maintainability of the codebase. 

In this article, we will explore how Wasp simplifies full-stack development by building a demo application to demonstrate Wasps’s features, including setting up authentication and database handling.

## Wasp's architecture

At the heart of Wasp's architecture lies the Wasp Compiler (built with Haskell), which is responsible for processing the Wasp domain-specific language (DSL) and generating the full source code for your web application in the respective stacks. 

Here is a diagrammatic representation of how it works: ![Wasp Architecture Diagram](https://blog.logrocket.com/wp-content/uploads/2024/05/wasp-architecture-diagram.png)

### Why you should use Wasp for your project

*   **Ships quickly**: With Wasp, the time from an idea to a fully deployed production-ready web app is greatly reduced
*   **No vendor lock-in**: You can deploy your Wasp app anywhere to any platform of your choice and have full control over the code
*   **Less boilerplate code**: Wasp comes with less boilerplate code, which makes it easy to maintain, understand, and upgrade

### Key features of Wasp

*   **Full-stack authentication:** One of the standout features of Wasp is its robust, out-of-the-box authentication system, which includes [pre-built login and signup UI](https://wasp-lang.dev/docs/auth/ui) components for quick integration
*   **Emails:** There is built-in support for sending emails directly from your app using your favorite email providers such as [SMTP](https://wasp-lang.dev/docs/advanced/email#using-the-smtp-provider), [Mailgun](https://wasp-lang.dev/docs/advanced/email#using-the-mailgun-provider), or [SendGrid](https://wasp-lang.dev/docs/advanced/email#using-the-sendgrid-provider)
*   **Typesafe RPC layer**: Wasp provides a client-server layer that brings together your data models and all server logic closer to your client
*   **Type safety**: Wasp also offers full-stack type safety in TypeScript with auto-generated types that span the whole application stack

### Wasp vs. Next.js/Nuxt.js/Gatsby

You might be asking, is Wasp not just another frontend framework? Yes, but it easily separates itself from the rest. Unlike Next.js, Nuxt.js, and Gatsby, which mainly focus on frontend development, Wasp is truly full-stack. It comes repacked with all your frontend and backend/database needs taken care of so that you don't have to integrate them separately. 

Additionally, Wasp is being developed to be framework agnostic, so there should be more support for other frameworks and tools in the future.

## Building a full-stack application with Wasp

In this section, you will build a [Google Keeps](https://keep.google.com/)-like app to demonstrate how a basic CRUD application can be built with Wasp. 

To get the most from this tutorial, you‘ll need the following:

*   Familiarity with JavaScript, React, and Node.js
*   Basic knowledge of Tailwind CSS, a React component library
*   Any IDE (I recommend [Visual Studio Code](https://code.visualstudio.com/))
*   Node.js >= v18 installed on your local machine (visit the [official Node.js page for instructions](https://nodejs.org/en/?ref=horizon-documentation))
*   A modern terminal shell such as [zsh](https://opensource.com/article/19/9/getting-started-zsh), [iTerm2](https://iterm2.com/) with [oh-my-zsh](https://ohmyz.sh/) for Mac, or [Hyper](https://hyper.is/) for Windows
*   A browser such as Chrome, Microsoft Edge, Brave, or Firefox
*   An active [Fly.io](http://Fly.io) account
*   [Docker installed](https://www.docker.com/get-started)

You can find the code files for this tutorial on [GitHub](https://github.com/wisdomekpotu/wasp-fullstack).

### Installing Wasp

You'll need to install the Wasp onto your local machine to get started. For Linux/macOS, open your terminal and run the command below:

```wasp
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
```

You might encounter an error when trying to install Wasp on the new Apple silicon Macbooks because the Wasp binary is built for x86 and not for arm64 (Apple Silicon). Hence to proceed, you will have to install [R](https://support.apple.com/en-us/102527)[osetta](https://support.apple.com/en-us/102527). Rosetta helps Apple silicon Macs to run apps built for intel Macs. 

To quickly mitigate this issue, run the following command:

```wasp
softwareupdate --install-rosetta
```

If you plan to work with Wasp on a Windows PC, it is recommended to do so with [Windows Subsystem for Linux](https://wasp-lang.dev/blog/2023/11/21/guide-windows-development-wasp-wsl) (WSL): ![Wasp Successful Installation](https://blog.logrocket.com/wp-content/uploads/2024/05/wasp-successful-installation.png) It may take a while to install. For context, it took approximately 35 minutes to install completely on my M1 Macbook.

### Starting a Wasp project

Run the following command to start your new Wasp project:

```wasp
wasp new
```

![Starting A New Wasp Project](https://blog.logrocket.com/wp-content/uploads/2024/05/starting-new-wasp-project.png)

```bash
cd <your-project-name>
wasp start
```

Now your Wasp project should be running on [localhost:3000](http://localhost:3000/): ![Wasp Project Running On Localhost](https://blog.logrocket.com/wp-content/uploads/2024/05/wasp-project-running-localhost.png) This is what the folder structure should look like:

```wasp
├── .wasp
├── public             
├── src                
│   ├── Main.css
│   ├── MainPage.jsx
│   ├── vite-env.d.ts
│   └── waspLogo.png
├── .gitignore
├── .waspignore
├── .wasproot
├── main.wasp  
├── package.json       
├── package-lock.json
├── tsconfig.json
├── vite.config.ts
```

## Setting up Tailwind

To add Tailwind CSS to your Wasp project, follow these steps. First, create a `tailwind.config.cjs` file in the root directory, then add the following code to it:

```javascript
## ./tailwind.config.cjs
const { resolveProjectPath } = require('wasp/dev');
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [resolveProjectPath('./src/**/*.{js,jsx,ts,tsx}')],
  theme: {
    extend: {},
  },
  plugins: [],
};
```

Next, create a `postcss.config.cjs` file with the code below:

```javascript
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};
```

Next, update the `src/Main.css` file to include the Tailwind directives:

```css
>@tailwind base;
@tailwind components;
@tailwind utilities;
```

Make sure you use the `.cjs` file extension and not `.js` so that the files can be detected by Wasp. Also, to make sure that your changes get picked up by Wasp, consider restarting Wasp using the `wasp start` command. 

Now your Wasp project is all set to use Tailwind CSS.

## Architecting our database

To set up the schema for your database, you have to make use of [Wasp Entities](https://wasp-lang.dev/docs/data-model/entities). Entities are how you define where data gets stored in your database. This is usually done using the Prisma Schema Language (PSL) and is defined between the `{=psl psl=}` tags. 

In your `main.wasp` file, add the following code:

```wasp
// ...
entity Note {=psl
    id          Int     @id @default(autoincrement())
    title       String
    description String
psl=}
```

This code defines a Prisma schema for a `Note` entity with three fields: `id`, `title`, and `description`. The `id` field is the primary key and will be automatically generated. 

Next up, you have to update the schema to include this newly added entity. To do this, run the code below:

```wasp
wasp db migrate-dev
```

![Updating Wasp Database](https://blog.logrocket.com/wp-content/uploads/2024/05/updating-wasp-database.png) Don't forget to stop Wasp from running before running this command. Also, anytime you make changes to your entity definition, you have to run the command so that it syncs. 

Now let's take a look at our database. In your terminal, run the following code:

```wasp
wasp db studio
```

![Opening Database Model](https://blog.logrocket.com/wp-content/uploads/2024/05/opening-database-model.png) Click on the **Note** model: ![Note Model](https://blog.logrocket.com/wp-content/uploads/2024/05/note-model.png) This is where the records of our database will be stored.

## Interacting with the database

Wasp offers two main types of operations when interacting with entities: queries and actions. Queries allow you to request data from the database, while actions allow you to create, modify, and delete data.

### Querying the database

First, you have to declare your query in the `main.wasp` file like so:

```wasp
...
query getNotes {
  fn: import { getNotes } from "@src/queries",
  entities: [Note]
}
```

After declaring the query, create a file called `queries.js` in the `./src` directory and add the following code:

```javascript
export const getNotes = async (args, context) => {
  return context.entities.Note.findMany({
    orderBy: { id: 'asc' },
  });
};
```

This code exports a function called `getNotes`, which fetches a list of notes from our database in ascending order.

## Connecting to the frontend

### Building the UI

The UI will be divided into two components: `AddNote.jsx` and `Notes.jsx` . 

This is the code for the `AddNote.jsx` component:

```javascript
export default function AddNote() {
  return (
       <form className='max-w-xl mt-20 mx-auto' onSubmit={handleSubmit}>
      <div className='w-full px-3'>
        <input
          type='text'
          name='title'
          placeholder='Enter note title'
          className='focus:shadow-soft-primary-outline text-sm leading-5.6 ease-soft block w-full appearance-none rounded border border-solid border-gray-300 bg-white bg-clip-padding px-3 py-2 font-normal text-gray-700 outline-none transition-all placeholder:text-gray-500 focus:border-fuchsia-300 focus:outline-none'
        ></input>
        <br />
        <textarea
          rows='4'
          name='description'
          placeholder='Enter note message'
          className='resize-none appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white focus:border-gray-500'
        ></textarea>
      </div>
      <div className='flex justify-between w-full px-3'>
        <div className='md:flex md:items-center'></div>
        <button
          className='shadow bg-indigo-600 hover:bg-indigo-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-6 rounded'
          type='submit'
        >
          Add Note
        </button>
      </div>
    </form>
  );
}
```

The following is the code for the `Notes.jsx` component:

```javascript
import { getNotes, useQuery} from 'wasp/client/operations';

export default function Notes() {
  const { data: notes, isLoading, error } = useQuery(getNotes);
  return (
    <div className='container px-0 py-0 mx-auto'>
      {notes && <Note notes={notes} />}
      {isLoading && 'Loading...'}
      {error && 'Error: ' + error}
    </div>
  );
}

export function Note({ notes }) {
  if (!notes?.length) return <div>No Note Found</div>;

  return (
  <div className='flex flex-wrap -m-4 py-6'>
      {notes.map((note, idx) => (
        <div className='p-4 md:w-1/3' note={note} key={idx}>
          <div className='h-full border-2 border-gray-200 border-opacity-60 rounded-lg overflow-hidden'>
            <div className='p-6'>
              <h1 className='title-font text-lg font-medium text-gray-900 mb-3'>
                {note.title}
              </h1>
              <p className='leading-relaxed mb-3'>{note.description}</p>
              <div className='flex items-center flex-wrap '>
                <a
                  className='text-indigo-500 inline-flex items-center md:mb-2 lg:mb-0'
                  onClick={() => removeNote(note.id)}
                >
                  Delete note
                </a>
              </div>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}
```

In this code, we invoked our query using the `useQuery` Hook to fetch any note available in our database. The `Note` component takes a `notes` prop and maps over the array of notes to render each note as a card with a title, description, and delete button. 

You will see that it shows `No Note Found`**.** This is because the database is empty as we are yet to add a record to it: ![No Note Found Error](https://blog.logrocket.com/wp-content/uploads/2024/05/no-note-found-error.png)

## Adding notes to a database

As stated earlier, this would be accomplished using actions. Just like queries, we have to declare an action first. 

Modify the `main.wasp` file as so:

```wasp
...
action createNote {
  fn: import { createNote } from "@src/actions",
  entities: [Note]
}
```

Now create a `actions.js` file in the `./src` directory with the code below:

```javascript
export const createNote = async (args, context) => {
  return context.entities.Note.create({
    data: { title: args.title, description: args.description },
  });
};
```

This function `createNote` takes in two arguments: `args` and `context`, and creates a new note in the database with the given title and description, which are extracted from the `args` object. 

In your `AddNote.jsx` component, add the following code:

```javascript
import { createNote } from 'wasp/client/operations';

export default function AddNote() {
  const handleSubmit = async (event) => {
    event.preventDefault();
    try {
      const target = event.target;
      const title = target.title.value;
      const description = target.description.value;
      target.reset();
      await createNote({ title, description });
    } catch (err) {
      window.alert('Error: ' + err.message);
    }
  };
...
```

Here, we import the `createNote` action (operation) and set up a function to submit the titles and descriptions obtained from the inputs to our database while resetting the input fields to empty.

## Deleting notes

As usual, we have to declare the delete action in the `main.wasp` file:

```wasp
action deleteNote {
  fn: import { deleteNote } from "@src/actions",
  entities: [Note]
}
```

Then, update our actions file with this code:

```javascript
...
export const deleteNote = async (args, context) => {
  return context.entities.Note.deleteMany({ where: { id: args.id } });
};
```

Next up, modify the `Notes.jsx` with the code below:

```javascript
...
export function Note({ notes }) {
  if (!notes?.length) return <div>No Note Found</div>;

  const removeNote = (id) => {
    if (!window.confirm('Are you sure?')) return;
    try {
      // Call the `deleteNote` operation with this note's ID as its argument
      deleteNote({ id })
        .then(() => console.log(`Deleted note ${id}`))
        .catch((err) => {
          throw new Error('Error deleting note: ' + err);
        });
    } catch (error) {
      alert(error.message);
    }
  };
...
```

Essentially, we created a `removeNote` function to handle deleting notes by their IDs. And that's it! Go ahead and test it out.

## Adding authentication

Now you have your app fully functional, let's add user authentication to allow users to sign in to create notes and show notes belonging to the logged-in user. 

Begin by creating a `User` entity in the `main.wasp` file:

```wasp
// ...
entity User {=psl
    id       Int    @id @default(autoincrement())
psl=}
```

Still in your `main.wasp` file, add the auth configuration. In our case, we would make use of sign-in by `usernameAndPassword`:

```wasp
app wasptutorial {
  wasp: {
    version: "^0.13.1"
  },
  title: "wasptutorial",
  auth: {
    userEntity: User,
    methods: {
      // Enable username and password auth.
      usernameAndPassword: {}
    },
    onAuthFailedRedirectTo: "/login"
  }
}
```

Run `wasp db migrate-dev` to sync these changes.

### Add client login/_s_ignup routes

Next, you need to create routes for both sign-in and login. Modify the `main.wasp` file with the code below:

```wasp
// main.wasp
...
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
  component: import { SignupPage } from "@src/SignupPage"
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
  component: import { LoginPage } from "@src/LoginPage"
}
```

In the `./src` directory, create `LoginPage.jsx` and `SignupPage.jsx` files: 

In `LoginPage.jsx`, add this code:

```javascript
import { Link } from 'react-router-dom'
import { LoginForm } from 'wasp/client/auth'

export const LoginPage = () => {
  return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
      <LoginForm />
      <br />
      <span>
        I don't have an account yet (<Link to="/signup">go to signup</Link>).
      </span>
    </div>
  )
}
```

In `SignupPage.jsx`, add this code:

```javascript
import { Link } from 'react-router-dom'
import { SignupForm } from 'wasp/client/auth'

export const SignupPage = () => {
  return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
      <SignupForm />
      <br />
      <span>
        I already have an account (<Link to="/login">go to login</Link>).
      </span>
    </div>
  )
}
```

### Protecting the main page

Because you do not want unauthorized users to have access to the main page, you will need to restrict it. To do so, modify the `main.wasp` file as follows:

```wasp
// ...
page MainPage {
  authRequired: true,
  component: import { MainPage } from "@src/MainPage"
}
```

Setting `authRequired` to `true` will make sure that all unauthenticated users will be redirected to the login page we just created. 

Now, try accessing the main page at [localhost:3000](http://localhost:3000). You should be redirected to `/login`: ![Redirected To Login](https://blog.logrocket.com/wp-content/uploads/2024/05/redirected-login.png) Because we do not have a user account in our database at this time, you’ll have to sign up to create one.

### Mapping users to notes

At this point, you might notice that all logged-in users are seeing the same notes. To address this, you should ensure that each user can only view notes that they have created. 

In your `main.wasp` file, modify the `User` and `Note` entities as follows:

```wasp
// ...
entity User {=psl
    id       Int    @id @default(autoincrement())
    notes    Note[]
psl=}

entity Note {=psl
    id          Int     @id @default(autoincrement())
    title       String
    description String
    user        User?   @relation(fields: [userId], references: [id])
    userId      Int?
psl=}
```

Here, we defined a `one-to-many` relationship between the users and notes to match each user to their notes. Don't forget to run `wasp db migrate-dev` for these changes to be reflected.

### Checking for authentication

Go to your `queries.js` file and modify the code to forbid non-logged-in users and only request notes belonging to individual logged-in users:

```javascript
import { HttpError } from 'wasp/server';
export const getNotes = async (args, context) => {
  if (!context.user) {
    throw new HttpError(401);
  }
  return context.entities.Note.findMany({
    where: { user: { id: context.user.id } },
    orderBy: { id: 'asc' },
  });
};
```

We also want only logged users to be able to create notes. Modify `actions.js` like so:

```javascript
import { HttpError } from 'wasp/server';
export const createNote = async (args, context) => {
  if (!context.user) {
    throw new HttpError(401);
  }
  return context.entities.Note.create({
    data: {
      title: args.title,
      description: args.description,
      user: { connect: { id: context.user.id } },
    },
  });
};
...
```

### Adding the logout button

The `Logout` button will be in the header component. Create a `Header.jsx` file with the code below:

```javascript
export default function Header() {
  return (
    <header className=' body-font'>
      <div className='container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center'>
        <a className='flex title-font font-medium items-center text-gray-900 mb-4 md:mb-0'>
          <svg
            xmlns='http://www.w3.org/2000/svg'
            fill='none'
            stroke='currentColor'
            strokeLinecap='round'
            strokeLinejoin='round'
            strokeWidth='2'
            className='w-10 h-10 text-white p-2 bg-indigo-500 rounded-full'
            viewBox='0 0 24 24'
          >
            <path d='M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5'></path>
          </svg>
          <span className='ml-3 text-xl'>Noteblocks</span>
        </a>
        <nav className='md:mr-auto md:ml-4 md:py-1 md:pl-4 md:border-l md:border-gray-400 flex flex-wrap items-center text-base justify-center'>
          <a className='mr-5 hover:text-gray-900'>Home</a>
        </nav>
        <button
          className='inline-flex text-white items-center bg-indigo-600 hover:bg-indigo-400  border-0 py-1 px-3 focus:outline-nonerounded text-base mt-4 md:mt-0'
          onClick={logout}
        >
          Log Out
          <svg
            fill='none'
            stroke='currentColor'
            strokeLinecap='round'
            strokeLinejoin='round'
            strokeWidth='2'
            className='w-4 h-4 ml-1'
            viewBox='0 0 24 24'
          >
            <path d='M5 12h14M12 5l7 7-7 7'></path>
          </svg>
        </button>
      </div>
    </header>
  );
}
```

At the top, include this import:

```javascript
import { logout } from 'wasp/client/auth';
...
```

Now, head to the `MainPage.jsx` file and import the `Header` component. And just like that, we have the logout functionality. Don’t forget to test out the logout functionality on the app.

## Deploying Wasp to Fly.io

With the Wasp CLI, you can deploy the React frontend, Node.js backend (server), and PostgreSQL database generated by the Wasp compiler to [Fly.io](https://fly.io/) with a single command. 

Before you can deploy to Fly.io, you should install `flyctl` on your machine. Find the the version for your operating system from the [flyctl documentation](https://fly.io/docs/hands-on/install-flyctl/) and install it. Note that all plans on Fly.io require you to add your card information or deployment will not work.

### Switching databases

Until now, we have been working with the default SQLite database, which is not supported in production. For production, we have to switch to using PostgreSQL. 

Go to your `main.wasp` file and add the following code:

```wasp
app MyApp {
  title: "My app",
  // ...
  db: {
    system: PostgreSQL,
    // ...
  }
}
```

At this point, we don't need the SQLite DB migrations and we can get rid of them by running these commands:

```wasp
rm -r migrations/wasp clean
```

To run the PostgreSQL DB, ensure Docker is running. Next, create a `.env.server` file in the root directory and add your database URL, which can be retrieved by starting the database with the `wasp start db` command. 

Once added, re-run the `wasp start db` and your database will be up and running. While the database is running, open another terminal and run `wasp db migrate-dev` to sync these changes: ![Running The Updated Database](https://blog.logrocket.com/wp-content/uploads/2024/05/running-updated-database.png) Once all that is set, run the following command:

```wasp
wasp deploy fly launch wasp-logrocket-tutorial-app mia
```

Congratulations! Your app is now fully deployed: ![Successfully Deployed App](https://blog.logrocket.com/wp-content/uploads/2024/05/sucessfully-deployed-app.png)

## Conclusion

The Wasp framework offers a great solution that can potentially make building for the web much easier. As the Wasp framework continues to evolve and gain popularity, it will likely be adopted by teams of all sizes seeking an efficient and robust full-stack development experience.