How we built a Trello clone with Wasp - Waspello!

Max - Jun 15 '22 - - Dev Community

We've built a Trello clone using our new language for building Javasript apps! Read on to learn how it went and how you can contribute.

Try Waspello here! | See the code

Wasp is a configuration language (DSL) for building full-stack web apps with less code and best practices that works alongside React and Node.js. We are on a mission to streamline web app development while empowering developers to continue using the power of code and their favorite tools. We are backed by Y Combinator and engineers from Airbnb, Facebook, and Lyft.

We are in Alpha (try it out)!Join our community

Why Trello?

While building Wasp, our goal is to use it as much as we can to build our projects and play with it, so we can learn what works and what we should do next. This is why Trello was a great choice of app to build with Wasp - it is one of the most well-known full-stack web apps, it's very simple and intuitive to use but also covers a good portion of features used by today's modern web apps.

So let's dig in and see and how it went - what works, what doesn't and, what's missing/coming next!

What works?

It's alive ⚡🤖 !!
The good news is all the basic functionality is here - Waspello users can signup/log in which brings them to their project board where they can perform CRUD operations on lists and cards - create them, edit them, move them around, etc. Let's see it in action:

Waspello in action!

As you can see things work, but not everything is perfect (e.g. there is a delay when creating/moving a card) - we'll examine why is that so a bit later.

Under the hood 🚘 🔧
Here is a simple visual overview of Waspello's code anatomy (which applies to every Wasp app):

Waspello code anatomy

Let's now dig in a bit deeper and shortly examine each of the concepts Wasp supports (page, query, entity, ...) and learn through code samples how to use it to implement Waspello.

Entities
It all starts with a data model definition (called entity in Wasp), which is defined via Prisma Schema Language:

main.wasp | Defining entities via Prisma Schema Language:

// Entities

entity User {=psl
    id          Int     @id @default(autoincrement())
    email       String  @unique
    password    String
    lists       List[]
    cards       Card[]
psl=}

entity List {=psl
    id          Int     @id @default(autoincrement())
    name        String
    pos         Float

    // List has a single author.
    user        User    @relation(fields: [userId], references: [id])
    userId      Int

    cards       Card[]
psl=}

entity Card {=psl
    id          Int     @id @default(autoincrement())
    title       String
    pos         Float

    // Card belongs to a single list.
    list        List    @relation(fields: [listId], references: [id])
    listId      Int

    // Card has a single author.
    author      User    @relation(fields: [authorId], references: [id])
    authorId    Int
psl=}
Enter fullscreen mode Exit fullscreen mode

Those three entities are all we need! Wasp uses Prisma to create a database schema underneath and allows the developer to query it through its generated SDK.

Queries and Actions (Operations)
After we've defined our data models, the next step is to do something with them! We can read/create/update/delete an entity and that is what query and action mechanisms are for. Below follows an example from the Waspello code that demonstrates how it works.

The first step is to declare to Wasp there will be a query, point to the actual function containing the query logic, and state from which entities it will be reading information.

main.wasp | Declaration of a query in Wasp:

query getListsAndCards {
    // Points to the function which contains query logic.
    fn: import { getListsAndCards } from "@ext/queries.js",

    // This query depends on List and Card entities.
    // If any of them changes this query will get re-fetched (cache invalidation).
    entities: [List, Card]
}
Enter fullscreen mode Exit fullscreen mode

The main point of this declaration is for Wasp to be aware of the query and thus be able to do a lot of heavy lifting for us - e.g. it will make the query available to the client without any extra code, all that developer needs to do is import it in their React component. Another big thing is cache invalidation / automatic re-fetching of the query once the data changes (this is why it is important to declare which entities it depends on).

The remaining step is to write the function with the query logic.

ext/queries.js | Query logic, using Prisma SDK via Node.js:

export const getListsAndCards = async (args, context) => {
  // Only authenticated users can execute this query.
  if (!context.user) { throw new HttpError(403) }

  return context.entities.List.findMany({
    // We want to make sure user can access only their own cards.
    where: { user: { id: context.user.id } },
    include: { cards: true }
  })
}
Enter fullscreen mode Exit fullscreen mode

