Merge branch 'release'
@ -32,6 +32,10 @@ See a full one-minute demo here:
|
||||
<iframe width="700" height="400" src="https://www.youtube.com/embed/u0MVsPb2MP8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
Check out [this blog post](https://wasp-lang.dev/blog/2023/07/17/how-we-built-gpt-web-app-generator) if you are interested in technical details of how implemented the Generator!
|
||||
|
||||
## The stack 📚
|
||||
|
||||
Besides React & Node.js, GPT Web App Generator uses [Prisma](https://www.prisma.io/) and [Wasp](https://github.com/wasp-lang/wasp).
|
||||
|
572
web/blog/2023-08-01-smol-ai-vs-wasp-ai.md
Normal file
@ -0,0 +1,572 @@
|
||||
---
|
||||
title: 'Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?'
|
||||
authors: [vinny]
|
||||
image: /img/smol-ai-vs-wasp-ai/smol-vs-wasp-banner.png
|
||||
tags: [wasp, ai, gpt, langchain, fullstack, node, react, agent]
|
||||
---
|
||||
import Link from '@docusaurus/Link';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
import InBlogCta from './components/InBlogCta';
|
||||
import WaspIntro from './_wasp-intro.md';
|
||||
import ImgWithCaption from './components/ImgWithCaption'
|
||||
|
||||
### TL;DR
|
||||
|
||||
AI-assisted coding tools are on the rise. In this article, we take a deep dive into two tools that use similar techniques, but are intended for different outcomes.
|
||||
|
||||
[Smol AI’s “Smol-Developer”](https://github.com/smol-ai/developer) gained a lot of notoriety very quickly by being one of the first such tools on the scene. It is a simple set of python scripts that allow a user to build prototype apps using natural language in an iterative approach.
|
||||
|
||||
[Wasp’s “GPT Web App Generator”](https://magic-app-generator.wasp-lang.dev/) is more of a newcomer and focuses on building more complex full-stack React + NodeJS web app prototypes through a simple prompt and fancy UI.
|
||||
|
||||
When comparing the two, Smol-Developer’s strength is its **versatility**. If you want to spend time tinkering and tweaking, you can do a lot to your own prompting, and even the code, in order to get decent results on a **broad range of apps**.
|
||||
|
||||
On the other hand, Wasp AI shines by being **specific.** Because it’s only built for generating full-stack React/NodeJS/Prisma/Tailwind codebases, it does the tweaking and advanced prompting for you, and thus it performs much better in generating **higher quality content** with less effort for a specific use case.
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/smol-ai-vs-wasp-ai/Untitled.png"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
Will either of these tools completely replace Junior Developers in their current form? Of course not. But they do allow for rapid prototyping and testing of novel ideas.
|
||||
|
||||
Read on to learn more about how they work, which tool is right for the job at hand, and how you can use them in your current workflow.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
## Intro
|
||||
|
||||
The age of AI-assisted coding tools is fully upon us. GitHub’s Copilot might be the go-to professional solution, but since its release numerous open-source solutions have popped up.
|
||||
|
||||
Most of these newer solutions tend towards functioning as an AI Agent, going beyond just suggesting the next logical pieces of code within your current file, they aim to create simple prototypes of entire apps. Some are focused more on scaffolding entire app prototypes from an initial prompt, while others function as interactive assistants, helping you modify and improve existing codebases.
|
||||
|
||||
Either way, they’re often being described as “AI Junior Developers”, because they can take a product requirement (i.e. “prompt”) and build a pretty good — but far from perfect — first iteration, saving developers a lot of time.
|
||||
|
||||
This article is going to focus on two tools that aim to build somewhat complex working prototypes from a single prompt: [Smol AI](https://github.com/smol-ai/developer) and [Wasp AI](https://magic-app-generator.wasp-lang.dev/). We’ll test them out by running the same prompts through each and seeing what we get.
|
||||
|
||||
By the end of it, you’ll have a pretty good understanding of how they work, their advantages and disadvantages, and what kind of tasks they’re best suited for.
|
||||
|
||||
## Before We Begin
|
||||
|
||||
[Wasp = }](https://wasp-lang.dev) is the only open-source, completely serverful fullstack React/Node framework with a built-in compiler and AI-assisted features that lets you build your app super quickly.
|
||||
|
||||
We’re working hard to help you build performant web apps as easily as possible — including creating content like this, which is released weekly!
|
||||
|
||||
We would be super grateful if you could help us out by starring our repo on GitHub: [https://www.github.com/wasp-lang/wasp](https://www.github.com/wasp-lang/wasp) 🙏
|
||||
|
||||
![please please please](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5b1bjvpt97e7o2psgle7.gif)
|
||||
|
||||
…e*ven Ron would star [Wasp on GitHub](https://www.github.com/wasp-lang/wasp)* 🤩
|
||||
|
||||
## The Tools
|
||||
|
||||
### Smol-Developer
|
||||
|
||||
Smol AI (described as a platform for “model distillation and AI developer agents”) actually has a few open-source tools on offer, but Smol-Developer is the one we’ll be taking a look at. It was initially released by [Swyx](https://twitter.com/swyx) on May 11th and already has over 10k GitHub stars!
|
||||
|
||||
It aims to be a generalist, prompt-based coding assistant run from the command line. The developer’s job becomes a process of iterative prompting, testing, and re-prompting in order to get the optimal output. It is not limited to any language or type of app it can create, although simple apps tend to work best.
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/smol-ai-vs-wasp-ai/smol-ai-tweet.png"
|
||||
width="400px"
|
||||
/>
|
||||
|
||||
Check out this tweet thread above to get a better understanding: [https://twitter.com/swyx/status/1657892220492738560](https://twitter.com/swyx/status/1657892220492738560)
|
||||
|
||||
Running from the command line, Smol AI is essentially a chain of calls to the OpenAI chat completions (i.e. “ChatGpt”) endpoint via a python script that:
|
||||
|
||||
1. takes an initial user-generated prompt
|
||||
2. creates a plan based on internal prompts* for executing the app with:
|
||||
1. the structure of the entire app
|
||||
2. each file and its exported variables to be generated
|
||||
3. function names
|
||||
3. generates file paths based on the plan
|
||||
4. loops through file paths and generates code for each file based on plan and prompt
|
||||
|
||||
The generated output can then be evaluated by the developer and the prompt can be iterated on to account for any errors or bugs found during runtime.
|
||||
|
||||
Smol-Developer quickly gained notoriety by being one of the first of such tools on the scene, in addition to Swyx’s prominence within it. So if you’re curious to see what’s being built with it, just check out some of the numerous YouTube videos on it.
|
||||
|
||||
One of my personal favorites is AI Jason’s exposé and commentary. He gives a concise explanation, shows you some great tips on how to use Smol-Developer effectively, and as a Product Designer/Manager he gives an interesting perspective on its benefits:
|
||||
|
||||
<!-- [https://www.youtube.com/watch?v=BMRywudsqtY](https://www.youtube.com/watch?v=BMRywudsqtY) -->
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/BMRywudsqtY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
- Smol-Developer GitHub Repo: [https://github.com/smol-ai/developer/](https://github.com/smol-ai/developer/)
|
||||
|
||||
<details>
|
||||
<summary> *Curious to see what the internal system prompt looks like? </summary>
|
||||
|
||||
|
||||
You are a top tier AI developer who is trying to write a program that will generate code for the user based on their intent.
|
||||
|
||||
Do not leave any todos, fully implement every feature requested.
|
||||
|
||||
When writing code, add comments to explain what you intend to do and why it aligns with the program plan and specific instructions from the original prompt.
|
||||
|
||||
In response to the user's prompt, write a plan.
|
||||
|
||||
In this plan, please name and briefly describe the structure of the app we will generate, including, for each file we are generating, what variables they export, data schemas, id names of every DOM elements that javascript functions will use, message names, and function names.
|
||||
|
||||
Respond only with plans following the above schema.
|
||||
|
||||
the app prompt is: {prompt}
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
### Wasp’s GPT Web App Generator
|
||||
|
||||
In contrast to Smol-Developer, Wasp’s AI tool, [GPT Web App Generator](https://magic-app-generator.wasp-lang.dev/), is currently an open-source web app (yes, it’s a web app that makes web apps). Since it’s release on the 12th of July, there have been over 6,500 apps generated with over 300 apps being generated each day!
|
||||
|
||||
<!-- ![Untitled](../static/img/smol-ai-vs-wasp-ai/Untitled%201.png) -->
|
||||
<ImgWithCaption
|
||||
source="img/smol-ai-vs-wasp-ai/Untitled%201.png"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
Here’s a quick 1 minute video showcasing how [GPT Web App Generator](https://magic-app-generator.wasp-lang.dev/) works:
|
||||
|
||||
<!-- [https://www.youtube.com/watch?v=u0MVsPb2MP8](https://www.youtube.com/watch?v=u0MVsPb2MP8) -->
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/u0MVsPb2MP8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
So to give a bit of background, [Wasp](https://wasp-lang.dev) is actually a full-stack web app framework built around a compiler and config file. Using this approach, Wasp simplifies the web app creation process by handling boilerplate code for you, taking the core app logic written by the developer and connecting the entire stack together from frontend to backend, and database management.
|
||||
|
||||
It currently works with React, NodeJS, Tanstack-Query, and Prisma, taking care of features like Auth, Routing, Cron Jobs, Fullstack Typesafety, and Caching. This allows developers to focus more on the fun stuff, like the app’s features, instead of spending time on boring configurations.
|
||||
|
||||
Because Wasp uses a compiler and config file to generate the app from, this makes it surprisingly well suited for guiding LLMs like ChatGPT towards creating more complex apps with it, as it essentially a plan or set of instructions for how to build the app!
|
||||
|
||||
Take this simple example of how you’d tell Wasp that you want `username and password` authentication in your app:
|
||||
|
||||
```jsx
|
||||
// main.wasp file
|
||||
|
||||
app RecipeApp {
|
||||
title: "My Recipes",
|
||||
wasp: { version: "^0.11.0" },
|
||||
auth: {
|
||||
methods: { usernameAndPassword: {} },
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
userEntity: User
|
||||
}
|
||||
}
|
||||
|
||||
entity User {=psl // Data models are defined using Prisma Schema Language.
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
recipes Recipe[]
|
||||
psl=}
|
||||
```
|
||||
|
||||
Wasp’s config file is like an app outline that the compiler understands and can then use to connect and glue the app together, taking care of the boilerplate for you.
|
||||
|
||||
By leveraging the powers of Wasp, GPT Web App Generator works by:
|
||||
|
||||
1. taking a simple user-generated prompt via the UI
|
||||
2. giving GPT a descriptive example of a Wasp app and config file via internal prompts*
|
||||
3. creating a plan that meets these requirements
|
||||
4. generating the code for each part of the app according to the plan
|
||||
5. checking each file for expected errors/hallucinations and fixing them
|
||||
|
||||
In the end, the user can download the codebase as a zipped file and run it locally. Simpler apps, such as [TodoApp](https://magic-app-generator.wasp-lang.dev/result/07ed440a-3155-4969-b3f5-2031fb1f622f) or [MyPlants](https://magic-app-generator.wasp-lang.dev/result/3bb5dca2-f134-4f96-89d6-0812deab6e0c) tend to work straight out of the box, while more complex apps need a bit of finessing to get working.
|
||||
|
||||
- Try out the GPT Web App Generator at: [https://magic-app-generator.wasp-lang.dev/](https://magic-app-generator.wasp-lang.dev/) or via the command line via Wasp's [experimental release](https://magic-app-generator.wasp-lang.dev/#:~:text=%5BAdvanced%5D%20Can%20I%20use%20GPT4%20for%20the%20whole%20app%3F)
|
||||
- Wasp AI / Generator GitHub: [https://github.com/wasp-lang/wasp/tree/wasp-ai/waspc/src/Wasp/AI](https://github.com/wasp-lang/wasp/tree/wasp-ai/waspc/src/Wasp/AI)
|
||||
|
||||
<details>
|
||||
<summary> *Curious to see what the internal system prompt looks like? </summary>
|
||||
<div>
|
||||
|
||||
Wasp is a full-stack web app framework that uses React (for client), NodeJS and Prisma (for server).
|
||||
High-level of the app is described in main.wasp file (which is written in special Wasp DSL), details in JS/JSX files.
|
||||
Wasp DSL (used in main.wasp) reminds a bit of JSON, and doesn't use single quotes for strings, only double quotes. Examples will follow.
|
||||
|
||||
Important Wasp features:
|
||||
- Routes and Pages: client side, Pages are written in React.
|
||||
- Queries and Actions: RPC, called from client, execute on server (nodejs).
|
||||
Queries are for fetching and should not do any mutations, Actions are for mutations.
|
||||
- Entities: central data models, defined via PSL (Prisma schema language), manipulated via Prisma.
|
||||
Typical flow: Routes point to Pages, Pages call Queries and Actions, Queries and Actions work with Entities.
|
||||
|
||||
Example main.wasp (comments are explanation for you):
|
||||
|
||||
```wasp
|
||||
app todoApp {
|
||||
wasp: { version: "^0.11.1" },
|
||||
title: "ToDo App",
|
||||
auth: {
|
||||
userEntity: User,
|
||||
methods: { usernameAndPassword: {} },
|
||||
onAuthFailedRedirectTo: "/login"
|
||||
},
|
||||
client: {
|
||||
rootComponent: import { Layout } from "@client/Layout.jsx",
|
||||
},
|
||||
db: {
|
||||
prisma: {
|
||||
clientPreviewFeatures: ["extendedWhereUnique"]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import Signup from "@client/pages/auth/Signup.jsx"
|
||||
}
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
page LoginPage {
|
||||
component: import Login from "@client/pages/auth/Login.jsx"
|
||||
}
|
||||
|
||||
route DashboardRoute { path: "/", to: Dashboard }
|
||||
page DashboardPage {
|
||||
authRequired: true,
|
||||
component: import Dashboard from "@client/pages/Dashboard.jsx"
|
||||
}
|
||||
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
tasks Task[]
|
||||
psl=}
|
||||
|
||||
entity Task {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
description String
|
||||
isDone Boolean @default(false)
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
psl=}
|
||||
|
||||
query getUser {
|
||||
fn: import { getUser } from "@server/queries.js",
|
||||
entities: [User] // Entities that this query operates on.
|
||||
}
|
||||
|
||||
query getTasks {
|
||||
fn: import { getTasks } from "@server/queries.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action createTask {
|
||||
fn: import { createTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
|
||||
action updateTask {
|
||||
fn: import { updateTask } from "@server/actions.js",
|
||||
entities: [Task]
|
||||
}
|
||||
```
|
||||
|
||||
We are looking for a plan to build a new Wasp app (description at the end of prompt).
|
||||
|
||||
Instructions you must follow while generating plan:
|
||||
- App uses username and password authentication.
|
||||
- App MUST have a 'User' entity, with following fields required:
|
||||
- `id Int @id @default(autoincrement())`
|
||||
- `username String @unique`
|
||||
- `password String`
|
||||
It is also likely to have a field that refers to some other entity that user owns, e.g. `tasks Task[]`.
|
||||
- One of the pages in the app must have a route path "/".
|
||||
- Don't generate the Login or Signup pages and routes under any circumstances. They are already generated.
|
||||
|
||||
Plan is represented as JSON with the following schema:
|
||||
|
||||
{
|
||||
"entities": [{ "entityName": string, "entityBodyPsl": string }],
|
||||
"actions": [{ "opName": string, "opFnPath": string, "opDesc": string }],
|
||||
"queries": [{ "opName": string, "opFnPath": string, "opDesc": string }],
|
||||
"pages": [{ "pageName": string, "componentPath": string, "routeName": string, "routePath": string, "pageDesc": string }]
|
||||
}
|
||||
|
||||
Here is an example of a plan (a bit simplified, as we didn't list all of the entities/actions/queries/pages):
|
||||
|
||||
{
|
||||
"entities": [{
|
||||
"entityName": "User",
|
||||
"entityBodyPsl": " id Int @id @default(autoincrement())\n username String @unique\n password String\n tasks Task[]"
|
||||
}],
|
||||
"actions": [{
|
||||
"opName": "createTask",
|
||||
"opFnPath": "@server/actions.js",
|
||||
"opDesc": "Checks that user is authenticated and if so, creates new Task belonging to them. Takes description as an argument and by default sets isDone to false. Returns created Task."
|
||||
}],
|
||||
"queries": [{
|
||||
"opName": "getTask",
|
||||
"opFnPath": "@server/queries.js",
|
||||
"opDesc": "Takes task id as an argument. Checks that user is authenticated, and if so, fetches and returns their task that has specified task id. Throws HttpError(400) if tasks exists but does not belong to them."
|
||||
}],
|
||||
"pages": [{
|
||||
"pageName": "TaskPage",
|
||||
"componentPath": "@client/pages/Task.jsx",
|
||||
"routeName: "TaskRoute",
|
||||
"routePath": "/task/:taskId",
|
||||
"pageDesc": "Diplays a Task with the specified taskId. Allows editing of the Task. Uses getTask query and createTask action.",
|
||||
}]
|
||||
}
|
||||
|
||||
We will later use this plan to write main.wasp file and all the other parts of Wasp app,
|
||||
so make sure descriptions are detailed enough to guide implementing them.
|
||||
Also, mention in the descriptions of actions/queries which entities they work with,
|
||||
and in descriptions of pages mention which actions/queries they use.
|
||||
|
||||
Typically, plan will have AT LEAST one query, at least one action, at least one page, and at
|
||||
least two entities. It will very likely have more than one of each, though.
|
||||
|
||||
DO NOT create actions for login and logout under any circumstances. They are already included in Wasp.
|
||||
|
||||
Note that we are using SQLite as a database for Prisma, so don't use scalar arrays in PSL, like `String[]`,
|
||||
as those are not supported in SQLite. You can of course normally use arrays of other models, like `Task[]`.
|
||||
|
||||
Please, respond ONLY with a valid JSON that is a plan.
|
||||
There should be no other text in the response.
|
||||
|
||||
==== APP DESCRIPTION: ====
|
||||
|
||||
App name: TodoApp
|
||||
A simple todo app with one main page that lists all the tasks. User can create new tasks by providing their description, toggle existing ones, or edit their description. User owns tasks. User can only see and edit their own tasks. Tasks are saved in the database.
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
## Comparison Test
|
||||
|
||||
### Prompt 1: PONG Game
|
||||
|
||||
To get a sense for how each coding agent performed, I tried out two different prompts on both Smol-Developer and Wasp’s GPT Web App Generator with only slight modifications to the prompts to fit the requirements of each tool.
|
||||
|
||||
The first prompt was the default prompt that comes hardcoded into Smol-Developer’s `[main.py](http://main.py)` script:
|
||||
|
||||
> *a simple JavaScript/HTML/CSS/Canvas app that is a one player game of PONG. The left paddle is controlled by the player, following where the mouse goes. The right paddle is controlled by a simple AI algorithm, which slowly moves the paddle toward the ball at every frame, with some probability of error. Make the canvas a 400 x 400 black square and center it in the app. Make the paddles 100px long, yellow and the ball small and red. Make sure to render the paddles and name them so they can controlled in javascript. Implement the collision detection and scoring as well. Every time the ball bounces off a paddle, the ball should move faster.*
|
||||
>
|
||||
|
||||
:::note
|
||||
💡 For Wasp’s GPT Web App Generator, I replaced the first line with “a simple one player game of PONG” since Wasp will automatically generate a full-stack React/NodeJS app.
|
||||
|
||||
:::
|
||||
|
||||
Both were able to create a functional PONG game out-of-the box, but only on the second try. The first try created decent PONG starters, but both had buggy game logic (e.g. computer opponent failed to hit ball, or ball would spin off into oblivion). I didn’t change the prompts at all, but just simply ran them a second time each — and that did the trick!
|
||||
|
||||
<!-- ![Smol AI’s PONG game](../static/img/smol-ai-vs-wasp-ai/Untitled%202.png) -->
|
||||
<ImgWithCaption
|
||||
source="img/smol-ai-vs-wasp-ai/Untitled%202.png"
|
||||
width="400px"
|
||||
caption="Smol AI’s PONG game"
|
||||
/>
|
||||
|
||||
<!-- ![Wasp’s PONG game](../static/img/smol-ai-vs-wasp-ai/Untitled%203.png) -->
|
||||
<ImgWithCaption
|
||||
source="img/smol-ai-vs-wasp-ai/Untitled%203.png"
|
||||
width="400px"
|
||||
caption="Wasp AI’s PONG game"
|
||||
/>
|
||||
|
||||
|
||||
For both of the generated apps, the game logic was very simple. Scores weren’t recorded, and once a game ended, you’d have to refresh the page to start a new one.
|
||||
|
||||
Although, while Smol-Developer only created the game logic, GPT Web App Generator created the game logic as well as the logic for authentication, creating games, and updating a game’s score, saving it all to the database (though the scoring functions weren’t being utilized initially).
|
||||
|
||||
To be fair, this isn’t really a surprise though as these features are baked into the design of Wasp and the Generator.
|
||||
|
||||
On the other hand, to get these same features for Smol-Developer, we’d have to elaborate on our prompt, giving it explicit instructions to implement them, and iterate on it a number of times before landing on an acceptable prototype.
|
||||
|
||||
This is what I attempted to test out with the second prompt.
|
||||
|
||||
### Prompt 2: Blog App
|
||||
|
||||
![Untitled](../static/img/smol-ai-vs-wasp-ai/Untitled%204.png)
|
||||
|
||||
This time, for the second app test, I used a default prompt featured on the GPT Web App Generator homepage for creating a Blog app:
|
||||
|
||||
> A blogging platform with posts and post comments.
|
||||
User owns posts and comments and they are saved in the database.
|
||||
Everybody can see all posts, but only the owner can edit or delete them. Everybody can see all the comments.
|
||||
App has four pages:
|
||||
>
|
||||
> 1. "Home" page lists all posts (their titles and authors) and is accessible by anybody.
|
||||
> If you click on a post, you are taken to the "View post" page.
|
||||
> It also has a 'New post' button, that only logged in users can see, and that takes you to the "New post" page.
|
||||
> 2. "New post" page is accessible only by the logged in users. It has a form for creating a new post (title, content).
|
||||
> 3. "Edit post" page is accessible only by the post owner. It has a form for editing the post with the id specified in the url.
|
||||
> 4. "View post" page is accessible by anybody and it shows the details of the post with the id specified in the url: its title, author, content and comments.
|
||||
> It also has a form for creating a new comment, that is accessible only by the logged in users.
|
||||
|
||||
:::note
|
||||
💡 For the Smol-Developer prompt, I added the lines: “The app consists of a React client and a NodeJS server. Posts are saved in an sqlite database using Prisma ORM.”
|
||||
:::
|
||||
|
||||
<!-- [Running Wasp’s GPT Web App Generator](https://youtu.be/8A_i5L9MJ90) -->
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/8A_i5L9MJ90" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
|
||||
As this was a suggested prompt on the GPT Web App Generator page, let’s start with the Wasp app result first.
|
||||
|
||||
After downloading the generated codebase and running the app, I ran into an error `Failed to resolve import "./ext-src/ViewPost.jsx" from "src/router.jsx". Does the file exist?`
|
||||
|
||||
<!-- ![Untitled](../static/img/smol-ai-vs-wasp-ai/Untitled%205.png) -->
|
||||
<ImgWithCaption
|
||||
source="img/smol-ai-vs-wasp-ai/Untitled%205.png"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
|
||||
One quick look at the `main.wasp` file revealed that the Generator gave the wrong path to the `ViewPost` page, although it did get all the other Page paths correct (highlighted in yellow above).
|
||||
|
||||
Once that path was corrected, a working app popped up at localhost:3000. Nice!
|
||||
|
||||
<!-- ![Kapture 2023-07-27 at 11.49.19.mp4](../static/img/smol-ai-vs-wasp-ai/Kapture_2023-07-27_at_11.49.19.mp4) -->
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/c8JacesyTe8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
The video above was my first time trying out the app, and as you can see, most of the functionality is there and working correctly — Authentication and Authorization, and basic CRUD operations. Pretty amazing!
|
||||
|
||||
There were still a couple of errors that prevented the app from being fully functional out-of-the-box, but they were easy to fix:
|
||||
|
||||
1. Blog posts on the homepage did not have a link in order to redirect to the their specific post page — fixable by just wrapping them in `<Link to={`/post/${post.id}`}>`
|
||||
2. The client was passing the `postId` as a String instead of an `Int` to the `getPost` endpoint — fixable by wrapping the argument in `parseInt(postId)` to convert strings to integers
|
||||
|
||||
<!-- ![Untitled](../static/img/smol-ai-vs-wasp-ai/Untitled%206.png) -->
|
||||
<ImgWithCaption
|
||||
source="img/smol-ai-vs-wasp-ai/Untitled%206.png"
|
||||
width="400px"
|
||||
/>
|
||||
|
||||
And with those simple fixes we got a fully functioning, full-stack blog app with authentication, database, and simple tailwind css styling! The best part was that all this took about ~5 minutes from start to finish. Sweet :)
|
||||
|
||||
:::note
|
||||
🧑💻 The Generator saves all the apps it creates along with a sharable link, so if you want to check out the original generated Blog app code (before fixes) from above, click here: [https://magic-app-generator.wasp-lang.dev/result/a3a76887-952b-4774-a773-42209c4bffa8](https://magic-app-generator.wasp-lang.dev/result/a3a76887-952b-4774-a773-42209c4bffa8)
|
||||
|
||||
:::
|
||||
|
||||
<!-- [Running Smol-Developer](https://youtu.be/oT0pCbN-JgE?t=53) -->
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/oT0pCbN-JgE?start=50" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
The Smol-Developer result was also very impressive, with a solid ExpressJS server and a lot of React client pages, but there were too many complicated errors that prevented me from getting the app started, including but not limited to:
|
||||
|
||||
1. No build tools or configuration files
|
||||
2. The server was importing database models that didn’t exist
|
||||
3. The server was importing but not utilizing Prisma as the ORM to communicate with the DB
|
||||
4. Client had Auth logic, but was not utilizing it to protect pages/routes
|
||||
|
||||
![Untitled](../static/img/smol-ai-vs-wasp-ai/Untitled%207.png)
|
||||
|
||||
Because there were too many fundamental issues with the app, I went ahead and added some more lines to the bottom of the prompt:
|
||||
|
||||
> …
|
||||
>
|
||||
>Scaffold the app to be able to use Vite as the client's build tool. Include a package.json file >with the dependencies and scripts for running the client and server.
|
||||
>
|
||||
|
||||
This second attempt produced some of the changes I was looking for, like package.json files and Vite config files to bootstrap the React app, but it still **failed** to include:
|
||||
|
||||
1. An index.html file
|
||||
2. Package.json files with the correct dependencies being imported from within the client and server
|
||||
3. A `prisma.schema` file
|
||||
4. A css file (although it did include `classNames` in the jsx code)
|
||||
|
||||
On the other hand, the server code, albeit much sparser this time, did at least import and use Prisma correctly.
|
||||
|
||||
So I went ahead for a third attempt and modified and added the following lines to the bottom of the prompt:
|
||||
|
||||
> …
|
||||
>
|
||||
>Scaffold the app to be able to use Vite as the client's build tool.
|
||||
>
|
||||
>Make sure to include the following:
|
||||
>1. package.json files for both the server and client. Make sure that these files include the >dependencies being imported in the respective apps.
|
||||
>2. an index.html file in the client's public folder, so that Vite can build the app.
|
||||
>3. a `prisma.schema` file with the models and their fields. Make sure these are the same models >being used app-wide.
|
||||
>4. a css file with styles that match the `className`s used in the app.
|
||||
>
|
||||
|
||||
With these additions to the prompt, the third iteration of the app did in fact include them! Well, most of them, but unfortunately not all of them. Now I was getting the css and package.json files, but no vite config file was created this time, even though the instructions for using “Vite as the client’s build tool” produced one previously.
|
||||
|
||||
Besides that, no auth logic was implemented, imports were out place or missing, and an `index.jsx` file was also nowhere to be found, so I decided to stop there.
|
||||
|
||||
<!-- ![Untitled](../static/img/smol-ai-vs-wasp-ai/Untitled%208.png) -->
|
||||
<ImgWithCaption
|
||||
source="img/smol-ai-vs-wasp-ai/Untitled%208.png"
|
||||
width="400px"
|
||||
/>
|
||||
|
||||
I’m sure I could have iterated on the prompt enough times until I got closer to a working app, but at ~$0.80-$1.20 a generation, I didn’t feel like racking up more of an OpenAI bill.
|
||||
|
||||
:::note
|
||||
💸 Price per generation is another big difference between the Smol AI and Wasp AI. Because more work is being done by Wasp’s compiler and less by GPT, each app costs about ~$0.10-$0.20 to generate (although Wasp covers the cost and allows you to use it for free), whereas to generate [complex full-stack apps with Smol-Developer can cost upwards of ~$10.00](https://www.youtube.com/watch?v=zsxyqz6SYp8)!
|
||||
|
||||
:::
|
||||
|
||||
Plus, there are plenty of YouTubers who’ve created videos about the process of using Smol-Developer and it seems they all come to similar conclusions: you need to create a very detailed and explicit prompt in order to get a working prototype (In fact, in AI Jason’s Smol-AI video above, he mentioned that he got the best results out of the box when prompting Smol-Developer to write everything to one file only — of course this limits you to generating simple apps only that are not so easy to continue from manually).
|
||||
|
||||
## Thoughts & Further Considerations
|
||||
|
||||
At their core, SmolAI and WaspAI function quite similarly, by first prompting the LLM to create a plan for the app’s architecture, and then to execute on that plan, file by file.
|
||||
|
||||
But because Smol-Developer aims to be able to generate a wider range of apps, the expectation is on the Developer (or “Prompt Engineer”) to create a highly detailed, explicit prompt, which is more akin to a Product Requirement Doc that a Product Designer would write. This can take a few iterations to get right and pushes Smol-Developer in the direction of “Natural Language Programming” tool.
|
||||
|
||||
On the other hand, Wasp’s GPT Web App Generator has a lot of prompting and programming going on behind the scenes, abstracted away from the user and hidden within the Generator’s code and Wasp’s compiler. Wasp comes with a lot of knowledge baked in and already has a good idea of what it wants to build, which means the user has less to think about it. This means that we’re more likely to get a working complex prototype from a short, simple prompt, but we have less flexibility in the kinds of apps we’re able to create — we always get a full-stack web app.
|
||||
|
||||
In general, Wasp is like a junior developer specialized in web dev and has a lot of experience with a specific stack, while Smol AI is a junior developer that’s a generalist who is more versatile, but has less specific knowledge and experience with web dev 🙂
|
||||
|
||||
| | Smol AI | Wasp AI |
|
||||
| --- | --- | --- |
|
||||
| 🧑💻 Types of Apps | Varied | Full-stack Web Apps |
|
||||
| 🗯 Programming Languages | All Types | JavaScript/TypeScript |
|
||||
| 📈 Complexity of Generated App | Simple to Medium | Medium to Complex |
|
||||
| 💰 Price per Generation — via OpenAI’s API | $0.80 to $10.00 | $0.10 to $0.20 |
|
||||
| 💳 Payment Method | bring your own API key | free — paid for by Wasp |
|
||||
| 🐛 Debugging | Yes, if you’re willing to tinker | Built-in, but limited |
|
||||
| 🗣 Type of Prompt Needed | Complex and detailed, 1 or more pages (e.g. an entire Product Requirement Doc) | Simple, 1-3 paragraphs |
|
||||
| 😎 Intended User | Engineers, Product Designers wanting to generate a broad range of simple prototypes | Web Devs, Product Designers that want a feature rich full-stack web app prototype |
|
||||
|
||||
Other big differences lie within:
|
||||
|
||||
1. Error Correction upon Code Creation
|
||||
1. Smol AI initially had a debugging script, but this has temporarily deprecated due to the fact that it expects the entire codebase when debugging, and current 32k and 100k token context windows are only available in private beta for GPT4 and Anthropic at the moment.
|
||||
2. Wasp AI has some error correction baked into its process, as the structure of a Wasp app is more defined and the range of errors are more predictable.
|
||||
2. Price per app generation via OpenAI’s chat completion endpoints
|
||||
1. Smol AI can cost anywhere from **~$0.80 to $10.00** [depending on the complexity of the app](https://www.youtube.com/watch?v=zsxyqz6SYp8).
|
||||
2. Wasp AI costs ~**$0.10 to $0.20** per app, when using the default mix of GPT 4 and GPT 3.5 turbo, but Wasp covers the bill here. If you choose to run [it just with GPT4](https://magic-app-generator.wasp-lang.dev/#:~:text=%5B-,Advanced,-%5D%20Can%20I%20use), then the cost is 10x at **$1.00 to $2.00** per generation and you have to provide your own API key.
|
||||
3. User Interface
|
||||
1. Smol Developer works through the command line and has minimal logging and process feedback
|
||||
2. Wasp AI currently uses a clean web app UI with more logging and feedback, as well as through the command line without a UI (you have to download the [experimental Wasp release](https://magic-app-generator.wasp-lang.dev/#:~:text=%5BAdvanced%5D%20Can%20I%20use%20GPT4%20for%20the%20whole%20app%3F) to do so at this time).
|
||||
|
||||
Overall, both solutions produce amazing results, allowing solo developers or teams iterate on ideas and generate prototypes faster than before. But they still have a lot of room for improvement.
|
||||
|
||||
For example, what these tools lack the most at the moment is in interactive debugging and incremental generation. It would be great if they could allow the user to generate additional code and fix problems in the codebase on the fly, rather than having to go back, rewrite the prompt, and regenerate an entire new codebase.
|
||||
|
||||
I’m not aware of the Smol AI roadmap, but seeing that it’s received a grant from [Vercel’s AI accelerator program,](https://vercel.com/blog/ai-accelerator-participants) I’m sure we will be seeing development on it continue and the tool improve (let me know in the comments if you do have some insight here).
|
||||
|
||||
On the other hand, as I’m a member of the Wasp team, I can confidently say that Wasp will soon be adding the initial generation process and interactive debugging into Wasp’s command line interface!
|
||||
|
||||
So I definitely think it’s early days and that these tools will continue to progress — and continue to produce more impressive results 🚀
|
||||
|
||||
## Which Tool Should You Use?
|
||||
|
||||
Obviously, there can be no clear winner here as the answer to question of which tool you should use as your next “AI Junior Developer” depends largely on your goals.
|
||||
|
||||
Are you looking for a tool that can generate a broad range of simple apps? And are you interested in learning more about building AI-assisted coding tools and natural language programming and don’t mind tweaking and tinkering for a while? Well then, [Smol-Developer](https://github.com/smol-ai/developer) is what you’re looking for!
|
||||
|
||||
Do you want to generate a working full-stack React/Node app prototype with all the bells and whistles as quickly and easily as possible? Head straight for Wasp’s [GPT Web App Generator](https://magic-app-generator.wasp-lang.dev/)!
|
||||
|
||||
:::info Help me help you
|
||||
🌟 **If you haven’t yet, please** [star us on GitHub](https://www.github.com/wasp-lang/wasp), especially if you found this useful! If you do, it helps support us in creating more content like this. And if you don’t… well, we will deal with it, I guess.
|
||||
|
||||
![https://media.giphy.com/media/3oEjHEmvj6yScz914s/giphy.gif](https://media.giphy.com/media/3oEjHEmvj6yScz914s/giphy.gif)
|
||||
:::
|
||||
|
||||
In general, as Jason “AI Jason” Zhou said:
|
||||
|
||||
> “*I’m really excited about [AI-assisted coding tools] because if I want to user-test a certain product idea I can ask it to build a prototype very, very quickly, and test with real users”*
|
||||
>
|
||||
|
||||
Jason makes a great point here, that these tools don’t really have the capacity to replace Junior Developers entirely in their current capacity (although they will surely improve in the future), but they do improve the speed and ease with which we can try out novel ideas!
|
||||
|
||||
I personally believe that in the near future we will see more domain-specific AI-assisted tools like Wasp’s [GPT Web App Generator](https://magic-app-generator.wasp-lang.dev) because of the performance gains they bring to the end user. Code agents that are focused on a niche can produce better results out of the box due to the embedded knowledge. In the future, I think we can expect a lot of agents that are each tailored towards fulfilling a specific task.
|
||||
|
||||
But don’t just take my word for it. Go ahead try out [Smol-Developer](https://github.com/smol-ai/developer) and the [GPT Web App Generator](https://magic-app-generator.wasp-lang.dev) for yourself and let me know what you think in the comments!
|
@ -0,0 +1,888 @@
|
||||
---
|
||||
title: 'Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️'
|
||||
authors: [vinny]
|
||||
image: /img/websockets-app/websockets-resized.png
|
||||
tags: [wasp, websockets, react, typescript, real-time, node, express]
|
||||
---
|
||||
import Link from '@docusaurus/Link';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
import InBlogCta from './components/InBlogCta';
|
||||
import WaspIntro from './_wasp-intro.md';
|
||||
import ImgWithCaption from './components/ImgWithCaption'
|
||||
|
||||
|
||||
## TL;DR
|
||||
|
||||
WebSockets allow your app to have “real time” features, where updates are instant because they’re passed on an open, two-way channel. This is a different from CRUD apps, which usually use HTTP requests that must establish a connection, send a request, receive a response, and then close the connection.
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled.png"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
To use WebSockets in your React app, you’ll need a dedicated server, such as an ExpressJS app with NodeJS, in order to maintain a persistent connection.
|
||||
|
||||
Unfortunately, serverless solutions (e.g. NextJS, AWS lambda) don’t natively support WebSockets. Bummer. 😞
|
||||
|
||||
Why not? Well, serverless services turn on and off depending on if a request is coming in. With WebSockets, we need this “always on” connection that only a dedicated server can provide (although you can pay for third-party services as a workaround).
|
||||
|
||||
Luckily, we’re going to talk about two great ways you can implement them:
|
||||
|
||||
1. Implementing and configuring it yourself with React, NodeJS, and Socket.IO
|
||||
2. By using [Wasp](https://wasp-lang.dev), a full-stack React-NodeJS framework, to configure and integrate Socket.IO into your app for you.
|
||||
|
||||
These methods allow you to build fun stuff, like this instantly updating “voting with friends” app we built here (check out the [GitHub repo for it](https://github.com/vincanger/websockets-wasp)):
|
||||
|
||||
<!-- [https://www.youtube.com/watch?v=Twy-2P0Co6M](https://www.youtube.com/watch?v=Twy-2P0Co6M) -->
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/Twy-2P0Co6M" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
## Why WebSockets?
|
||||
|
||||
So, imagine you're at a party sending text messages to a friend to tell them what food to bring.
|
||||
|
||||
Now, wouldn’t it be easier if you called your friend on the phone so you could talk constantly, instead of sending sporadic messages? That's pretty much what WebSockets are in the world of web applications.
|
||||
|
||||
For example, traditional HTTP requests (e.g. CRUD/RESTful) are like those text messages — your app has to **ask the server** every time it wants new information, just like you had to send a text message to your friend every time you thought of food for your party.
|
||||
|
||||
But with WebSockets, once a connection is established, it **remains open** for constant, two-way communication, so the server can send new information to your app the instant it becomes available, even if the client didn’t ask for it.
|
||||
|
||||
This is perfect for real-time applications like chat apps, game servers, or when you're keeping track of stock prices. For example, apps like Google Docs, Slack, WhatsApp, Uber, Zoom, and Robinhood all use WebSockets to power their real-time communication features.
|
||||
|
||||
![https://media3.giphy.com/media/26u4hHj87jMePiO3u/giphy.gif?cid=7941fdc6hxgjnub1rcs80udcj652956fwmm4qhxsmk6ldxg7&ep=v1_gifs_search&rid=giphy.gif&ct=g](https://media3.giphy.com/media/26u4hHj87jMePiO3u/giphy.gif?cid=7941fdc6hxgjnub1rcs80udcj652956fwmm4qhxsmk6ldxg7&ep=v1_gifs_search&rid=giphy.gif&ct=g)
|
||||
|
||||
So remember, when your app and server have a lot to talk about, go for WebSockets and let the conversation flow freely!
|
||||
|
||||
## How WebSockets Work
|
||||
|
||||
If you want real-time capabilities in your app, you don’t always need WebSockets. You can implement similar functionality by using resource-heavy processes, such as:
|
||||
|
||||
1. long-polling, e.g. running `setInterval` to periodically hit the server and check for updates.
|
||||
2. one-way “server-sent events”, e.g. keeping a unidirectional server-to-client connection open to receive new updates from the server only.
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled 1.png"
|
||||
width="550px"
|
||||
caption="1. HTTP handshake, 2. two-way instant communication, 3. close connection"
|
||||
/>
|
||||
|
||||
WebSockets, on the other hand, provide a two-way (aka “full-duplex”) communication channel between the client and server.
|
||||
|
||||
Once established via an HTTP “handshake”, the server and client can freely exchange information instantly before the connection is finally closed by either side.
|
||||
|
||||
Although introducing WebSockets does add complexity due to asynchronous and event-driven components, choosing the right libraries and frameworks can make it easy.
|
||||
|
||||
In the sections below, we will show you two ways to implement WebSockets into a React-NodeJS app:
|
||||
|
||||
1. Configuring it yourself alongside your own standalone Node/ExpressJS server
|
||||
2. Letting Wasp, a full-stack framework with superpowers, easily configure it for you
|
||||
|
||||
## Adding WebSockets Support in a React-NodeJS App
|
||||
|
||||
### What You Shouldn’t Use: Serverless Architecture
|
||||
|
||||
But first, here’s a “heads up” for you: despite being a great solution for certain use-cases, serverless solutions are **not** the right tool for this job.
|
||||
|
||||
That means, popular frameworks and infrastructure, like NextJS and AWS Lambda, do not support WebSockets integration out-of-the-box.
|
||||
|
||||
<!-- [https://www.youtube.com/watch?v=e5Cye4pIFeA](https://www.youtube.com/watch?v=e5Cye4pIFeA) -->
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/e5Cye4pIFeA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
Instead of running on a dedicated, traditional server, such solutions utilize serverless functions (also known as lambda functions), which are designed to execute and complete a task as soon as a request comes in. It’s as if they “turn on” when the request comes in, and then “turn off” once it’s completed.
|
||||
|
||||
This serverless architecture is not ideal for keeping a WebSocket connection alive because we want a persistent, “always-on” connection.
|
||||
|
||||
That’s why you need a “serverful” architecture if you want to build real-time apps. And although there is a workaround to getting WebSockets on a serverless architecture, [like using third-party services](https://vercel.com/guides/do-vercel-serverless-functions-support-websocket-connections), this has a number of drawbacks:
|
||||
|
||||
- **Cost:** these services exist as subscriptions and can get costly as your app scales
|
||||
- **Limited Customization:** you’re using a pre-built solution, so you have less control
|
||||
- **Debugging:** fixing errors gets more difficult, as your app is not running locally
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled 2.png"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
### Using ExpressJS with Socket.IO — Complex/Customizable Method
|
||||
|
||||
Okay, let's start with the first, more traditional approach: creating a dedicated server for your client to establish a two-way communication channel with.
|
||||
|
||||
:::note
|
||||
👨💻 If you want to code along you can follow the instructions below. Alternatively, if you just want to see the finished React-NodeJS full-stack app, check out the [github repo here](https://github.com/vincanger/websockets-react)
|
||||
:::
|
||||
|
||||
In this exampple, we’ll be using [ExpressJS](https://expressjs.com/) with the [Socket.IO](http://Socket.io) library. Although there are others out there, Socket.IO is a great library that makes working with WebSockets in NodeJS [easier](https://socket.io/docs/v4/).
|
||||
|
||||
If you want to code along, first clone the `start` branch:
|
||||
|
||||
```bash
|
||||
git clone --branch start https://github.com/vincanger/websockets-react.git
|
||||
```
|
||||
|
||||
You’ll notice that inside we have two folders:
|
||||
|
||||
- 📁 `ws-client` for our React app
|
||||
- 📁 `ws-server` for our ExpressJS/NodeJS server
|
||||
|
||||
Let’s `cd` into the server folder and install the dependencies:
|
||||
|
||||
```bash
|
||||
cd ws-server && npm install
|
||||
```
|
||||
|
||||
We also need to install the types for working with typescript:
|
||||
|
||||
```bash
|
||||
npm i --save-dev @types/cors
|
||||
```
|
||||
|
||||
Now run the server, using the `npm start` command in your terminal.
|
||||
|
||||
You should see `listening on *:8000` printed to the console!
|
||||
|
||||
At the moment, this is what our `index.ts` file looks like:
|
||||
|
||||
```tsx
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
|
||||
const app = express();
|
||||
app.use(cors({ origin: '*' }));
|
||||
const server = require('http').createServer(app);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send(`<h1>Hello World</h1>`);
|
||||
});
|
||||
|
||||
server.listen(8000, () => {
|
||||
console.log('listening on *:8000');
|
||||
});
|
||||
```
|
||||
|
||||
There’s not much going on here, so let’s install the [Socket.IO](http://Socket.IO) package and start adding WebSockets to our server!
|
||||
|
||||
First, let’s kill the server with `ctrl + c` and then run:
|
||||
|
||||
```bash
|
||||
npm install socket.io
|
||||
```
|
||||
|
||||
Let’s go ahead and replace the `index.ts` file with the following code. I know it’s a lot of code, so I’ve left a bunch of comments that explain what’s going on ;):
|
||||
|
||||
```tsx
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
|
||||
type PollState = {
|
||||
question: string;
|
||||
options: {
|
||||
id: number;
|
||||
text: string;
|
||||
description: string;
|
||||
votes: string[];
|
||||
}[];
|
||||
};
|
||||
interface ClientToServerEvents {
|
||||
vote: (optionId: number) => void;
|
||||
askForStateUpdate: () => void;
|
||||
}
|
||||
interface ServerToClientEvents {
|
||||
updateState: (state: PollState) => void;
|
||||
}
|
||||
interface InterServerEvents { }
|
||||
interface SocketData {
|
||||
user: string;
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.use(cors({ origin: 'http://localhost:5173' })); // this is the default port that Vite runs your React app on
|
||||
const server = require('http').createServer(app);
|
||||
// passing these generic type parameters to the `Server` class
|
||||
// ensures data flowing through the server are correctly typed.
|
||||
const io = new Server<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents,
|
||||
SocketData
|
||||
>(server, {
|
||||
cors: {
|
||||
origin: 'http://localhost:5173',
|
||||
methods: ['GET', 'POST'],
|
||||
},
|
||||
});
|
||||
|
||||
// this is middleware that Socket.IO uses on initiliazation to add
|
||||
// the authenticated user to the socket instance. Note: we are not
|
||||
// actually adding real auth as this is beyond the scope of the tutorial
|
||||
io.use(addUserToSocketDataIfAuthenticated);
|
||||
|
||||
// the client will pass an auth "token" (in this simple case, just the username)
|
||||
// to the server on initialize of the Socket.IO client in our React App
|
||||
async function addUserToSocketDataIfAuthenticated(socket: Socket, next: (err?: Error) => void) {
|
||||
const user = socket.handshake.auth.token;
|
||||
if (user) {
|
||||
try {
|
||||
socket.data = { ...socket.data, user: user };
|
||||
} catch (err) {}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// the server determines the PollState object, i.e. what users will vote on
|
||||
// this will be sent to the client and displayed on the front-end
|
||||
const poll: PollState = {
|
||||
question: "What are eating for lunch ✨ Let's order",
|
||||
options: [
|
||||
{
|
||||
id: 1,
|
||||
text: 'Party Pizza Place',
|
||||
description: 'Best pizza in town',
|
||||
votes: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: 'Best Burger Joint',
|
||||
description: 'Best burger in town',
|
||||
votes: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
text: 'Sus Sushi Place',
|
||||
description: 'Best sushi in town',
|
||||
votes: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('a user connected', socket.data.user);
|
||||
|
||||
// the client will send an 'askForStateUpdate' request on mount
|
||||
// to get the initial state of the poll
|
||||
socket.on('askForStateUpdate', () => {
|
||||
console.log('client asked For State Update');
|
||||
socket.emit('updateState', poll);
|
||||
});
|
||||
|
||||
socket.on('vote', (optionId: number) => {
|
||||
// If user has already voted, remove their vote.
|
||||
poll.options.forEach((option) => {
|
||||
option.votes = option.votes.filter((user) => user !== socket.data.user);
|
||||
});
|
||||
// And then add their vote to the new option.
|
||||
const option = poll.options.find((o) => o.id === optionId);
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
option.votes.push(socket.data.user);
|
||||
// Send the updated PollState back to all clients
|
||||
io.emit('updateState', poll);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('user disconnected');
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(8000, () => {
|
||||
console.log('listening on *:8000');
|
||||
});
|
||||
```
|
||||
|
||||
Great, start the server again with `npm start` and let’s add the [Socket.IO](http://Socket.IO) client to the front-end.
|
||||
|
||||
`cd` into the `ws-client` directory and run
|
||||
|
||||
```bash
|
||||
cd ../ws-client && npm install
|
||||
```
|
||||
|
||||
Next, start the development server with `npm run dev` and you should see the hardcoded starter app in your browser:
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled 3.png"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
You may have noticed that poll does not match the `PollState` from our server. We need to install the [Socket.IO](http://Socket.IO) client and set it all up in order start our real-time communication and get the correct poll from the server.
|
||||
|
||||
Go ahead and kill the development server with `ctrl + c` and run:
|
||||
|
||||
```bash
|
||||
npm install socket.io-client
|
||||
```
|
||||
|
||||
Now let’s create a hook that initializes and returns our WebSocket client after it establishes a connection. To do that, create a new file in `./ws-client/src` called `useSocket.ts`:
|
||||
|
||||
```tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import socketIOClient, { Socket } from 'socket.io-client';
|
||||
|
||||
export type PollState = {
|
||||
question: string;
|
||||
options: {
|
||||
id: number;
|
||||
text: string;
|
||||
description: string;
|
||||
votes: string[];
|
||||
}[];
|
||||
};
|
||||
interface ServerToClientEvents {
|
||||
updateState: (state: PollState) => void;
|
||||
}
|
||||
interface ClientToServerEvents {
|
||||
vote: (optionId: number) => void;
|
||||
askForStateUpdate: () => void;
|
||||
}
|
||||
|
||||
export function useSocket({endpoint, token } : { endpoint: string, token: string }) {
|
||||
// initialize the client using the server endpoint, e.g. localhost:8000
|
||||
// and set the auth "token" (in our case we're simply passing the username
|
||||
// for simplicity -- you would not do this in production!)
|
||||
// also make sure to use the Socket generic types in the reverse order of the server!
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = socketIOClient(endpoint, {
|
||||
auth: {
|
||||
token: token
|
||||
}
|
||||
})
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('useSocket useEffect', endpoint, socket)
|
||||
|
||||
function onConnect() {
|
||||
setIsConnected(true)
|
||||
}
|
||||
|
||||
function onDisconnect() {
|
||||
setIsConnected(false)
|
||||
}
|
||||
|
||||
socket.on('connect', onConnect)
|
||||
socket.on('disconnect', onDisconnect)
|
||||
|
||||
return () => {
|
||||
socket.off('connect', onConnect)
|
||||
socket.off('disconnect', onDisconnect)
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
// we return the socket client instance and the connection state
|
||||
return {
|
||||
isConnected,
|
||||
socket,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Now let’s go back to our main `App.tsx` page and replace it with the following code (again I’ve left comments to explain):
|
||||
|
||||
```tsx
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { Layout } from './Layout';
|
||||
import { Button, Card } from 'flowbite-react';
|
||||
import { useSocket } from './useSocket';
|
||||
import type { PollState } from './useSocket';
|
||||
|
||||
const App = () => {
|
||||
// set the PollState after receiving it from the server
|
||||
const [poll, setPoll] = useState<PollState | null>(null);
|
||||
|
||||
// since we're not implementing Auth, let's fake it by
|
||||
// creating some random user names when the App mounts
|
||||
const randomUser = useMemo(() => {
|
||||
const randomName = Math.random().toString(36).substring(7);
|
||||
return `User-${randomName}`;
|
||||
}, []);
|
||||
|
||||
// 🔌⚡️ get the connected socket client from our useSocket hook!
|
||||
const { socket, isConnected } = useSocket({ endpoint: `http://localhost:8000`, token: randomUser });
|
||||
|
||||
const totalVotes = useMemo(() => {
|
||||
return poll?.options.reduce((acc, option) => acc + option.votes.length, 0) ?? 0;
|
||||
}, [poll]);
|
||||
|
||||
// every time we receive an 'updateState' event from the server
|
||||
// e.g. when a user makes a new vote, we set the React's state
|
||||
// with the results of the new PollState
|
||||
socket.on('updateState', (newState: PollState) => {
|
||||
setPoll(newState);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
socket.emit('askForStateUpdate');
|
||||
}, []);
|
||||
|
||||
function handleVote(optionId: number) {
|
||||
socket.emit('vote', optionId);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout user={randomUser}>
|
||||
<div className='w-full max-w-2xl mx-auto p-8'>
|
||||
<h1 className='text-2xl font-bold'>{poll?.question ?? 'Loading...'}</h1>
|
||||
<h2 className='text-lg italic'>{isConnected ? 'Connected ✅' : 'Disconnected 🛑'}</h2>
|
||||
{poll && <p className='leading-relaxed text-gray-500'>Cast your vote for one of the options.</p>}
|
||||
{poll && (
|
||||
<div className='mt-4 flex flex-col gap-4'>
|
||||
{poll.options.map((option) => (
|
||||
<Card key={option.id} className='relative transition-all duration-300 min-h-[130px]'>
|
||||
<div className='z-10'>
|
||||
<div className='mb-2'>
|
||||
<h2 className='text-xl font-semibold'>{option.text}</h2>
|
||||
<p className='text-gray-700'>{option.description}</p>
|
||||
</div>
|
||||
<div className='absolute bottom-5 right-5'>
|
||||
{randomUser && !option.votes.includes(randomUser) ? (
|
||||
<Button onClick={() => handleVote(option.id)}>Vote</Button>
|
||||
) : (
|
||||
<Button disabled>Voted</Button>
|
||||
)}
|
||||
</div>
|
||||
{option.votes.length > 0 && (
|
||||
<div className='mt-2 flex gap-2 flex-wrap max-w-[75%]'>
|
||||
{option.votes.map((vote) => (
|
||||
<div
|
||||
key={vote}
|
||||
className='py-1 px-3 bg-gray-100 rounded-lg flex items-center justify-center shadow text-sm'
|
||||
>
|
||||
<div className='w-2 h-2 bg-green-500 rounded-full mr-2'></div>
|
||||
<div className='text-gray-700'>{vote}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='absolute top-5 right-5 p-2 text-sm font-semibold bg-gray-100 rounded-lg z-10'>
|
||||
{option.votes.length} / {totalVotes}
|
||||
</div>
|
||||
<div
|
||||
className='absolute inset-0 bg-gradient-to-r from-yellow-400 to-orange-500 opacity-75 rounded-lg transition-all duration-300'
|
||||
style={{
|
||||
width: `${totalVotes > 0 ? (option.votes.length / totalVotes) * 100 : 0}%`,
|
||||
}}
|
||||
></div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
export default App;
|
||||
```
|
||||
|
||||
Go ahead now and start the client with `npm run dev`. Open another terminal window/tab, `cd` into the `ws-server` directory and run `npm start`.
|
||||
|
||||
If we did that correctly, we should be seeing our finished, working, REAL TIME app! 🙂
|
||||
|
||||
It looks and works great if you open it up in two or three browser tabs. Check it out:
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled.gif"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
Nice!
|
||||
|
||||
So we’ve got the core functionality here, but as this is just a demo, there are a couple very important pieces missing that make this app unusable in production.
|
||||
|
||||
Mainly, we’re creating a random fake user each time the app mounts. You can check this by refreshing the page and voting again. You’ll see the votes just add up, as we’re creating a new random user each time. We don’t want that!
|
||||
|
||||
We should instead be authenticating and persisting a session for a user that’s registered in our database. But another problem: we don’t even have a database at all in this app!
|
||||
|
||||
You can start to see the how the complexity add ups for even just a simple voting feature
|
||||
|
||||
Luckily, our next solution, Wasp, has integrated Authentication and Database Management. Not to mention, it also takes care of a lot of the WebSockets configuration for us.
|
||||
|
||||
So let’s go ahead and give that a go!
|
||||
|
||||
### Implementing WebSockets with Wasp — Fast/Zero Config Method
|
||||
|
||||
Because Wasp is an innovative full-stack framework, it makes building React-NodeJS apps quick and developer-friendly.
|
||||
|
||||
Wasp has lots of time-saving features, including WebSocket support via [Socket.IO](http://socket.io/), Authentication, Database Management, and Full-stack type-safety out-of-the box.
|
||||
|
||||
<!-- [https://twitter.com/WaspLang/status/1673742264873500673?s=20](https://twitter.com/WaspLang/status/1673742264873500673?s=20) -->
|
||||
|
||||
Wasp can take care of all this heavy lifting for you because of its use of a config file, which you can think of like a set of instructions that the Wasp compiler uses to help glue your app together.
|
||||
|
||||
To see it in action, let's implement WebSocket communication using Wasp by following these steps
|
||||
|
||||
:::tip
|
||||
If you just want to see finished app’s code, you can check out the [GitHub repo here](https://github.com/vincanger/websockets-wasp)
|
||||
:::
|
||||
|
||||
1. Install Wasp globally by running the following command in your terminal:
|
||||
|
||||
```bash
|
||||
curl -sSL [https://get.wasp-lang.dev/installer.sh](https://get.wasp-lang.dev/installer.sh) | sh
|
||||
```
|
||||
|
||||
If you want to code along, first clone the `start` branch of the example app:
|
||||
|
||||
```bash
|
||||
git clone --branch start https://github.com/vincanger/websockets-wasp.git
|
||||
```
|
||||
|
||||
You’ll notice that the structure of the Wasp app is split:
|
||||
|
||||
- 🐝 a `main.wasp` config file exists at the root
|
||||
- 📁 `src/client` is our directory for our React files
|
||||
- 📁 `src/server` is our directory for our ExpressJS/NodeJS functions
|
||||
|
||||
Let’s start out by taking a quick look at our `main.wasp` file.
|
||||
|
||||
```jsx
|
||||
app whereDoWeEat {
|
||||
wasp: {
|
||||
version: "^0.11.0"
|
||||
},
|
||||
title: "where-do-we-eat",
|
||||
client: {
|
||||
rootComponent: import { Layout } from "@client/Layout.jsx",
|
||||
},
|
||||
// 🔐 this is how we get auth in our app.
|
||||
auth: {
|
||||
userEntity: User,
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
methods: {
|
||||
usernameAndPassword: {}
|
||||
}
|
||||
},
|
||||
dependencies: [
|
||||
("flowbite", "1.6.6"),
|
||||
("flowbite-react", "0.4.9")
|
||||
]
|
||||
}
|
||||
|
||||
// 👱 this is the data model for our registered users in our database
|
||||
entity User {=psl
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
psl=}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
With this, the Wasp compiler will know what to do and will configure these features for us.
|
||||
|
||||
Let’s tell it we want WebSockets, as well. Add the `webSocket` definition to the `main.wasp` file, just between `auth` and `dependencies`:
|
||||
|
||||
```jsx
|
||||
app whereDoWeEat {
|
||||
// ...
|
||||
webSocket: {
|
||||
fn: import { webSocketFn } from "@server/ws-server.js",
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Now we have to define the `webSocketFn`. In the `./src/server` directory create a new file, `ws-server.ts` and copy the following code:
|
||||
|
||||
```tsx
|
||||
import { WebSocketDefinition } from '@wasp/webSocket';
|
||||
import { User } from '@wasp/entities';
|
||||
|
||||
// define the types. this time we will get the entire User object
|
||||
// in SocketData from the Auth that Wasp automatically sets up for us 🎉
|
||||
type PollState = {
|
||||
question: string;
|
||||
options: {
|
||||
id: number;
|
||||
text: string;
|
||||
description: string;
|
||||
votes: string[];
|
||||
}[];
|
||||
};
|
||||
interface ServerToClientEvents {
|
||||
updateState: (state: PollState) => void;
|
||||
}
|
||||
interface ClientToServerEvents {
|
||||
vote: (optionId: number) => void;
|
||||
askForStateUpdate: () => void;
|
||||
}
|
||||
interface InterServerEvents {}
|
||||
interface SocketData {
|
||||
user: User;
|
||||
}
|
||||
|
||||
// pass the generic types to the websocketDefinition just like
|
||||
// in the previous example
|
||||
export const webSocketFn: WebSocketDefinition<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents,
|
||||
SocketData
|
||||
> = (io, _context) => {
|
||||
const poll: PollState = {
|
||||
question: "What are eating for lunch ✨ Let's order",
|
||||
options: [
|
||||
{
|
||||
id: 1,
|
||||
text: 'Party Pizza Place',
|
||||
description: 'Best pizza in town',
|
||||
votes: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: 'Best Burger Joint',
|
||||
description: 'Best burger in town',
|
||||
votes: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
text: 'Sus Sushi Place',
|
||||
description: 'Best sushi in town',
|
||||
votes: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
io.on('connection', (socket) => {
|
||||
if (!socket.data.user) {
|
||||
console.log('Socket connected without user');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Socket connected: ', socket.data.user?.username);
|
||||
socket.on('askForStateUpdate', () => {
|
||||
socket.emit('updateState', poll);
|
||||
});
|
||||
|
||||
socket.on('vote', (optionId) => {
|
||||
// If user has already voted, remove their vote.
|
||||
poll.options.forEach((option) => {
|
||||
option.votes = option.votes.filter((username) => username !== socket.data.user.username);
|
||||
});
|
||||
// And then add their vote to the new option.
|
||||
const option = poll.options.find((o) => o.id === optionId);
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
option.votes.push(socket.data.user.username);
|
||||
io.emit('updateState', poll);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Socket disconnected: ', socket.data.user?.username);
|
||||
});
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
You may have noticed that there’s a lot less configuration and boilerplate needed here in the Wasp implementation. That’s because the:
|
||||
|
||||
- endpoints,
|
||||
- authentication,
|
||||
- and Express and [Socket.IO](http://Socket.IO) middleware
|
||||
|
||||
are all being handled for you by Wasp. Noice!
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled 4.png"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
Let’s go ahead now and run the app to see what we have at this point.
|
||||
|
||||
First, we need to initialize the database so that our Auth works correctly. This is something we didn’t do in the previous example due to high complexity, but is easy to do with Wasp:
|
||||
|
||||
```bash
|
||||
wasp db migrate-dev
|
||||
```
|
||||
|
||||
Once that’s finished, run the app (it my take a while on first run to install all depenedencies):
|
||||
|
||||
```bash
|
||||
wasp start
|
||||
```
|
||||
|
||||
You should see a login screen this time. Go ahead and first register a user, then login:
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled 5.png"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
Once logged in, you’ll see the same hardcoded poll data as in the previous example, because, again, we haven’t set up the [Socket.IO](http://Socket.IO) client on the frontend. But this time it should be much easier.
|
||||
|
||||
Why? Well, besides less configuration, another nice benefit of working with [TypeScript with Wasp](https://wasp-lang.dev/docs/typescript#websocket-full-stack-type-support), is that you just have to define payload types with matching event names on the server, and those types will get exposed automatically on the client!
|
||||
|
||||
Let’s take a look at how that works now.
|
||||
|
||||
In `.src/client/MainPage.tsx`, replace the contents with the following code:
|
||||
|
||||
```tsx
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { Button, Card } from "flowbite-react";
|
||||
// Wasp provides us with pre-configured hooks and types based on
|
||||
// our server code. No need to set it up ourselves!
|
||||
import {
|
||||
useSocketListener,
|
||||
useSocket,
|
||||
ServerToClientPayload,
|
||||
} from "@wasp/webSocket";
|
||||
import useAuth from "@wasp/auth/useAuth";
|
||||
|
||||
const MainPage = () => {
|
||||
// we can easily access the logged in user with this hook
|
||||
// that wasp provides for us
|
||||
const { data: user } = useAuth();
|
||||
const [poll, setPoll] = useState<ServerToClientPayload<"updateState"> | null>(
|
||||
null
|
||||
);
|
||||
const totalVotes = useMemo(() => {
|
||||
return (
|
||||
poll?.options.reduce((acc, option) => acc + option.votes.length, 0) ?? 0
|
||||
);
|
||||
}, [poll]);
|
||||
|
||||
// pre-built hooks, configured for us by Wasp
|
||||
const { socket } = useSocket();
|
||||
useSocketListener("updateState", (newState) => {
|
||||
setPoll(newState);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
socket.emit("askForStateUpdate");
|
||||
}, []);
|
||||
|
||||
function handleVote(optionId: number) {
|
||||
socket.emit("vote", optionId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-2xl mx-auto p-8">
|
||||
<h1 className="text-2xl font-bold">{poll?.question ?? "Loading..."}</h1>
|
||||
{poll && (
|
||||
<p className="leading-relaxed text-gray-500">
|
||||
Cast your vote for one of the options.
|
||||
</p>
|
||||
)}
|
||||
{poll && (
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
{poll.options.map((option) => (
|
||||
<Card key={option.id} className="relative transition-all duration-300 min-h-[130px]">
|
||||
<div className="z-10">
|
||||
<div className="mb-2">
|
||||
<h2 className="text-xl font-semibold">{option.text}</h2>
|
||||
<p className="text-gray-700">{option.description}</p>
|
||||
</div>
|
||||
<div className="absolute bottom-5 right-5">
|
||||
{user && !option.votes.includes(user.username) ? (
|
||||
<Button onClick={() => handleVote(option.id)}>Vote</Button>
|
||||
) : (
|
||||
<Button disabled>Voted</Button>
|
||||
)}
|
||||
{!user}
|
||||
</div>
|
||||
{option.votes.length > 0 && (
|
||||
<div className="mt-2 flex gap-2 flex-wrap max-w-[75%]">
|
||||
{option.votes.map((vote) => (
|
||||
<div
|
||||
key={vote}
|
||||
className="py-1 px-3 bg-gray-100 rounded-lg flex items-center justify-center shadow text-sm"
|
||||
>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
|
||||
<div className="text-gray-700">{vote}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute top-5 right-5 p-2 text-sm font-semibold bg-gray-100 rounded-lg z-10">
|
||||
{option.votes.length} / {totalVotes}
|
||||
</div>
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-yellow-400 to-orange-500 opacity-75 rounded-lg transition-all duration-300"
|
||||
style={{
|
||||
width: `${
|
||||
totalVotes > 0
|
||||
? (option.votes.length / totalVotes) * 100
|
||||
: 0
|
||||
}%`,
|
||||
}}
|
||||
></div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default MainPage;
|
||||
```
|
||||
|
||||
In comparison to the previous implementation, Wasp saved us from having to configure the [Socket.IO](http://Socket.IO) client, as well as building our own hooks.
|
||||
|
||||
Also, hover over the variables in your client-side code, and you’ll see that the types are being automatically inferred for you!
|
||||
|
||||
Here’s just one example, but it should work for them all:
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled 6.png"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
Now if you open up a new private/incognito tab, register a new user, and login, you’ll see a fully working, real-time voting app. The best part is, in comparison to the previous approach, we can log out and back in, and our voting data persists, which is exactly what we’d expect from a production grade app. 🎩
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled 1.gif"
|
||||
width="550px"
|
||||
/>
|
||||
|
||||
Awesome… 😏
|
||||
|
||||
## Comparing the Two Approaches
|
||||
|
||||
Now, just because one approach seems easier, doesn’t always mean it’s always better. Let’s give a quick run-down of the advantages and disadvantages of both the implementations above.
|
||||
|
||||
| | Without Wasp | With Wasp |
|
||||
| --- | --- | --- |
|
||||
| 😎 Intended User | Senior Developers, web development teams | Full-stack developers, “Indiehackers”, junior devs |
|
||||
| 📈 Complexity of Code | Medium-to-High | Low |
|
||||
| 🚤 Speed | Slower, more methodical | Faster, more integrated |
|
||||
| 🧑💻 Libraries | Any | Socket.IO |
|
||||
| ⛑ Type safety | Implement on both server and client | Implement once on server, inferred by Wasp on client |
|
||||
| 🎮 Amount of control | High, as you determine the implementation | Opinionated, as Wasp decides the basic implementation |
|
||||
| 🐛 Learning Curve | Complex: full knowledge of front and backend technologies, including WebSockets | Intermediate: Knowledge of full-stack fundamentals necessary. |
|
||||
|
||||
### Implementing WebSockets Using React, Express.js (Without Wasp)
|
||||
|
||||
Advantages:
|
||||
|
||||
1. Control & **Flexibility**: You can approach the implementation of WebSockets in the way that best suits your project's needs, as well as your choice between a [number of different WebSocket libraries](https://www.atatus.com/blog/websocket-libraries-for-nodejs/), not just Socket.IO.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
1. **More Code & Complexity**: Without the abstractions provided by a framework like Wasp, you might need to write more code and create your own abstractions to handle common tasks. Not to mention the proper configuration of a NodeJS/ExpressJS server (the one provided in the example is very basic)
|
||||
2. Manual **Type Safety: If you’re working with TypeScript, you have to be more careful typing your event handlers and payload types coming into and going out from the server, or implement a more type-safe approach yourself.**
|
||||
|
||||
### Implementing WebSockets with Wasp (uses React, ExpressJS, and [Socket.IO](http://Socket.IO) under the hood)
|
||||
|
||||
Advantages:
|
||||
|
||||
1. Fully-Integrated**/Less code**: Wasp provides useful abstractions such as `useSocket` and `useSocketListener` hooks for use in React components (on top of other features like Auth, Async Jobs, Email-sending, DB management, and Deployment), simplifying the client-side code, and allowing for full integration with less configuration.
|
||||
2. **Type Safety**: Wasp facilitates full-stack type safety for WebSocket events and payloads. This reduces the likelihood of runtime errors due to mismatched data types and saves you from writing even more boilerplate.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
1. **Learning curve**: Developers unfamiliar with Wasp will need to learn the framework to effectively use it.
|
||||
2. **Less control**: While Wasp provides a lot of conveniences, it abstracts away some of the details, giving developers slightly less control over certain aspects of socket management.
|
||||
|
||||
## Conclusion
|
||||
|
||||
In general, how you add WebSockets to your React app depends on the specifics of your project, your comfort level with the available tools, and the trade-offs you're willing to make between ease of use, control, and complexity.
|
||||
|
||||
Don’t forget, if you want to check out the full finished code from our “Lunch Voting” example full-stack app, go here: [https://github.com/vincanger/websockets-wasp](https://github.com/vincanger/websockets-wasp)
|
||||
|
||||
And if you know of a better, cooler, sleeker way of implementing WebSockets into your apps, let us know in the comments below
|
||||
|
||||
<ImgWithCaption
|
||||
source="img/websockets-app/Untitled 7.png"
|
||||
width="550px"
|
||||
/>
|
@ -10,9 +10,7 @@ Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly
|
||||
If you encounter any issues, reach out to us on [Discord](https://discord.gg/rzdnErX) and we will make sure to help you out!
|
||||
:::
|
||||
|
||||
# Automated
|
||||
|
||||
## Wasp CLI
|
||||
## Deploying with the Wasp CLI
|
||||
|
||||
Using the Wasp CLI, you can easily deploy a new app to [Fly.io](https://fly.io) with just a single command:
|
||||
```shell
|
||||
@ -44,7 +42,46 @@ To do so, go to your [account's billing page](https://fly.io/dashboard/personal/
|
||||
|
||||
The list of available Fly regions can be found [here](https://fly.io/docs/reference/regions/). You can also run `wasp deploy fly cmd platform regions --context server`.
|
||||
|
||||
### Commands
|
||||
### Using a custom domain for your app
|
||||
|
||||
Setting up a custom domain for your app is easy. First, you need to add your domain to Fly. You can do this by running:
|
||||
```shell
|
||||
wasp deploy fly cmd --context client certs create mycoolapp.com
|
||||
```
|
||||
|
||||
:::info Use Your Own Domain
|
||||
Make sure to replace `mycoolapp.com` with your domain in all of the commands mentioned in this section 🙃
|
||||
:::
|
||||
|
||||
This command will output the instructions to add the DNS records to your domain. It will look something like this:
|
||||
```shell-session
|
||||
You can direct traffic to mycoolapp.com by:
|
||||
|
||||
1: Adding an A record to your DNS service which reads
|
||||
|
||||
A @ 66.241.1XX.154
|
||||
|
||||
You can validate your ownership of mycoolapp.com by:
|
||||
|
||||
2: Adding an AAAA record to your DNS service which reads:
|
||||
|
||||
AAAA @ 2a09:82XX:1::1:ff40
|
||||
```
|
||||
|
||||
Next, you need to add the DNS records for your domain. This will depend on your domain provider, but it should be a matter of adding an A record for `@` and an AAAA record for `@` with the values provided by the previous command.
|
||||
|
||||
Finally, you need to set your domain as the `WASP_WEB_CLIENT_URL` environment variable for your server app. We need to do this to keep our CORS configuration up to date.
|
||||
|
||||
You can do this by running:
|
||||
```shell
|
||||
wasp deploy fly cmd --context server secrets set WASP_WEB_CLIENT_URL=https://mycoolapp.com
|
||||
```
|
||||
|
||||
That's it, your app should be available at `https://mycoolapp.com`! 🎉
|
||||
|
||||
### Deployment commands
|
||||
|
||||
#### Apps and the database
|
||||
|
||||
`setup` will create your client and server apps on Fly, and add some secrets, but does _not_ deploy them. We need a database first, which we create with `create-db`, and it is automatically linked to your server.
|
||||
|
||||
@ -60,6 +97,8 @@ Finally, we `deploy` which will push your client and server live. We run this si
|
||||
Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducability, the CLI defaults to the use of a remote Fly.io builder. If you wish to build locally, you may supply the `--build-locally` option to `wasp deploy fly launch` or `wasp deploy fly deploy`.
|
||||
:::
|
||||
|
||||
#### Setting secrets
|
||||
|
||||
If you would like to run arbitrary Fly commands (eg, `flyctl secrets list` for your server app), you can run them like so:
|
||||
```shell
|
||||
wasp deploy fly cmd secrets list --context server
|
||||
@ -79,11 +118,11 @@ wasp deploy fly cmd secrets set GOOGLE_CLIENT_ID=<...> GOOGLE_CLIENT_SECRET=<...
|
||||
```
|
||||
:::
|
||||
|
||||
:::note
|
||||
If you have multiple orgs, you can specify a `--org` option. For example: `wasp deploy fly launch my-wasp-app mia --org hive`
|
||||
:::
|
||||
#### Multiple orgs
|
||||
|
||||
# Manual
|
||||
If you have multiple orgs, you can specify a `--org` option. For example: `wasp deploy fly launch my-wasp-app mia --org hive`
|
||||
|
||||
## Manual Deployment
|
||||
|
||||
In addition to the CLI, you can deploy a Wasp project by generating the code and then deploying generated code "manually", as explained below.
|
||||
|
||||
@ -93,18 +132,23 @@ If you want to deploy your App completely **free** of charge, continue reading b
|
||||
|
||||
If you prefer to host client and server on **one platform**, and don't mind paying a very small fee for extra features, we suggest following the guide on using [Railway as your provider](#deploying-to-railway-freemium-all-in-one-solution).
|
||||
|
||||
## Generating deployable code
|
||||
|
||||
### Generating deployable code
|
||||
The following command generates deployable code for the whole app in the `.wasp/build/` directory:
|
||||
```
|
||||
wasp build
|
||||
```
|
||||
|
||||
generates deployable code for the whole app in the `.wasp/build/` directory. Next, we will deploy this code.
|
||||
|
||||
NOTE: You will not be able to build the app if you are using SQLite as a database (which is a default database) -> you will have to [switch to PostgreSQL](/docs/language/features#migrating-from-sqlite-to-postgresql).
|
||||
:::note Using SQLite in production
|
||||
You will not be able to build the app if you are using SQLite as a database (which is a default database) -> you will have to [switch to PostgreSQL](/docs/language/features#migrating-from-sqlite-to-postgresql).
|
||||
:::
|
||||
|
||||
## Deploying API server (backend)
|
||||
|
||||
:::note
|
||||
Since Wasp's API server is a Node.js app, it can be deployed anywhere where Node.js apps can be deployed. Below, we provide instructions for deploying to a few hosting providers.
|
||||
:::
|
||||
|
||||
|
||||
In `.wasp/build/`, there is a `Dockerfile` describing an image for building the server.
|
||||
|
||||
To run server in production, deploy this docker image to your favorite hosting provider, ensure that env vars are correctly set, and that is it.
|
||||
|
@ -67,11 +67,11 @@ Let's now define the Action's JavaScript implementation in `src/server/actions.t
|
||||
```js title="src/server/actions.js"
|
||||
// ...
|
||||
|
||||
export const updateTask = async ({ id, isDone }, context) => {
|
||||
export const updateTask = async (args, context) => {
|
||||
return context.entities.Task.update({
|
||||
where: { id },
|
||||
where: { args.id },
|
||||
data: {
|
||||
isDone: isDone,
|
||||
isDone: args.isDone,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -89,13 +89,13 @@ import { CreateTask, UpdateTask } from "@wasp/actions/types"
|
||||
type UpdateTaskPayload = Pick<Task, "id" | "isDone">
|
||||
|
||||
export const updateTask: UpdateTask<UpdateTaskPayload, Task> = async (
|
||||
{ id, isDone },
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
return context.entities.Task.update({
|
||||
where: { id },
|
||||
where: { args.id },
|
||||
data: {
|
||||
isDone: isDone,
|
||||
isDone: args.isDone,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -349,8 +349,8 @@ export const updateTask = async (args, context) => {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.updateMany({
|
||||
where: { id: args.taskId, user: { id: context.user.id } },
|
||||
data: { isDone: args.data.isDone }
|
||||
where: { id: args.id, user: { id: context.user.id } },
|
||||
data: { isDone: args.isDone }
|
||||
})
|
||||
}
|
||||
```
|
||||
@ -382,15 +382,15 @@ export const createTask: CreateTask<CreateTaskPayload, Task> = async (
|
||||
type UpdateTaskPayload = Pick<Task, "id" | "isDone">
|
||||
|
||||
export const updateTask: UpdateTask<UpdateTaskPayload, { count: number }> = async (
|
||||
{ id, isDone },
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401)
|
||||
}
|
||||
return context.entities.Task.updateMany({
|
||||
where: { id: args.taskId, user: { id: context.user.id } },
|
||||
data: { isDone: args.data.isDone }
|
||||
where: { id: args.id, user: { id: context.user.id } },
|
||||
data: { isDone: args.isDone }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
@ -153,7 +153,7 @@ module.exports = {
|
||||
sidebarPath: require.resolve("./sidebars.js"),
|
||||
sidebarCollapsible: true,
|
||||
// Please change this to your repo.
|
||||
editUrl: "https://github.com/wasp-lang/wasp/edit/main/web",
|
||||
editUrl: "https://github.com/wasp-lang/wasp/edit/release/web",
|
||||
remarkPlugins: [autoImportTabs, fileExtSwitcher],
|
||||
},
|
||||
blog: {
|
||||
@ -162,7 +162,7 @@ module.exports = {
|
||||
blogSidebarCount: "ALL",
|
||||
blogSidebarTitle: "All our posts",
|
||||
postsPerPage: "ALL",
|
||||
editUrl: "https://github.com/wasp-lang/wasp/edit/main/web/blog",
|
||||
editUrl: "https://github.com/wasp-lang/wasp/edit/release/web",
|
||||
},
|
||||
theme: {
|
||||
customCss: [require.resolve("./src/css/custom.css")],
|
||||
|
BIN
web/static/img/smol-ai-vs-wasp-ai/Untitled 1.png
Normal file
After Width: | Height: | Size: 266 KiB |
BIN
web/static/img/smol-ai-vs-wasp-ai/Untitled 2.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
web/static/img/smol-ai-vs-wasp-ai/Untitled 3.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
web/static/img/smol-ai-vs-wasp-ai/Untitled 4.png
Normal file
After Width: | Height: | Size: 749 KiB |
BIN
web/static/img/smol-ai-vs-wasp-ai/Untitled 5.png
Normal file
After Width: | Height: | Size: 253 KiB |
BIN
web/static/img/smol-ai-vs-wasp-ai/Untitled 6.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
web/static/img/smol-ai-vs-wasp-ai/Untitled 7.png
Normal file
After Width: | Height: | Size: 239 KiB |
BIN
web/static/img/smol-ai-vs-wasp-ai/Untitled 8.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
web/static/img/smol-ai-vs-wasp-ai/Untitled.png
Normal file
After Width: | Height: | Size: 2.2 MiB |
BIN
web/static/img/smol-ai-vs-wasp-ai/smol-ai-tweet.png
Normal file
After Width: | Height: | Size: 155 KiB |
BIN
web/static/img/smol-ai-vs-wasp-ai/smol-vs-wasp-banner.png
Normal file
After Width: | Height: | Size: 203 KiB |
BIN
web/static/img/websockets-app/Untitled 1.gif
Normal file
After Width: | Height: | Size: 4.9 MiB |
BIN
web/static/img/websockets-app/Untitled 1.png
Normal file
After Width: | Height: | Size: 209 KiB |
BIN
web/static/img/websockets-app/Untitled 2.png
Normal file
After Width: | Height: | Size: 224 KiB |
BIN
web/static/img/websockets-app/Untitled 3.png
Normal file
After Width: | Height: | Size: 754 KiB |
BIN
web/static/img/websockets-app/Untitled 4.png
Normal file
After Width: | Height: | Size: 2.9 MiB |
BIN
web/static/img/websockets-app/Untitled 5.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
web/static/img/websockets-app/Untitled 6.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
web/static/img/websockets-app/Untitled 7.png
Normal file
After Width: | Height: | Size: 436 KiB |
BIN
web/static/img/websockets-app/Untitled.gif
Normal file
After Width: | Height: | Size: 18 MiB |
BIN
web/static/img/websockets-app/Untitled.png
Normal file
After Width: | Height: | Size: 200 KiB |
BIN
web/static/img/websockets-app/websockets-banner.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
web/static/img/websockets-app/websockets-resized.png
Normal file
After Width: | Height: | Size: 42 KiB |