This is just a regular Node.js function, there are no limits on what you can return! All the stuff provided by Wasp (user data, Prisma SDK for a specific entity) comes in a context variable.

The code for actions is very similar (we just need to use action keyword instead of query) so I won't repeat it here. You can check out the code for updateCard action here.

Pages, routing & components
To display all the nice data we have, we'll use React components. There are no limits to how you can use React components within Wasp, the only one is that each page has its root component.

main.wasp | Declaration of a page & route in Wasp:

route MainRoute { path: "/", to: Main }
page Main {
    authRequired: true,
    component: import Main from "@ext/MainPage.js"
}
Enter fullscreen mode Exit fullscreen mode

All pretty straightforward so far! As you can see here, Wasp also provides authentication out-of-the-box.

Currently, the majority of the client logic of Waspello is contained in ext/MainPage.js (we should break it down a little 😅 - you can help us!). Just to give you an idea, here's a quick glimpse into it.

ext/MainPage.js | Using React component in Wasp:

// "Special" imports provided by Wasp.
import { useQuery } from '@wasp/queries'
import getListsAndCards from '@wasp/queries/getListsAndCards'
import createList from '@wasp/actions/createList'

const MainPage = ({ user }) => {
  // Fetching data via useQuery.
  const { data: listsAndCards, isFetchingListsAndCards, errorListsAndCards }
    = useQuery(getListsAndCards)

  // A lot of data transformations and sub components.
  ...

  // Display lists and cards.
  return (
    ...
  )
}
Enter fullscreen mode Exit fullscreen mode

Once you've defined a query or action as described above, you can immediately import it into your client code as shown in the code sample, by using the @wasp prefix in the import path. useQuery ensures reactivity so once the data changes the query will get re-fetched. You can find more details about it here.

This is pretty much it from the stuff that works 😄 ! I kinda rushed a bit through things here - for more details on all Wasp features and to build your first app with Wasp, check out our docs.

What doesn't work (yet)

The main problem of the current implementation of Waspello is the lack of support for optimistic UI updates in Wasp. What this means is that currently, when an entity-related change is made (e.g. a card is moved from one list to another), we have to wait until that change is fully executed on the server until it is visible in the UI, which causes a noticeable delay.

In many cases that is not an issue, but when UI elements are all visible at once and it is expected from them to be updated immediately, then it is noticeable. This is also one of the main reasons why we chose to work on Waspello - to have a benchmark/sandbox for this feature! Due to this issue, here's how things currently look like:

Without an optimistic UI update, there is a delay

You can notice the delay between the moment the card is dropped on the "Done" list and the moment it becomes a part of that list. The reason is that at the moment of dropping the card on "Done" list, the API request with the change is sent to the server, and only when that change is fully processed on the server and saved to the database, the query getListsAndCards returns the correct info and consequently, UI is updated to the correct state.
That is why upon dropping on "Done", the card first goes back to the original list (because the change is not saved in db yet, so useQuery(getListsAndCards) still returns the "old" state), it waits a bit until the API request is processed successfully, and just then the change gets reflected in the UI.

The solution

There's a MR in our repo, check it out!

We're almost there!

What's missing (next features)

Although it looks super simple at the first glance, Trello is in fact a huge app with lots and lots of cool features hidden under the surface! Here are some of the more obvious ones that are currently not supported in Waspello:

  • Users can have multiple boards, for different projects (currently we have no notion of a "Board" entity in Waspello at all, so there is implicitly only one)
  • Detailed card view - when clicked on a card, a "full" view with extra options opens
  • Search - user can search for a specific list/card
  • Collaboration - multiple users can participate on the same board

And many more - e.g. support for workspaces (next level of the hierarchy, a collection of boards), card labels, filters.

It is very helpful to have such a variety of features since we can use it as a testing ground for Wasp and use it as a guiding star towards Beta/1.0!

Become a Waspeller!

Image description

If you want to get involved with OSS and at the same time familiarize yourself with Wasp, this is a great way to get started - feel free to choose one of the features listed here or add your own and help us make Waspello the best demo productivity app out there!

Also, make sure to join our community on Discord. We’re always there and are looking forward to seeing what you build!

And yeah, subscribe to our updates. There's so much stuff we want to show!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .