From 6747c3a32514f6b111e845371733f7d90040063a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Mon, 16 Oct 2023 16:53:12 +0200 Subject: [PATCH] Deploy website - based on 5e45dad54a5efaf1f4dd74d9a047b72b541f3c79 --- 404.html | 8 ++--- ...ac00a.0c3c6d45.js => 045ac00a.1e6a1bd3.js} | 2 +- ...8e6cc.d9594c04.js => 0608e6cc.c9d6adfb.js} | 2 +- ...5ad39.5af33ebb.js => 09d5ad39.c5ceee01.js} | 2 +- assets/js/0dc22d83.40470123.js | 1 - assets/js/0dc22d83.f9e9ba43.js | 1 + ...d3d16.41740cbc.js => 104d3d16.3e0b4dc0.js} | 2 +- ...e4e2b.916108ff.js => 16ee4e2b.491ccdf5.js} | 2 +- ...36e0e.85b6930f.js => 18f36e0e.36fe862b.js} | 2 +- ...8f921.5f945fc0.js => 1978f921.ca4d763f.js} | 2 +- assets/js/1ca5f360.ad6203fb.js | 1 + assets/js/1ca5f360.d2b35cb3.js | 1 - assets/js/235c188d.16eeb94c.js | 1 + assets/js/235c188d.eb271f95.js | 1 - assets/js/25253a68.3ed404e2.js | 1 + assets/js/25253a68.88474a42.js | 1 - assets/js/29d77fd9.681f60cf.js | 1 + assets/js/29d77fd9.9fe56093.js | 1 - ...477ec.16d41a99.js => 366477ec.db2c93e6.js} | 2 +- ...d9ec4.ac1ed572.js => 3d5d9ec4.bc4eacb8.js} | 2 +- ...12a28.410d0ad6.js => 40412a28.47375842.js} | 2 +- assets/js/4d54d076.98accce3.js | 1 - assets/js/4d54d076.d32713f7.js | 1 + assets/js/4e839322.b4e0b348.js | 1 - ...fbeff.9243f053.js => 545fbeff.dc1e33f6.js} | 2 +- assets/js/56069e91.a17bdec1.js | 1 - assets/js/5ce17120.55b06808.js | 1 + ...c3693.ab540052.js => 5d6c3693.69ef3393.js} | 2 +- ...9aa30.c4b83163.js => 5ea9aa30.d33859bf.js} | 2 +- assets/js/691f9382.311714ab.js | 1 + assets/js/691f9382.e1e8c906.js | 1 - assets/js/6c7e1e04.dc336932.js | 1 - ...57cc8.32b0a5ef.js => 71257cc8.df5cbd9c.js} | 2 +- ...8013f.afdda49f.js => 72b8013f.ac3bb18c.js} | 2 +- assets/js/740fc5e3.a4668184.js | 1 + assets/js/740fc5e3.c31d8b0e.js | 1 - ...5bc0e.781971a2.js => 7655bc0e.7e37b31c.js} | 2 +- assets/js/814f3328.618e198f.js | 1 - assets/js/814f3328.77f9374c.js | 1 + ...51dd4.c118dd8d.js => 81b51dd4.5c786992.js} | 2 +- ...9c6bf.5208ebbd.js => 8229c6bf.f3b24c27.js} | 2 +- ...37c52.f70d9069.js => 85337c52.5d6b8ac9.js} | 2 +- assets/js/935f2afb.101bb1de.js | 1 - assets/js/935f2afb.fa6dc62a.js | 1 + ...22b11.748701c4.js => 95622b11.15a400b7.js} | 2 +- assets/js/960c2261.bf5e9f19.js | 1 - assets/js/960c2261.d3137fe9.js | 1 + ...098b1.ca867379.js => 962098b1.aaad5547.js} | 2 +- ...820cb.5d752b57.js => 965820cb.b0148f91.js} | 2 +- ...d2545.4ebe899d.js => 965d2545.5894d6de.js} | 2 +- assets/js/9fac9a61.04fdc851.js | 1 - assets/js/9fac9a61.19a9f936.js | 1 + assets/js/9ffc1a5a.17f7f029.js | 1 - ...84b25.5dbdb09d.js => a2e84b25.d9f8190e.js} | 2 +- assets/js/a38d0cd3.5d345b14.js | 1 - assets/js/a4b4a701.b69e38cf.js | 1 - assets/js/a4b4a701.e45fc532.js | 1 + ...2f4c4.8682b217.js => ab42f4c4.f533b274.js} | 2 +- ...caa44.d1ef25dd.js => afbcaa44.f62139cd.js} | 2 +- ...ac3fa.d8e6db02.js => b08ac3fa.55beba32.js} | 2 +- ...fafc9.5ea70d62.js => b12d23a1.ad5f8e87.js} | 2 +- assets/js/b2f554cd.3a72f5e5.js | 1 + assets/js/b2f554cd.79ea39d9.js | 1 - assets/js/b4dfce9f.3ad3853d.js | 1 - assets/js/b4dfce9f.cbce25c9.js | 1 + assets/js/b6687191.29078639.js | 1 + ...3559c.c2307a4a.js => b7d3559c.f26b518f.js} | 2 +- ...1f3a4.a110fad1.js => bcb12ab6.9d991321.js} | 2 +- ...5d8e4.9271eb0b.js => c4f5d8e4.2ba3944d.js} | 2 +- assets/js/c6663459.8078c4ca.js | 1 - assets/js/ca152a46.d8eaeed8.js | 1 - assets/js/ca1f5e93.46487941.js | 1 + assets/js/ca1f5e93.6a07fcb1.js | 1 - ...7e918.f0f142bd.js => cf47e918.68c90b10.js} | 2 +- assets/js/d423e08e.33cad58f.js | 1 + assets/js/d423e08e.4e5847fb.js | 1 - assets/js/d634e00a.26da7a8c.js | 1 + assets/js/dd75c6f4.306e8f7a.js | 1 + assets/js/dd75c6f4.513c4996.js | 1 - ...bc641.10961b0b.js => ebcbc641.6447a2e5.js} | 2 +- assets/js/ed335c87.7825ff1d.js | 1 - assets/js/ede70cd4.4782c45c.js | 1 + assets/js/ede70cd4.e24ff771.js | 1 - ...c04f0.d99cc036.js => f18c04f0.bdc7cb4c.js} | 2 +- ...36422.c45f0cbd.js => f5b36422.6f35d96a.js} | 2 +- ...2263b.83fd4d06.js => f802263b.b2535f55.js} | 2 +- ...df778.afdd6498.js => f84df778.22446d06.js} | 2 +- assets/js/main.845671c4.js | 2 ++ ...CENSE.txt => main.845671c4.js.LICENSE.txt} | 0 assets/js/main.9773e3c1.js | 2 -- assets/js/runtime~main.100bbb09.js | 1 + assets/js/runtime~main.d0f64164.js | 1 - blog.html | 10 +++--- blog/2019/09/01/hello-wasp.html | 10 +++--- blog/2021/02/23/journey-to-ycombinator.html | 10 +++--- blog/2021/03/02/wasp-alpha.html | 10 +++--- blog/2021/04/29/discord-bot-introduction.html | 10 +++--- blog/2021/09/01/haskell-forall-tutorial.html | 10 +++--- blog/2021/11/21/seed-round.html | 10 +++--- blog/2021/11/22/fundraising-learnings.html | 10 +++--- blog/2021/12/02/waspello.html | 10 +++--- blog/2021/12/21/shayne-intro.html | 10 +++--- blog/2022/01/27/waspleau.html | 10 +++--- blog/2022/05/31/filip-intro.html | 10 +++--- blog/2022/06/01/gitpod-hackathon-guide.html | 10 +++--- .../2022/06/15/jobs-feature-announcement.html | 10 +++--- .../ML-code-gen-vs-coding-by-hand-future.html | 10 +++--- ...ate-why-your-startup-is-worth-joining.html | 10 +++--- ...ow-and-why-i-got-started-with-haskell.html | 10 +++--- ...w-to-get-started-with-haskell-in-2022.html | 10 +++--- blog/2022/09/05/dev-excuses-app-tutrial.html | 10 +++--- blog/2022/09/29/journey-to-1000-gh-stars.html | 10 +++--- .../2022/10/28/farnance-hackathon-winner.html | 10 +++--- .../2022/11/15/auth-feature-announcement.html | 10 +++--- .../16/alpha-testing-program-post-mortem.html | 10 +++--- .../11/16/tailwind-feature-announcement.html | 10 +++--- blog/2022/11/17/hacktoberfest-wrap-up.html | 10 +++--- blog/2022/11/26/erlis-amicus-usecase.html | 10 +++--- blog/2022/11/26/michael-curry-usecase.html | 10 +++--- blog/2022/11/26/wasp-beta-launch-week.html | 10 +++--- blog/2022/11/28/why-we-chose-prisma.html | 10 +++--- blog/2022/11/29/permissions-in-web-apps.html | 10 +++--- .../29/typescript-feature-announcement.html | 10 +++--- blog/2022/11/29/wasp-beta.html | 10 +++--- ...ptimistic-update-feature-announcement.html | 10 +++--- blog/2022/12/01/beta-ide-improvements.html | 10 +++--- blog/2022/12/08/fast-fullstack-chatgpt.html | 10 +++--- blog/2023/01/11/betathon-review.html | 10 +++--- blog/2023/01/18/wasp-beta-update-dec.html | 10 +++--- blog/2023/01/31/wasp-beta-launch-review.html | 10 +++--- blog/2023/02/02/no-best-framework.html | 10 +++--- .../02/14/amicus-indiehacker-interview.html | 10 +++--- .../21/junior-developer-misconceptions.html | 10 +++--- blog/2023/03/02/wasp-beta-update-feb.html | 10 +++--- ...truths-junior-developers-need-to-hear.html | 10 +++--- ...ing-a-full-stack-app-supabase-vs-wasp.html | 10 +++--- ...ew-react-docs-pretend-spas-dont-exist.html | 10 +++--- blog/2023/04/11/wasp-launch-week-two.html | 10 +++--- blog/2023/04/12/auth-ui.html | 10 +++--- blog/2023/04/13/db-start-and-seed.html | 10 +++--- .../04/17/How-I-Built-CoverLetterGPT.html | 10 +++--- blog/2023/04/27/wasp-hackathon-two.html | 10 +++--- blog/2023/05/19/hackathon-2-review.html | 10 +++--- blog/2023/06/07/wasp-beta-update-may-23.html | 10 +++--- blog/2023/06/22/wasp-launch-week-three.html | 10 +++--- ...uild-your-own-twitter-agent-langchain.html | 10 +++--- .../06/28/what-can-you-build-with-wasp.html | 10 +++--- blog/2023/06/29/new-wasp-lsp.html | 10 +++--- blog/2023/06/30/tutorial-jam.html | 10 +++--- blog/2023/07/10/gpt-web-app-generator.html | 10 +++--- .../how-we-built-gpt-web-app-generator.html | 10 +++--- blog/2023/08/01/smol-ai-vs-wasp-ai.html | 10 +++--- ...oting-app-websockets-react-typescript.html | 10 +++--- ...ents-generate-better-web-apps-with-ai.html | 10 +++--- ...rator-how-to-use-openai-function-call.html | 10 +++--- .../contributing-open-source-land-a-job.html | 12 +++---- ...n-importance-of-naming-in-programming.html | 12 +++---- blog/2023/10/13/wasp-launch-week-four.html | 10 +++--- blog/archive.html | 10 +++--- blog/atom.xml | 6 ++-- blog/rss.xml | 6 ++-- blog/tags.html | 10 +++--- blog/tags/agent.html | 10 +++--- blog/tags/ai.html | 10 +++--- blog/tags/auth.html | 10 +++--- blog/tags/career.html | 10 +++--- blog/tags/chakra.html | 10 +++--- blog/tags/chatgpt.html | 10 +++--- blog/tags/clean-code.html | 10 +++--- blog/tags/css.html | 10 +++--- blog/tags/database.html | 10 +++--- blog/tags/discord.html | 10 +++--- blog/tags/express.html | 10 +++--- blog/tags/feature.html | 10 +++--- blog/tags/framework.html | 10 +++--- blog/tags/full-stack.html | 10 +++--- blog/tags/fullstack.html | 10 +++--- blog/tags/function-calling.html | 10 +++--- blog/tags/generate.html | 10 +++--- blog/tags/github.html | 10 +++--- blog/tags/gitpod.html | 10 +++--- blog/tags/gpt.html | 10 +++--- blog/tags/hack.html | 10 +++--- blog/tags/hackathon.html | 10 +++--- blog/tags/hacktoberfest.html | 10 +++--- blog/tags/haskell.html | 10 +++--- blog/tags/hiring.html | 10 +++--- blog/tags/indie-hacker.html | 10 +++--- blog/tags/interview.html | 10 +++--- blog/tags/javascript.html | 10 +++--- blog/tags/jobs.html | 10 +++--- blog/tags/junior-developers.html | 10 +++--- blog/tags/langchain.html | 10 +++--- blog/tags/language.html | 10 +++--- blog/tags/launch-week.html | 10 +++--- blog/tags/meme.html | 10 +++--- blog/tags/ml.html | 10 +++--- blog/tags/new-hire.html | 10 +++--- blog/tags/node.html | 10 +++--- blog/tags/nodejs.html | 10 +++--- blog/tags/open-source.html | 10 +++--- blog/tags/openai.html | 10 +++--- blog/tags/optimistic.html | 10 +++--- blog/tags/pern.html | 10 +++--- blog/tags/prd.html | 10 +++--- blog/tags/prisma.html | 10 +++--- blog/tags/product-requirement.html | 10 +++--- blog/tags/product-update.html | 10 +++--- blog/tags/programming.html | 10 +++--- blog/tags/react.html | 10 +++--- blog/tags/real-time.html | 10 +++--- blog/tags/reddit.html | 10 +++--- blog/tags/saa-s.html | 10 +++--- blog/tags/saas.html | 10 +++--- blog/tags/showcase.html | 10 +++--- blog/tags/solopreneur.html | 10 +++--- blog/tags/startup.html | 10 +++--- blog/tags/startups.html | 10 +++--- blog/tags/state-of-js.html | 10 +++--- blog/tags/stripe.html | 10 +++--- blog/tags/supabase.html | 10 +++--- blog/tags/tech-career.html | 10 +++--- blog/tags/tutorial.html | 10 +++--- blog/tags/typescript.html | 10 +++--- blog/tags/update.html | 10 +++--- blog/tags/updates.html | 10 +++--- blog/tags/wasp-ai.html | 10 +++--- blog/tags/wasp.html | 10 +++--- blog/tags/web-dev.html | 10 +++--- blog/tags/web-development.html | 10 +++--- blog/tags/webdev.html | 10 +++--- blog/tags/websockets.html | 10 +++--- docs.html | 10 +++--- docs/advanced/apis.html | 10 +++--- docs/advanced/deployment/cli.html | 10 +++--- docs/advanced/deployment/manually.html | 10 +++--- docs/advanced/deployment/overview.html | 12 +++---- docs/advanced/email.html | 10 +++--- docs/advanced/jobs.html | 10 +++--- docs/advanced/links.html | 10 +++--- docs/advanced/middleware-config.html | 10 +++--- docs/advanced/web-sockets.html | 10 +++--- docs/auth/email.html | 10 +++--- docs/auth/overview.html | 10 +++--- docs/auth/social-auth/github.html | 10 +++--- docs/auth/social-auth/google.html | 10 +++--- docs/auth/social-auth/overview.html | 10 +++--- docs/auth/ui.html | 10 +++--- docs/auth/username-and-pass.html | 10 +++--- docs/contact.html | 10 +++--- docs/contributing.html | 10 +++--- docs/data-model/backends.html | 15 +++++---- docs/data-model/crud.html | 10 +++--- docs/data-model/entities.html | 10 +++--- docs/data-model/operations/actions.html | 10 +++--- docs/data-model/operations/overview.html | 10 +++--- docs/data-model/operations/queries.html | 10 +++--- docs/editor-setup.html | 10 +++--- docs/examples.html | 10 +++--- docs/general/cli.html | 10 +++--- docs/general/language.html | 10 +++--- docs/language/features.html | 8 ++--- docs/project/client-config.html | 10 +++--- docs/project/css-frameworks.html | 10 +++--- docs/project/custom-vite-config.html | 31 +++++++++++++++++++ docs/project/customizing-app.html | 10 +++--- docs/project/dependencies.html | 10 +++--- docs/project/env-vars.html | 10 +++--- docs/project/server-config.html | 10 +++--- docs/project/starter-templates.html | 10 +++--- docs/project/static-assets.html | 10 +++--- docs/project/testing.html | 10 +++--- docs/quick-start.html | 10 +++--- docs/telemetry.html | 10 +++--- docs/tutorial/actions.html | 10 +++--- docs/tutorial/auth.html | 10 +++--- docs/tutorial/create.html | 10 +++--- docs/tutorial/entities.html | 10 +++--- docs/tutorial/pages.html | 10 +++--- docs/tutorial/project-structure.html | 12 ++++--- docs/tutorial/queries.html | 10 +++--- docs/tutorials/dev-excuses-app.html | 31 ------------------- .../01-creating-the-project.html | 31 ------------------- .../02-modifying-main-wasp-file.html | 31 ------------------- .../dev-excuses-app/03-adding-operations.html | 31 ------------------- .../04-updating-main-page-js-file.html | 31 ------------------- .../05-perform-migration-and-run.html | 31 ------------------- docs/typescript.html | 8 ++--- docs/vision.html | 10 +++--- index.html | 10 +++--- search.html | 8 ++--- sitemap.xml | 2 +- 292 files changed, 1058 insertions(+), 1217 deletions(-) rename assets/js/{045ac00a.0c3c6d45.js => 045ac00a.1e6a1bd3.js} (99%) rename assets/js/{0608e6cc.d9594c04.js => 0608e6cc.c9d6adfb.js} (98%) rename assets/js/{09d5ad39.5af33ebb.js => 09d5ad39.c5ceee01.js} (96%) delete mode 100644 assets/js/0dc22d83.40470123.js create mode 100644 assets/js/0dc22d83.f9e9ba43.js rename assets/js/{104d3d16.41740cbc.js => 104d3d16.3e0b4dc0.js} (97%) rename assets/js/{16ee4e2b.916108ff.js => 16ee4e2b.491ccdf5.js} (98%) rename assets/js/{18f36e0e.85b6930f.js => 18f36e0e.36fe862b.js} (93%) rename assets/js/{1978f921.5f945fc0.js => 1978f921.ca4d763f.js} (98%) create mode 100644 assets/js/1ca5f360.ad6203fb.js delete mode 100644 assets/js/1ca5f360.d2b35cb3.js create mode 100644 assets/js/235c188d.16eeb94c.js delete mode 100644 assets/js/235c188d.eb271f95.js create mode 100644 assets/js/25253a68.3ed404e2.js delete mode 100644 assets/js/25253a68.88474a42.js create mode 100644 assets/js/29d77fd9.681f60cf.js delete mode 100644 assets/js/29d77fd9.9fe56093.js rename assets/js/{366477ec.16d41a99.js => 366477ec.db2c93e6.js} (99%) rename assets/js/{3d5d9ec4.ac1ed572.js => 3d5d9ec4.bc4eacb8.js} (99%) rename assets/js/{40412a28.410d0ad6.js => 40412a28.47375842.js} (98%) delete mode 100644 assets/js/4d54d076.98accce3.js create mode 100644 assets/js/4d54d076.d32713f7.js delete mode 100644 assets/js/4e839322.b4e0b348.js rename assets/js/{545fbeff.9243f053.js => 545fbeff.dc1e33f6.js} (98%) delete mode 100644 assets/js/56069e91.a17bdec1.js create mode 100644 assets/js/5ce17120.55b06808.js rename assets/js/{5d6c3693.ab540052.js => 5d6c3693.69ef3393.js} (98%) rename assets/js/{5ea9aa30.c4b83163.js => 5ea9aa30.d33859bf.js} (55%) create mode 100644 assets/js/691f9382.311714ab.js delete mode 100644 assets/js/691f9382.e1e8c906.js delete mode 100644 assets/js/6c7e1e04.dc336932.js rename assets/js/{71257cc8.32b0a5ef.js => 71257cc8.df5cbd9c.js} (97%) rename assets/js/{72b8013f.afdda49f.js => 72b8013f.ac3bb18c.js} (98%) create mode 100644 assets/js/740fc5e3.a4668184.js delete mode 100644 assets/js/740fc5e3.c31d8b0e.js rename assets/js/{7655bc0e.781971a2.js => 7655bc0e.7e37b31c.js} (57%) delete mode 100644 assets/js/814f3328.618e198f.js create mode 100644 assets/js/814f3328.77f9374c.js rename assets/js/{81b51dd4.c118dd8d.js => 81b51dd4.5c786992.js} (97%) rename assets/js/{8229c6bf.5208ebbd.js => 8229c6bf.f3b24c27.js} (57%) rename assets/js/{85337c52.f70d9069.js => 85337c52.5d6b8ac9.js} (99%) delete mode 100644 assets/js/935f2afb.101bb1de.js create mode 100644 assets/js/935f2afb.fa6dc62a.js rename assets/js/{95622b11.748701c4.js => 95622b11.15a400b7.js} (97%) delete mode 100644 assets/js/960c2261.bf5e9f19.js create mode 100644 assets/js/960c2261.d3137fe9.js rename assets/js/{962098b1.ca867379.js => 962098b1.aaad5547.js} (97%) rename assets/js/{965820cb.5d752b57.js => 965820cb.b0148f91.js} (99%) rename assets/js/{965d2545.4ebe899d.js => 965d2545.5894d6de.js} (98%) delete mode 100644 assets/js/9fac9a61.04fdc851.js create mode 100644 assets/js/9fac9a61.19a9f936.js delete mode 100644 assets/js/9ffc1a5a.17f7f029.js rename assets/js/{a2e84b25.5dbdb09d.js => a2e84b25.d9f8190e.js} (96%) delete mode 100644 assets/js/a38d0cd3.5d345b14.js delete mode 100644 assets/js/a4b4a701.b69e38cf.js create mode 100644 assets/js/a4b4a701.e45fc532.js rename assets/js/{ab42f4c4.8682b217.js => ab42f4c4.f533b274.js} (99%) rename assets/js/{afbcaa44.d1ef25dd.js => afbcaa44.f62139cd.js} (52%) rename assets/js/{b08ac3fa.d8e6db02.js => b08ac3fa.55beba32.js} (97%) rename assets/js/{aa0fafc9.5ea70d62.js => b12d23a1.ad5f8e87.js} (62%) create mode 100644 assets/js/b2f554cd.3a72f5e5.js delete mode 100644 assets/js/b2f554cd.79ea39d9.js delete mode 100644 assets/js/b4dfce9f.3ad3853d.js create mode 100644 assets/js/b4dfce9f.cbce25c9.js create mode 100644 assets/js/b6687191.29078639.js rename assets/js/{b7d3559c.c2307a4a.js => b7d3559c.f26b518f.js} (80%) rename assets/js/{ed31f3a4.a110fad1.js => bcb12ab6.9d991321.js} (62%) rename assets/js/{c4f5d8e4.9271eb0b.js => c4f5d8e4.2ba3944d.js} (64%) delete mode 100644 assets/js/c6663459.8078c4ca.js delete mode 100644 assets/js/ca152a46.d8eaeed8.js create mode 100644 assets/js/ca1f5e93.46487941.js delete mode 100644 assets/js/ca1f5e93.6a07fcb1.js rename assets/js/{cf47e918.f0f142bd.js => cf47e918.68c90b10.js} (97%) create mode 100644 assets/js/d423e08e.33cad58f.js delete mode 100644 assets/js/d423e08e.4e5847fb.js create mode 100644 assets/js/d634e00a.26da7a8c.js create mode 100644 assets/js/dd75c6f4.306e8f7a.js delete mode 100644 assets/js/dd75c6f4.513c4996.js rename assets/js/{ebcbc641.10961b0b.js => ebcbc641.6447a2e5.js} (50%) delete mode 100644 assets/js/ed335c87.7825ff1d.js create mode 100644 assets/js/ede70cd4.4782c45c.js delete mode 100644 assets/js/ede70cd4.e24ff771.js rename assets/js/{f18c04f0.d99cc036.js => f18c04f0.bdc7cb4c.js} (77%) rename assets/js/{f5b36422.c45f0cbd.js => f5b36422.6f35d96a.js} (98%) rename assets/js/{f802263b.83fd4d06.js => f802263b.b2535f55.js} (96%) rename assets/js/{f84df778.afdd6498.js => f84df778.22446d06.js} (99%) create mode 100644 assets/js/main.845671c4.js rename assets/js/{main.9773e3c1.js.LICENSE.txt => main.845671c4.js.LICENSE.txt} (100%) delete mode 100644 assets/js/main.9773e3c1.js create mode 100644 assets/js/runtime~main.100bbb09.js delete mode 100644 assets/js/runtime~main.d0f64164.js create mode 100644 docs/project/custom-vite-config.html delete mode 100644 docs/tutorials/dev-excuses-app.html delete mode 100644 docs/tutorials/dev-excuses-app/01-creating-the-project.html delete mode 100644 docs/tutorials/dev-excuses-app/02-modifying-main-wasp-file.html delete mode 100644 docs/tutorials/dev-excuses-app/03-adding-operations.html delete mode 100644 docs/tutorials/dev-excuses-app/04-updating-main-page-js-file.html delete mode 100644 docs/tutorials/dev-excuses-app/05-perform-migration-and-run.html diff --git a/404.html b/404.html index 9ae23347c2..c476bdba9f 100644 --- a/404.html +++ b/404.html @@ -19,13 +19,13 @@ - - + +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- - + + \ No newline at end of file diff --git a/assets/js/045ac00a.0c3c6d45.js b/assets/js/045ac00a.1e6a1bd3.js similarity index 99% rename from assets/js/045ac00a.0c3c6d45.js rename to assets/js/045ac00a.1e6a1bd3.js index fe0b6b7a2e..47b243b06d 100644 --- a/assets/js/045ac00a.0c3c6d45.js +++ b/assets/js/045ac00a.1e6a1bd3.js @@ -1 +1 @@ -"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[18],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>h});var n=a(67294);function o(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function s(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}function r(e){for(var t=1;t=0||(o[a]=e[a]);return o}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(o[a]=e[a])}return o}var l=n.createContext({}),u=function(e){var t=n.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):r(r({},t),e)),a},p=function(e){var t=u(e.components);return n.createElement(l.Provider,{value:t},e.children)},c="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var a=e.components,o=e.mdxType,s=e.originalType,l=e.parentName,p=i(e,["components","mdxType","originalType","parentName"]),c=u(a),d=o,h=c["".concat(l,".").concat(d)]||c[d]||m[d]||s;return a?n.createElement(h,r(r({ref:t},p),{},{components:a})):n.createElement(h,r({ref:t},p))}));function h(e,t){var a=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var s=a.length,r=new Array(s);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[c]="string"==typeof e?e:o,r[1]=i;for(var u=2;u{a.d(t,{Z:()=>s});var n=a(67294),o=a(44996);const s=e=>n.createElement("div",null,n.createElement("p",{align:"center"},n.createElement("figure",null,n.createElement("img",{style:{width:e.width},alt:e.alt,src:(0,o.Z)(e.source)}),n.createElement("figcaption",{class:"image-caption",style:{fontStyle:"italic",opacity:.6,fontSize:"0.9rem"}},e.caption))))},92908:(e,t,a)=>{a.d(t,{Z:()=>r});var n=a(67294),o=a(39960);a(44996);const s=()=>n.createElement("span",{className:"in-blog-cta--divider"}," \u2192 "),r=()=>n.createElement("p",{className:"in-blog-cta-link-container"},n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://e44cy1h4s0q.typeform.com/to/ycUzQa5A"},"We are in Beta (try it out)!"),n.createElement(s,null),n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://discord.gg/rzdnErX"},"Join our community"),n.createElement(s,null),n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://wasp-lang.notion.site/Founding-Engineer-at-Wasp-402274568afa4d7eb7f428f8fa2c0816"},"Work with us"))},48238:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>u,default:()=>g,frontMatter:()=>l,metadata:()=>p,toc:()=>m});var n=a(87462),o=(a(67294),a(3905)),s=(a(39960),a(44996)),r=a(92908),i=a(70589);a(38610);const l={title:"Feature Announcement - Wasp Jobs",authors:["shayneczyzewski"],image:"/img/jobs-snippet2.png",tags:["webdev","wasp","feature","jobs"]},u=void 0,p={permalink:"/blog/2022/06/15/jobs-feature-announcement",editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/blog/2022-06-15-jobs-feature-announcement.md",source:"@site/blog/2022-06-15-jobs-feature-announcement.md",title:"Feature Announcement - Wasp Jobs",description:'You get a job!Storytime",id:"storytime",level:2},{value:"Most jobs have a boss. Our first job executor is a... pg-boss. \ud83d\ude05",id:"most-jobs-have-a-boss-our-first-job-executor-is-a-pg-boss-",level:2},{value:"Real Example - Updating Waspleau",id:"real-example---updating-waspleau",level:2},{value:"Looks neat! What\u2019s next?",id:"looks-neat-whats-next",level:2}],d={toc:m},h="wrapper";function g(e){let{components:t,...l}=e;return(0,o.kt)(h,(0,n.Z)({},d,l,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"You get a job!",src:(0,s.Z)("img/jobs-oprah.gif"),width:"300px"})),(0,o.kt)(i.ZP,{mdxType:"WaspIntro"}),(0,o.kt)(r.Z,{mdxType:"InBlogCta"}),(0,o.kt)("h2",{id:"storytime"},(0,o.kt)("strong",{parentName:"h2"},"Storytime")),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"Storytime",src:(0,s.Z)("img/jobs-storytime.gif"),width:"300px"})),(0,o.kt)("p",null,"Imagine you are working on the next unicorn SaaS web app and need to send a user an email, manipulate an uploaded image via an external API call, or recalculate some internal metrics every night. (Or, maybe you\u2019re doing some fancy blockchain thing for that frothy investment multiple; :D whatever it is, just envision an operation that may take a significant amount of time and/or fail.) How would you implement this?"),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"Spinning!",src:(0,s.Z)("img/jobs-spinner.gif"),width:"30px"})),(0,o.kt)("p",null,"You wouldn\u2019t want the server to delay sending its HTTP response until those are done (unless you are one of those people who love seeing the Mac spinning icon), so you'll need something out-of-band from the normal request-response flow. Even in an event-loop-based system like Node.js, just calling an async function isn't ideal since you will need to handle failures, retries, and throttling, amongst other concerns. And sometimes we need to schedule tasks to run in the future, or repeatedly, so we need a completely different toolset."),(0,o.kt)("p",null,"The typical solution here is to use a job queue of some kind. They are not impossible to set up, of course, but there is a fair amount of boilerplate involved, some operational expertise/overhead required, and moving from one system to another when you outgrow it is usually a challenge. These are the exact areas where we feel Wasp can provide real value, so we are happy to introduce Wasp Jobs to help out with this!"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-js",metastring:"title=src/server/workers/github.js",title:"src/server/workers/github.js"},"import axios from 'axios'\nimport { upsertMetric } from './utils.js'\n\nexport async function workerFunction() {\n const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')\n\n const metrics = [\n { name: 'Wasp GitHub Stars', value: response.data.stargazers_count },\n { name: 'Wasp GitHub Language', value: response.data.language },\n { name: 'Wasp GitHub Forks', value: response.data.forks },\n { name: 'Wasp GitHub Open Issues', value: response.data.open_issues },\n ]\n\n await Promise.all(metrics.map(upsertMetric))\n\n return metrics\n}\n")),(0,o.kt)("p",null,"Wasp allows you to write a regular async JavaScript function (like the one above that gathers GitHub metrics and stores them in the DB) and have it run within the context of a job queue system, which we call an executor. You can manually submit work to be done on the server, or specify a cron schedule to have your job automatically invoked. And, best of all, as we add more job executors in the future, you can change where it runs on a single line in your .wasp file."),(0,o.kt)("h2",{id:"most-jobs-have-a-boss-our-first-job-executor-is-a-pg-boss-"},"Most jobs have a boss. Our first job executor is a... pg-boss. \ud83d\ude05"),(0,o.kt)("p",{align:"center"},(0,o.kt)("figure",null,(0,o.kt)("img",{alt:"Eeek",src:(0,s.Z)("img/jobs-eyes.gif")}),(0,o.kt)("figcaption",null,"Me trying to lay off the job-related puns. Ok, ok, I\u2019ll quit. Ahhh!"))),(0,o.kt)("p",null,"In my prior life as a Ruby on Rails developer, the decision of how to implement jobs was pretty simple. You had Active Job at your disposal, and for backends, you would use something like Sidekiq or Delayed Job. In a similarly paved path, Python developers would have likely looked first to Celery."),(0,o.kt)("p",null,"In the JavaScript world, ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/OptimalBits/bull"},"Bull")," is quite popular these days. However, we decided to use ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/timgit/pg-boss"},"pg-boss"),", as it too provides persistence, delayed jobs, and schedules (plus many other features). But critically, pg-boss uses PostgreSQL instead of Redis (like Bull) for storage and coordination, and this was important since we did not want to introduce any new infrastructure dependencies to our existing production stack."),(0,o.kt)("p",null,"But isn\u2019t a database as a queue an anti-pattern, you may ask? Well, historically I\u2019d probably say yes. However, PostgreSQL 9.5 added SKIP LOCKED, which it specifically mentions can aid in avoiding lock contention with multiple consumer queue-like workloads [",(0,o.kt)("a",{parentName:"p",href:"https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE"},"https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE"),"]. So for the low-volume background job workloads that many apps have, we feel using a database as a queue is a great compromise and starting point for many users from a benefit vs. complexity perspective."),(0,o.kt)("p",null,"However, we will also continue to expand the number of job execution runtimes we support. Let us know in ",(0,o.kt)("a",{parentName:"p",href:"https://discord.gg/rzdnErX"},"Discord")," what you\u2019d like to see next!"),(0,o.kt)("h2",{id:"real-example---updating-waspleau"},"Real Example - Updating Waspleau"),(0,o.kt)("p",null,"If you are a regular reader of this blog (thank you, you deserve a raise! \ud83d\ude0a), you may recall we created an example app of a metrics dashboard called ",(0,o.kt)("a",{parentName:"p",href:"https://wasp-lang.dev/blog/2022/01/27/waspleau"},"Waspleau")," that used workers in the background to make periodic HTTP calls for data. In that example, we didn\u2019t yet have access to recurring jobs in Wasp, so we used Bull for scheduled jobs instead. To set up our queue-related logic we had to have this huge ",(0,o.kt)("inlineCode",{parentName:"p"},"setupFn")," wiring it all up; but now, we can remove all that code and simply use jobs instead! Here is what the new DSL looks like:"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"title=main.wasp",title:"main.wasp"},'// A cron job for fetching GitHub stats\njob getGithubStats {\n executor: PgBoss,\n perform: {\n fn: import { workerFunction } from "@server/workers/github.js"\n },\n schedule: {\n cron: "*/10 * * * *"\n }\n}\n\n// A cron job to measure how long a webpage takes to load\njob calcPageLoadTime {\n executor: PgBoss,\n perform: {\n fn: import { workerFunction } from "@server/workers/loadTime.js"\n },\n schedule: {\n cron: "*/5 * * * *",\n args: {=json {\n "url": "https://wasp-lang.dev",\n "name": "wasp-lang.dev Load Time"\n } json=}\n }\n}\n')),(0,o.kt)("p",null,"And here is an example of how you can reference and invoke jobs on the server. ",(0,o.kt)("em",{parentName:"p"},"Note: We did not even need to do this step since jobs with a schedule are automatically configured to run at the desired time.")),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-js",metastring:"title=src/server/serverSetup.js",title:"src/server/serverSetup.js"},"/**\n* These Jobs are automatically scheduled by Wasp.\n* However, let's kick them off on server setup to ensure we have data right away.\n*/\nimport { github } from '@wasp/jobs/getGithubStats.js'\nimport { loadTime } from '@wasp/jobs/calcPageLoadTime.js'\n\nexport default async function () {\n await github.submit()\n await loadTime.submit({\n url: \"https://wasp-lang.dev\",\n name: \"wasp-lang.dev Load Time\"\n })\n}\n")),(0,o.kt)("p",null,"And voila, it is really that simple. Wasp takes care of setting up pg-boss and hooking up all your job callbacks, leaving you to focus on what matters- your own code. Here is a visual of what is happening behind the scenes:"),(0,o.kt)("p",null,(0,o.kt)("img",{alt:"Architecture",src:a(37149).Z,width:"2626",height:"1452"})),(0,o.kt)("p",null,"For those interested, check out the ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/commit/1721371fc73f4485ca0046aafea2ee3fc0be41cf#diff-e158328e137176b595ad01641ba68faf82dbb88ccc5be3597009bb576fcd6505"},"full diff here")," and weep with joy for all those boilerplate lines of code we fired! We were also able to ax Redis from our infrastructure!"),(0,o.kt)("h2",{id:"looks-neat-whats-next"},"Looks neat! What\u2019s next?"),(0,o.kt)("p",null,"First off, please check out our docs for ",(0,o.kt)("a",{parentName:"p",href:"/docs/advanced/jobs"},"Jobs"),". There, you will find all the info you need to start using them. Next, if you want to see the code for this example in full, you can find it here: ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/tree/release/examples/waspleau"},"https://github.com/wasp-lang/wasp/tree/release/examples/waspleau")),(0,o.kt)("p",null,"In the future, we plan to add more job executors, including support for polyglot workers (imagine running your Python ML function from Wasp!). We are also open to any other ideas on how jobs can become more useful to you (like client-side access to server-side jobs, or client-side jobs using similar abstractions?). Let us know what you think!"),(0,o.kt)("hr",null),(0,o.kt)("small",null,"Special thanks to Tim Jones for his hard work building an amazing OSS library, ",(0,o.kt)("a",{href:"https://github.com/timgit/pg-boss",target:"_blank"},"pg-boss"),", and for reviewing this post. Please consider supporting that project if it solves your needs!"))}g.isMDXComponent=!0},70589:(e,t,a)=>{a.d(t,{ZP:()=>i});var n=a(87462),o=(a(67294),a(3905));const s={toc:[]},r="wrapper";function i(e){let{components:t,...a}=e;return(0,o.kt)(r,(0,n.Z)({},s,a,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",null,(0,o.kt)("em",{parentName:"p"},"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.")))}i.isMDXComponent=!0},37149:(e,t,a)=>{a.d(t,{Z:()=>n});const n=a.p+"assets/images/jobs-arch-3ebc08ebc717194dfac7e67fca5b8a7d.png"}}]); \ No newline at end of file +"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[18],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>h});var n=a(67294);function o(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function s(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}function r(e){for(var t=1;t=0||(o[a]=e[a]);return o}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(o[a]=e[a])}return o}var l=n.createContext({}),u=function(e){var t=n.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):r(r({},t),e)),a},p=function(e){var t=u(e.components);return n.createElement(l.Provider,{value:t},e.children)},c="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var a=e.components,o=e.mdxType,s=e.originalType,l=e.parentName,p=i(e,["components","mdxType","originalType","parentName"]),c=u(a),d=o,h=c["".concat(l,".").concat(d)]||c[d]||m[d]||s;return a?n.createElement(h,r(r({ref:t},p),{},{components:a})):n.createElement(h,r({ref:t},p))}));function h(e,t){var a=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var s=a.length,r=new Array(s);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[c]="string"==typeof e?e:o,r[1]=i;for(var u=2;u{a.d(t,{Z:()=>s});var n=a(67294),o=a(44996);const s=e=>n.createElement("div",null,n.createElement("p",{align:"center"},n.createElement("figure",null,n.createElement("img",{style:{width:e.width},alt:e.alt,src:(0,o.Z)(e.source)}),n.createElement("figcaption",{class:"image-caption",style:{fontStyle:"italic",opacity:.6,fontSize:"0.9rem"}},e.caption))))},92908:(e,t,a)=>{a.d(t,{Z:()=>r});var n=a(67294),o=a(39960);a(44996);const s=()=>n.createElement("span",{className:"in-blog-cta--divider"}," \u2192 "),r=()=>n.createElement("p",{className:"in-blog-cta-link-container"},n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://e44cy1h4s0q.typeform.com/to/ycUzQa5A"},"We are in Beta (try it out)!"),n.createElement(s,null),n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://discord.gg/rzdnErX"},"Join our community"),n.createElement(s,null),n.createElement(o.Z,{className:"in-blog-cta--link",to:"https://wasp-lang.notion.site/Founding-Engineer-at-Wasp-402274568afa4d7eb7f428f8fa2c0816"},"Work with us"))},48238:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>u,default:()=>g,frontMatter:()=>l,metadata:()=>p,toc:()=>m});var n=a(87462),o=(a(67294),a(3905)),s=(a(39960),a(44996)),r=a(92908),i=a(70589);a(38610);const l={title:"Feature Announcement - Wasp Jobs",authors:["shayneczyzewski"],image:"/img/jobs-snippet2.png",tags:["webdev","wasp","feature","jobs"]},u=void 0,p={permalink:"/blog/2022/06/15/jobs-feature-announcement",editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/blog/2022-06-15-jobs-feature-announcement.md",source:"@site/blog/2022-06-15-jobs-feature-announcement.md",title:"Feature Announcement - Wasp Jobs",description:'You get a job!Storytime",id:"storytime",level:2},{value:"Most jobs have a boss. Our first job executor is a... pg-boss. \ud83d\ude05",id:"most-jobs-have-a-boss-our-first-job-executor-is-a-pg-boss-",level:2},{value:"Real Example - Updating Waspleau",id:"real-example---updating-waspleau",level:2},{value:"Looks neat! What\u2019s next?",id:"looks-neat-whats-next",level:2}],d={toc:m},h="wrapper";function g(e){let{components:t,...l}=e;return(0,o.kt)(h,(0,n.Z)({},d,l,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"You get a job!",src:(0,s.Z)("img/jobs-oprah.gif"),width:"300px"})),(0,o.kt)(i.ZP,{mdxType:"WaspIntro"}),(0,o.kt)(r.Z,{mdxType:"InBlogCta"}),(0,o.kt)("h2",{id:"storytime"},(0,o.kt)("strong",{parentName:"h2"},"Storytime")),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"Storytime",src:(0,s.Z)("img/jobs-storytime.gif"),width:"300px"})),(0,o.kt)("p",null,"Imagine you are working on the next unicorn SaaS web app and need to send a user an email, manipulate an uploaded image via an external API call, or recalculate some internal metrics every night. (Or, maybe you\u2019re doing some fancy blockchain thing for that frothy investment multiple; :D whatever it is, just envision an operation that may take a significant amount of time and/or fail.) How would you implement this?"),(0,o.kt)("p",{align:"center"},(0,o.kt)("img",{alt:"Spinning!",src:(0,s.Z)("img/jobs-spinner.gif"),width:"30px"})),(0,o.kt)("p",null,"You wouldn\u2019t want the server to delay sending its HTTP response until those are done (unless you are one of those people who love seeing the Mac spinning icon), so you'll need something out-of-band from the normal request-response flow. Even in an event-loop-based system like Node.js, just calling an async function isn't ideal since you will need to handle failures, retries, and throttling, amongst other concerns. And sometimes we need to schedule tasks to run in the future, or repeatedly, so we need a completely different toolset."),(0,o.kt)("p",null,"The typical solution here is to use a job queue of some kind. They are not impossible to set up, of course, but there is a fair amount of boilerplate involved, some operational expertise/overhead required, and moving from one system to another when you outgrow it is usually a challenge. These are the exact areas where we feel Wasp can provide real value, so we are happy to introduce Wasp Jobs to help out with this!"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-js",metastring:"title=src/server/workers/github.js",title:"src/server/workers/github.js"},"import axios from 'axios'\nimport { upsertMetric } from './utils.js'\n\nexport async function workerFunction() {\n const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')\n\n const metrics = [\n { name: 'Wasp GitHub Stars', value: response.data.stargazers_count },\n { name: 'Wasp GitHub Language', value: response.data.language },\n { name: 'Wasp GitHub Forks', value: response.data.forks },\n { name: 'Wasp GitHub Open Issues', value: response.data.open_issues },\n ]\n\n await Promise.all(metrics.map(upsertMetric))\n\n return metrics\n}\n")),(0,o.kt)("p",null,"Wasp allows you to write a regular async JavaScript function (like the one above that gathers GitHub metrics and stores them in the DB) and have it run within the context of a job queue system, which we call an executor. You can manually submit work to be done on the server, or specify a cron schedule to have your job automatically invoked. And, best of all, as we add more job executors in the future, you can change where it runs on a single line in your .wasp file."),(0,o.kt)("h2",{id:"most-jobs-have-a-boss-our-first-job-executor-is-a-pg-boss-"},"Most jobs have a boss. Our first job executor is a... pg-boss. \ud83d\ude05"),(0,o.kt)("p",{align:"center"},(0,o.kt)("figure",null,(0,o.kt)("img",{alt:"Eeek",src:(0,s.Z)("img/jobs-eyes.gif")}),(0,o.kt)("figcaption",null,"Me trying to lay off the job-related puns. Ok, ok, I\u2019ll quit. Ahhh!"))),(0,o.kt)("p",null,"In my prior life as a Ruby on Rails developer, the decision of how to implement jobs was pretty simple. You had Active Job at your disposal, and for backends, you would use something like Sidekiq or Delayed Job. In a similarly paved path, Python developers would have likely looked first to Celery."),(0,o.kt)("p",null,"In the JavaScript world, ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/OptimalBits/bull"},"Bull")," is quite popular these days. However, we decided to use ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/timgit/pg-boss"},"pg-boss"),", as it too provides persistence, delayed jobs, and schedules (plus many other features). But critically, pg-boss uses PostgreSQL instead of Redis (like Bull) for storage and coordination, and this was important since we did not want to introduce any new infrastructure dependencies to our existing production stack."),(0,o.kt)("p",null,"But isn\u2019t a database as a queue an anti-pattern, you may ask? Well, historically I\u2019d probably say yes. However, PostgreSQL 9.5 added SKIP LOCKED, which it specifically mentions can aid in avoiding lock contention with multiple consumer queue-like workloads [",(0,o.kt)("a",{parentName:"p",href:"https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE"},"https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE"),"]. So for the low-volume background job workloads that many apps have, we feel using a database as a queue is a great compromise and starting point for many users from a benefit vs. complexity perspective."),(0,o.kt)("p",null,"However, we will also continue to expand the number of job execution runtimes we support. Let us know in ",(0,o.kt)("a",{parentName:"p",href:"https://discord.gg/rzdnErX"},"Discord")," what you\u2019d like to see next!"),(0,o.kt)("h2",{id:"real-example---updating-waspleau"},"Real Example - Updating Waspleau"),(0,o.kt)("p",null,"If you are a regular reader of this blog (thank you, you deserve a raise! \ud83d\ude0a), you may recall we created an example app of a metrics dashboard called ",(0,o.kt)("a",{parentName:"p",href:"https://wasp-lang.dev/blog/2022/01/27/waspleau"},"Waspleau")," that used workers in the background to make periodic HTTP calls for data. In that example, we didn\u2019t yet have access to recurring jobs in Wasp, so we used Bull for scheduled jobs instead. To set up our queue-related logic we had to have this huge ",(0,o.kt)("inlineCode",{parentName:"p"},"setupFn")," wiring it all up; but now, we can remove all that code and simply use jobs instead! Here is what the new DSL looks like:"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-wasp",metastring:"title=main.wasp",title:"main.wasp"},'// A cron job for fetching GitHub stats\njob getGithubStats {\n executor: PgBoss,\n perform: {\n fn: import { workerFunction } from "@server/workers/github.js"\n },\n schedule: {\n cron: "*/10 * * * *"\n }\n}\n\n// A cron job to measure how long a webpage takes to load\njob calcPageLoadTime {\n executor: PgBoss,\n perform: {\n fn: import { workerFunction } from "@server/workers/loadTime.js"\n },\n schedule: {\n cron: "*/5 * * * *",\n args: {=json {\n "url": "https://wasp-lang.dev",\n "name": "wasp-lang.dev Load Time"\n } json=}\n }\n}\n')),(0,o.kt)("p",null,"And here is an example of how you can reference and invoke jobs on the server. ",(0,o.kt)("em",{parentName:"p"},"Note: We did not even need to do this step since jobs with a schedule are automatically configured to run at the desired time.")),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-js",metastring:"title=src/server/serverSetup.js",title:"src/server/serverSetup.js"},"/**\n* These Jobs are automatically scheduled by Wasp.\n* However, let's kick them off on server setup to ensure we have data right away.\n*/\nimport { github } from '@wasp/jobs/getGithubStats.js'\nimport { loadTime } from '@wasp/jobs/calcPageLoadTime.js'\n\nexport default async function () {\n await github.submit()\n await loadTime.submit({\n url: \"https://wasp-lang.dev\",\n name: \"wasp-lang.dev Load Time\"\n })\n}\n")),(0,o.kt)("p",null,"And voila, it is really that simple. Wasp takes care of setting up pg-boss and hooking up all your job callbacks, leaving you to focus on what matters- your own code. Here is a visual of what is happening behind the scenes:"),(0,o.kt)("p",null,(0,o.kt)("img",{alt:"Architecture",src:a(73130).Z,width:"2626",height:"1452"})),(0,o.kt)("p",null,"For those interested, check out the ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/commit/1721371fc73f4485ca0046aafea2ee3fc0be41cf#diff-e158328e137176b595ad01641ba68faf82dbb88ccc5be3597009bb576fcd6505"},"full diff here")," and weep with joy for all those boilerplate lines of code we fired! We were also able to ax Redis from our infrastructure!"),(0,o.kt)("h2",{id:"looks-neat-whats-next"},"Looks neat! What\u2019s next?"),(0,o.kt)("p",null,"First off, please check out our docs for ",(0,o.kt)("a",{parentName:"p",href:"/docs/advanced/jobs"},"Jobs"),". There, you will find all the info you need to start using them. Next, if you want to see the code for this example in full, you can find it here: ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/tree/release/examples/waspleau"},"https://github.com/wasp-lang/wasp/tree/release/examples/waspleau")),(0,o.kt)("p",null,"In the future, we plan to add more job executors, including support for polyglot workers (imagine running your Python ML function from Wasp!). We are also open to any other ideas on how jobs can become more useful to you (like client-side access to server-side jobs, or client-side jobs using similar abstractions?). Let us know what you think!"),(0,o.kt)("hr",null),(0,o.kt)("small",null,"Special thanks to Tim Jones for his hard work building an amazing OSS library, ",(0,o.kt)("a",{href:"https://github.com/timgit/pg-boss",target:"_blank"},"pg-boss"),", and for reviewing this post. Please consider supporting that project if it solves your needs!"))}g.isMDXComponent=!0},70589:(e,t,a)=>{a.d(t,{ZP:()=>i});var n=a(87462),o=(a(67294),a(3905));const s={toc:[]},r="wrapper";function i(e){let{components:t,...a}=e;return(0,o.kt)(r,(0,n.Z)({},s,a,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",null,(0,o.kt)("em",{parentName:"p"},"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.")))}i.isMDXComponent=!0},73130:(e,t,a)=>{a.d(t,{Z:()=>n});const n=a.p+"assets/images/jobs-arch-3ebc08ebc717194dfac7e67fca5b8a7d.png"}}]); \ No newline at end of file diff --git a/assets/js/0608e6cc.d9594c04.js b/assets/js/0608e6cc.c9d6adfb.js similarity index 98% rename from assets/js/0608e6cc.d9594c04.js rename to assets/js/0608e6cc.c9d6adfb.js index 06661eb8ec..b2e9ba7607 100644 --- a/assets/js/0608e6cc.d9594c04.js +++ b/assets/js/0608e6cc.c9d6adfb.js @@ -1 +1 @@ -"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[7434],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>f});var r=a(67294);function n(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function s(e){for(var t=1;t=0||(n[a]=e[a]);return n}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(n[a]=e[a])}return n}var l=r.createContext({}),c=function(e){var t=r.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):s(s({},t),e)),a},p=function(e){var t=c(e.components);return r.createElement(l.Provider,{value:t},e.children)},m="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},g=r.forwardRef((function(e,t){var a=e.components,n=e.mdxType,o=e.originalType,l=e.parentName,p=i(e,["components","mdxType","originalType","parentName"]),m=c(a),g=n,f=m["".concat(l,".").concat(g)]||m[g]||u[g]||o;return a?r.createElement(f,s(s({ref:t},p),{},{components:a})):r.createElement(f,s({ref:t},p))}));function f(e,t){var a=arguments,n=t&&t.mdxType;if("string"==typeof e||n){var o=a.length,s=new Array(o);s[0]=g;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[m]="string"==typeof e?e:n,s[1]=i;for(var c=2;c{a.d(t,{Z:()=>o});var r=a(67294),n=a(44996);const o=e=>r.createElement("div",null,r.createElement("p",{align:"center"},r.createElement("figure",null,r.createElement("img",{style:{width:e.width},alt:e.alt,src:(0,n.Z)(e.source)}),r.createElement("figcaption",{class:"image-caption",style:{fontStyle:"italic",opacity:.6,fontSize:"0.9rem"}},e.caption))))},92908:(e,t,a)=>{a.d(t,{Z:()=>s});var r=a(67294),n=a(39960);a(44996);const o=()=>r.createElement("span",{className:"in-blog-cta--divider"}," \u2192 "),s=()=>r.createElement("p",{className:"in-blog-cta-link-container"},r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://e44cy1h4s0q.typeform.com/to/ycUzQa5A"},"We are in Beta (try it out)!"),r.createElement(o,null),r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://discord.gg/rzdnErX"},"Join our community"),r.createElement(o,null),r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://wasp-lang.notion.site/Founding-Engineer-at-Wasp-402274568afa4d7eb7f428f8fa2c0816"},"Work with us"))},21145:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var r=a(87462),n=(a(67294),a(3905));a(39960),a(44996),a(92908),a(70589),a(38610);const o={title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",authors:["matijasos"],image:"/img/amicus-usecase/amicus-hero-shot.png",tags:["webdev","wasp","startups","github"]},s=void 0,i={permalink:"/blog/2022/11/26/erlis-amicus-usecase",editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/blog/2022-11-26-erlis-amicus-usecase.md",source:"@site/blog/2022-11-26-erlis-amicus-usecase.md",title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",description:"amicus hero shot",date:"2022-11-26T00:00:00.000Z",formattedDate:"November 26, 2022",tags:[{label:"webdev",permalink:"/blog/tags/webdev"},{label:"wasp",permalink:"/blog/tags/wasp"},{label:"startups",permalink:"/blog/tags/startups"},{label:"github",permalink:"/blog/tags/github"}],readingTime:4.21,hasTruncateMarker:!0,authors:[{name:"Matija Sosic",title:"Co-founder & CEO @ Wasp",url:"https://github.com/matijasos",imageURL:"https://github.com/matijasos.png",key:"matijasos"}],frontMatter:{title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",authors:["matijasos"],image:"/img/amicus-usecase/amicus-hero-shot.png",tags:["webdev","wasp","startups","github"]},prevItem:{title:"Why we chose Prisma as a database layer for Wasp",permalink:"/blog/2022/11/28/why-we-chose-prisma"},nextItem:{title:"How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans",permalink:"/blog/2022/11/26/michael-curry-usecase"}},l={authorsImageUrls:[void 0]},c=[],p={toc:c},m="wrapper";function u(e){let{components:t,...o}=e;return(0,n.kt)(m,(0,r.Z)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("p",null,(0,n.kt)("img",{alt:"amicus hero shot",src:a(62418).Z,width:"1920",height:"1705"})),(0,n.kt)("p",null,(0,n.kt)("a",{parentName:"p",href:"https://github.com/ErlisK"},"Erlis Kllogjri")," is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how ",(0,n.kt)("a",{parentName:"p",href:"https://www.amicus.work/"},"Amicus")," started out."),(0,n.kt)("p",null,(0,n.kt)("a",{parentName:"p",href:"https://www.amicus.work/"},"Amicus"),' is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.'),(0,n.kt)("p",null,"Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!"))}u.isMDXComponent=!0},70589:(e,t,a)=>{a.d(t,{ZP:()=>i});var r=a(87462),n=(a(67294),a(3905));const o={toc:[]},s="wrapper";function i(e){let{components:t,...a}=e;return(0,n.kt)(s,(0,r.Z)({},o,a,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("p",null,(0,n.kt)("em",{parentName:"p"},"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.")))}i.isMDXComponent=!0},62418:(e,t,a)=>{a.d(t,{Z:()=>r});const r=a.p+"assets/images/amicus-hero-shot-5fa944706f38333bf0f22a6784b7fd2b.png"}}]); \ No newline at end of file +"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[7434],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>f});var r=a(67294);function n(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function s(e){for(var t=1;t=0||(n[a]=e[a]);return n}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(n[a]=e[a])}return n}var l=r.createContext({}),c=function(e){var t=r.useContext(l),a=t;return e&&(a="function"==typeof e?e(t):s(s({},t),e)),a},p=function(e){var t=c(e.components);return r.createElement(l.Provider,{value:t},e.children)},m="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},g=r.forwardRef((function(e,t){var a=e.components,n=e.mdxType,o=e.originalType,l=e.parentName,p=i(e,["components","mdxType","originalType","parentName"]),m=c(a),g=n,f=m["".concat(l,".").concat(g)]||m[g]||u[g]||o;return a?r.createElement(f,s(s({ref:t},p),{},{components:a})):r.createElement(f,s({ref:t},p))}));function f(e,t){var a=arguments,n=t&&t.mdxType;if("string"==typeof e||n){var o=a.length,s=new Array(o);s[0]=g;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[m]="string"==typeof e?e:n,s[1]=i;for(var c=2;c{a.d(t,{Z:()=>o});var r=a(67294),n=a(44996);const o=e=>r.createElement("div",null,r.createElement("p",{align:"center"},r.createElement("figure",null,r.createElement("img",{style:{width:e.width},alt:e.alt,src:(0,n.Z)(e.source)}),r.createElement("figcaption",{class:"image-caption",style:{fontStyle:"italic",opacity:.6,fontSize:"0.9rem"}},e.caption))))},92908:(e,t,a)=>{a.d(t,{Z:()=>s});var r=a(67294),n=a(39960);a(44996);const o=()=>r.createElement("span",{className:"in-blog-cta--divider"}," \u2192 "),s=()=>r.createElement("p",{className:"in-blog-cta-link-container"},r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://e44cy1h4s0q.typeform.com/to/ycUzQa5A"},"We are in Beta (try it out)!"),r.createElement(o,null),r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://discord.gg/rzdnErX"},"Join our community"),r.createElement(o,null),r.createElement(n.Z,{className:"in-blog-cta--link",to:"https://wasp-lang.notion.site/Founding-Engineer-at-Wasp-402274568afa4d7eb7f428f8fa2c0816"},"Work with us"))},21145:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var r=a(87462),n=(a(67294),a(3905));a(39960),a(44996),a(92908),a(70589),a(38610);const o={title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",authors:["matijasos"],image:"/img/amicus-usecase/amicus-hero-shot.png",tags:["webdev","wasp","startups","github"]},s=void 0,i={permalink:"/blog/2022/11/26/erlis-amicus-usecase",editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/blog/2022-11-26-erlis-amicus-usecase.md",source:"@site/blog/2022-11-26-erlis-amicus-usecase.md",title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",description:"amicus hero shot",date:"2022-11-26T00:00:00.000Z",formattedDate:"November 26, 2022",tags:[{label:"webdev",permalink:"/blog/tags/webdev"},{label:"wasp",permalink:"/blog/tags/wasp"},{label:"startups",permalink:"/blog/tags/startups"},{label:"github",permalink:"/blog/tags/github"}],readingTime:4.21,hasTruncateMarker:!0,authors:[{name:"Matija Sosic",title:"Co-founder & CEO @ Wasp",url:"https://github.com/matijasos",imageURL:"https://github.com/matijasos.png",key:"matijasos"}],frontMatter:{title:"Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!",authors:["matijasos"],image:"/img/amicus-usecase/amicus-hero-shot.png",tags:["webdev","wasp","startups","github"]},prevItem:{title:"Why we chose Prisma as a database layer for Wasp",permalink:"/blog/2022/11/28/why-we-chose-prisma"},nextItem:{title:"How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans",permalink:"/blog/2022/11/26/michael-curry-usecase"}},l={authorsImageUrls:[void 0]},c=[],p={toc:c},m="wrapper";function u(e){let{components:t,...o}=e;return(0,n.kt)(m,(0,r.Z)({},p,o,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("p",null,(0,n.kt)("img",{alt:"amicus hero shot",src:a(92390).Z,width:"1920",height:"1705"})),(0,n.kt)("p",null,(0,n.kt)("a",{parentName:"p",href:"https://github.com/ErlisK"},"Erlis Kllogjri")," is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how ",(0,n.kt)("a",{parentName:"p",href:"https://www.amicus.work/"},"Amicus")," started out."),(0,n.kt)("p",null,(0,n.kt)("a",{parentName:"p",href:"https://www.amicus.work/"},"Amicus"),' is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.'),(0,n.kt)("p",null,"Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!"))}u.isMDXComponent=!0},70589:(e,t,a)=>{a.d(t,{ZP:()=>i});var r=a(87462),n=(a(67294),a(3905));const o={toc:[]},s="wrapper";function i(e){let{components:t,...a}=e;return(0,n.kt)(s,(0,r.Z)({},o,a,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("p",null,(0,n.kt)("em",{parentName:"p"},"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.")))}i.isMDXComponent=!0},92390:(e,t,a)=>{a.d(t,{Z:()=>r});const r=a.p+"assets/images/amicus-hero-shot-5fa944706f38333bf0f22a6784b7fd2b.png"}}]); \ No newline at end of file diff --git a/assets/js/09d5ad39.5af33ebb.js b/assets/js/09d5ad39.c5ceee01.js similarity index 96% rename from assets/js/09d5ad39.5af33ebb.js rename to assets/js/09d5ad39.c5ceee01.js index 66160bd461..ac6e478a37 100644 --- a/assets/js/09d5ad39.5af33ebb.js +++ b/assets/js/09d5ad39.c5ceee01.js @@ -1 +1 @@ -"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[3030],{3905:(e,t,a)=>{a.d(t,{Zo:()=>u,kt:()=>g});var r=a(67294);function n(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function l(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function o(e){for(var t=1;t=0||(n[a]=e[a]);return n}(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(n[a]=e[a])}return n}var i=r.createContext({}),s=function(e){var t=r.useContext(i),a=t;return e&&(a="function"==typeof e?e(t):o(o({},t),e)),a},u=function(e){var t=s(e.components);return r.createElement(i.Provider,{value:t},e.children)},c="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},f=r.forwardRef((function(e,t){var a=e.components,n=e.mdxType,l=e.originalType,i=e.parentName,u=p(e,["components","mdxType","originalType","parentName"]),c=s(a),f=n,g=c["".concat(i,".").concat(f)]||c[f]||m[f]||l;return a?r.createElement(g,o(o({ref:t},u),{},{components:a})):r.createElement(g,o({ref:t},u))}));function g(e,t){var a=arguments,n=t&&t.mdxType;if("string"==typeof e||n){var l=a.length,o=new Array(l);o[0]=f;var p={};for(var i in t)hasOwnProperty.call(t,i)&&(p[i]=t[i]);p.originalType=e,p[c]="string"==typeof e?e:n,o[1]=p;for(var s=2;s{a.r(t),a.d(t,{assets:()=>s,contentTitle:()=>p,default:()=>f,frontMatter:()=>o,metadata:()=>i,toc:()=>u});var r=a(87462),n=(a(67294),a(3905)),l=a(44996);const o={title:"Examples"},p=void 0,i={unversionedId:"examples",id:"examples",title:"Examples",description:"We have a constantly growing collection of fully-functioning example apps, which you can use to learn more about Wasp's features.",source:"@site/docs/examples.md",sourceDirName:".",slug:"/examples",permalink:"/docs/examples",draft:!1,editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/docs/examples.md",tags:[],version:"current",frontMatter:{title:"Examples"}},s={},u=[{value:"Todo App",id:"todo-app",level:2},{value:"Waspello (Trello Clone)",id:"waspello-trello-clone",level:2},{value:"Waspleau (Realtime Statistics Dashboard)",id:"waspleau-realtime-statistics-dashboard",level:2}],c={toc:u},m="wrapper";function f(e){let{components:t,...a}=e;return(0,n.kt)(m,(0,r.Z)({},c,a,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("p",null,"We have a constantly growing collection of fully-functioning example apps, which you can use to learn more about Wasp's features."),(0,n.kt)("p",null,"The full list of examples can be found ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/tree/release/examples/"},"here"),". Here is a few of them:"),(0,n.kt)("h2",{id:"todo-app"},"Todo App"),(0,n.kt)("ul",null,(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("strong",{parentName:"li"},"Features"),": Auth (",(0,n.kt)("a",{parentName:"li",href:"language/features#authentication--authorization"},"username/password"),"), ",(0,n.kt)("a",{parentName:"li",href:"language/features#queries-and-actions-aka-operations"},"Queries & Actions"),", ",(0,n.kt)("a",{parentName:"li",href:"language/features#entity"},"Entities"),", ",(0,n.kt)("a",{parentName:"li",href:"language/features#route"},"Routes")),(0,n.kt)("li",{parentName:"ul"},"JS source code: ",(0,n.kt)("a",{parentName:"li",href:"https://github.com/wasp-lang/wasp/tree/release/examples/tutorials/TodoApp"},"GitHub")),(0,n.kt)("li",{parentName:"ul"},"TS source code: ",(0,n.kt)("a",{parentName:"li",href:"https://github.com/wasp-lang/wasp/tree/release/examples/todo-typescript"},"GitHub")),(0,n.kt)("li",{parentName:"ul"},"in-browser dev environment: ",(0,n.kt)("a",{parentName:"li",href:"https://gitpod.io/#https://github.com/wasp-lang/gitpod-template"},"GitPod"))),(0,n.kt)("h2",{id:"waspello-trello-clone"},"Waspello (Trello Clone)"),(0,n.kt)("ul",null,(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("strong",{parentName:"li"},"Features"),": Auth (",(0,n.kt)("a",{parentName:"li",href:"language/features#social-login-providers-oauth-20"},"Google"),", ",(0,n.kt)("a",{parentName:"li",href:"language/features#authentication--authorization"},"username/password"),"), ",(0,n.kt)("a",{parentName:"li",href:"language/features#the-useaction-hook"},"Optimistic Updates"),", ",(0,n.kt)("a",{parentName:"li",href:"/docs/project/css-frameworks"},"Tailwind CSS integration")),(0,n.kt)("li",{parentName:"ul"},"Source code: ",(0,n.kt)("a",{parentName:"li",href:"https://github.com/wasp-lang/wasp/tree/main/examples/waspello"},"GitHub")),(0,n.kt)("li",{parentName:"ul"},"Hosted at ",(0,n.kt)("a",{parentName:"li",href:"https://waspello-demo.netlify.app/login"},"https://waspello-demo.netlify.app"),(0,n.kt)("p",{align:"center"},(0,n.kt)("img",{src:(0,l.Z)("img/wespello-new.png"),width:"75%"})))),(0,n.kt)("h2",{id:"waspleau-realtime-statistics-dashboard"},"Waspleau (Realtime Statistics Dashboard)"),(0,n.kt)("ul",null,(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("strong",{parentName:"li"},"Features"),": Cron ",(0,n.kt)("a",{parentName:"li",href:"language/features#jobs"},"Jobs"),", ",(0,n.kt)("a",{parentName:"li",href:"language/features#server-configuration"},"Server Setup")),(0,n.kt)("li",{parentName:"ul"},"Source code: ",(0,n.kt)("a",{parentName:"li",href:"https://github.com/wasp-lang/wasp/tree/main/examples/waspleau"},"GitHub")),(0,n.kt)("li",{parentName:"ul"},"Hosted at ",(0,n.kt)("a",{parentName:"li",href:"https://waspleau.netlify.app/"},"https://waspleau.netlify.app/"),(0,n.kt)("p",{align:"center"},(0,n.kt)("img",{src:(0,l.Z)("img/waspleau.png"),width:"75%"})))))}f.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[3030],{3905:(e,t,a)=>{a.d(t,{Zo:()=>u,kt:()=>g});var r=a(67294);function n(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function l(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function o(e){for(var t=1;t=0||(n[a]=e[a]);return n}(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(n[a]=e[a])}return n}var i=r.createContext({}),s=function(e){var t=r.useContext(i),a=t;return e&&(a="function"==typeof e?e(t):o(o({},t),e)),a},u=function(e){var t=s(e.components);return r.createElement(i.Provider,{value:t},e.children)},c="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},f=r.forwardRef((function(e,t){var a=e.components,n=e.mdxType,l=e.originalType,i=e.parentName,u=p(e,["components","mdxType","originalType","parentName"]),c=s(a),f=n,g=c["".concat(i,".").concat(f)]||c[f]||m[f]||l;return a?r.createElement(g,o(o({ref:t},u),{},{components:a})):r.createElement(g,o({ref:t},u))}));function g(e,t){var a=arguments,n=t&&t.mdxType;if("string"==typeof e||n){var l=a.length,o=new Array(l);o[0]=f;var p={};for(var i in t)hasOwnProperty.call(t,i)&&(p[i]=t[i]);p.originalType=e,p[c]="string"==typeof e?e:n,o[1]=p;for(var s=2;s{a.r(t),a.d(t,{assets:()=>s,contentTitle:()=>p,default:()=>f,frontMatter:()=>o,metadata:()=>i,toc:()=>u});var r=a(87462),n=(a(67294),a(3905)),l=a(44996);const o={title:"Examples"},p=void 0,i={unversionedId:"examples",id:"examples",title:"Examples",description:"We have a constantly growing collection of fully-functioning example apps, which you can use to learn more about Wasp's features.",source:"@site/docs/examples.md",sourceDirName:".",slug:"/examples",permalink:"/docs/examples",draft:!1,editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/docs/examples.md",tags:[],version:"current",frontMatter:{title:"Examples"}},s={},u=[{value:"Todo App",id:"todo-app",level:2},{value:"Waspello (Trello Clone)",id:"waspello-trello-clone",level:2},{value:"Waspleau (Realtime Statistics Dashboard)",id:"waspleau-realtime-statistics-dashboard",level:2}],c={toc:u},m="wrapper";function f(e){let{components:t,...a}=e;return(0,n.kt)(m,(0,r.Z)({},c,a,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("p",null,"We have a constantly growing collection of fully-functioning example apps, which you can use to learn more about Wasp's features."),(0,n.kt)("p",null,"The full list of examples can be found ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/tree/release/examples/"},"here"),". Here is a few of them:"),(0,n.kt)("h2",{id:"todo-app"},"Todo App"),(0,n.kt)("ul",null,(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("strong",{parentName:"li"},"Features"),": Auth (",(0,n.kt)("a",{parentName:"li",href:"language/features#authentication--authorization"},"username/password"),"), ",(0,n.kt)("a",{parentName:"li",href:"language/features#queries-and-actions-aka-operations"},"Queries & Actions"),", ",(0,n.kt)("a",{parentName:"li",href:"language/features#entity"},"Entities"),", ",(0,n.kt)("a",{parentName:"li",href:"language/features#route"},"Routes")),(0,n.kt)("li",{parentName:"ul"},"JS source code: ",(0,n.kt)("a",{parentName:"li",href:"https://github.com/wasp-lang/wasp/tree/release/examples/tutorials/TodoApp"},"GitHub")),(0,n.kt)("li",{parentName:"ul"},"TS source code: ",(0,n.kt)("a",{parentName:"li",href:"https://github.com/wasp-lang/wasp/tree/release/examples/todo-typescript"},"GitHub")),(0,n.kt)("li",{parentName:"ul"},"in-browser dev environment: ",(0,n.kt)("a",{parentName:"li",href:"https://gitpod.io/#https://github.com/wasp-lang/gitpod-template"},"GitPod"))),(0,n.kt)("h2",{id:"waspello-trello-clone"},"Waspello (Trello Clone)"),(0,n.kt)("ul",null,(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("strong",{parentName:"li"},"Features"),": Auth (",(0,n.kt)("a",{parentName:"li",href:"language/features#social-login-providers-oauth-20"},"Google"),", ",(0,n.kt)("a",{parentName:"li",href:"language/features#authentication--authorization"},"username/password"),"), ",(0,n.kt)("a",{parentName:"li",href:"language/features#the-useaction-hook"},"Optimistic Updates"),", ",(0,n.kt)("a",{parentName:"li",href:"/docs/project/css-frameworks"},"Tailwind CSS integration")),(0,n.kt)("li",{parentName:"ul"},"Source code: ",(0,n.kt)("a",{parentName:"li",href:"https://github.com/wasp-lang/wasp/tree/main/examples/waspello"},"GitHub")),(0,n.kt)("li",{parentName:"ul"},"Hosted at ",(0,n.kt)("a",{parentName:"li",href:"https://waspello-demo.netlify.app/login"},"https://waspello-demo.netlify.app"),(0,n.kt)("p",{align:"center"},(0,n.kt)("img",{src:(0,l.Z)("img/wespello-new.png"),width:"75%"})))),(0,n.kt)("h2",{id:"waspleau-realtime-statistics-dashboard"},"Waspleau (Realtime Statistics Dashboard)"),(0,n.kt)("ul",null,(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("strong",{parentName:"li"},"Features"),": Cron ",(0,n.kt)("a",{parentName:"li",href:"language/features#jobs"},"Jobs"),", ",(0,n.kt)("a",{parentName:"li",href:"language/features#server-configuration"},"Server Setup")),(0,n.kt)("li",{parentName:"ul"},"Source code: ",(0,n.kt)("a",{parentName:"li",href:"https://github.com/wasp-lang/wasp/tree/main/examples/waspleau"},"GitHub")),(0,n.kt)("li",{parentName:"ul"},"Hosted at ",(0,n.kt)("a",{parentName:"li",href:"https://waspleau-app-client.fly.dev/"},"https://waspleau-app-client.fly.dev/"),(0,n.kt)("p",{align:"center"},(0,n.kt)("img",{src:(0,l.Z)("img/waspleau.png"),width:"75%"})))))}f.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0dc22d83.40470123.js b/assets/js/0dc22d83.40470123.js deleted file mode 100644 index 7a6e675ca8..0000000000 --- a/assets/js/0dc22d83.40470123.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkweb=self.webpackChunkweb||[]).push([[6055],{3905:(e,t,n)=>{n.d(t,{Zo:()=>p,kt:()=>h});var a=n(67294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function r(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var l=a.createContext({}),c=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},p=function(e){var t=c(e.components);return a.createElement(l.Provider,{value:t},e.children)},u="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},m=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,i=e.originalType,l=e.parentName,p=o(e,["components","mdxType","originalType","parentName"]),u=c(n),m=s,h=u["".concat(l,".").concat(m)]||u[m]||d[m]||i;return n?a.createElement(h,r(r({ref:t},p),{},{components:n})):a.createElement(h,r({ref:t},p))}));function h(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var i=n.length,r=new Array(i);r[0]=m;var o={};for(var l in t)hasOwnProperty.call(t,l)&&(o[l]=t[l]);o.originalType=e,o[u]="string"==typeof e?e:s,r[1]=o;for(var c=2;c{n.d(t,{Z:()=>r});var a=n(67294),s=n(39960);n(44996);const i=()=>a.createElement("span",{className:"in-blog-cta--divider"}," \u2192 "),r=()=>a.createElement("p",{className:"in-blog-cta-link-container"},a.createElement(s.Z,{className:"in-blog-cta--link",to:"https://e44cy1h4s0q.typeform.com/to/ycUzQa5A"},"We are in Beta (try it out)!"),a.createElement(i,null),a.createElement(s.Z,{className:"in-blog-cta--link",to:"https://discord.gg/rzdnErX"},"Join our community"),a.createElement(i,null),a.createElement(s.Z,{className:"in-blog-cta--link",to:"https://wasp-lang.notion.site/Founding-Engineer-at-Wasp-402274568afa4d7eb7f428f8fa2c0816"},"Work with us"))},36709:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>m,frontMatter:()=>r,metadata:()=>l,toc:()=>p});var a=n(87462),s=(n(67294),n(3905)),i=n(92908);const r={title:"Building an app to find an excuse for our sloppy work",authors:["maksym36ua"],tags:["wasp"]},o=void 0,l={permalink:"/blog/2022/09/05/dev-excuses-app-tutrial",editUrl:"https://github.com/wasp-lang/wasp/edit/release/web/blog/2022-09-05-dev-excuses-app-tutrial.md",source:"@site/blog/2022-09-05-dev-excuses-app-tutrial.md",title:"Building an app to find an excuse for our sloppy work",description:"We\u2019ll build a web app to solve every developer's most common problem \u2013 finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can\u2019t excuse ourselves from building it!",date:"2022-09-05T00:00:00.000Z",formattedDate:"September 5, 2022",tags:[{label:"wasp",permalink:"/blog/tags/wasp"}],readingTime:7.445,hasTruncateMarker:!0,authors:[{name:"Maksym Khamrovskyi",title:"DevRel @ Wasp",key:"maksym36ua"}],frontMatter:{title:"Building an app to find an excuse for our sloppy work",authors:["maksym36ua"],tags:["wasp"]},prevItem:{title:"How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)",permalink:"/blog/2022/09/29/journey-to-1000-gh-stars"},nextItem:{title:"How to get started with Haskell in 2022 (the straightforward way)",permalink:"/blog/2022/09/02/how-to-get-started-with-haskell-in-2022"}},c={authorsImageUrls:[void 0]},p=[{value:"The requirements were unclear.",id:"the-requirements-were-unclear",level:2},{value:"There\u2019s an issue with the third party library.",id:"theres-an-issue-with-the-third-party-library",level:2},{value:"Maybe something's wrong with the environment.",id:"maybe-somethings-wrong-with-the-environment",level:2},{value:"That worked perfectly when I developed it.",id:"that-worked-perfectly-when-i-developed-it",level:2},{value:"It would have taken twice as long to build it properly.",id:"it-would-have-taken-twice-as-long-to-build-it-properly",level:2}],u={toc:p},d="wrapper";function m(e){let{components:t,...r}=e;return(0,s.kt)(d,(0,a.Z)({},u,r,{components:t,mdxType:"MDXLayout"}),(0,s.kt)("p",null,"We\u2019ll build a web app to solve every developer's most common problem \u2013 finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can\u2019t excuse ourselves from building it!"),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"Best excuse of all time",src:n(41209).Z,width:"413",height:"360"})),(0,s.kt)("p",null,"Best excuse of all time! ",(0,s.kt)("a",{parentName:"p",href:"https://xkcd.com/303/"},"Taken from here.")),(0,s.kt)("h2",{id:"the-requirements-were-unclear"},"The requirements were unclear."),(0,s.kt)("p",null,"We\u2019ll use Michele Gerarduzzi\u2019s ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/michelegera/devexcuses-api"},"open-source project"),". It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let\u2019s define the requirements for the project: "),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"The app should be able to pull excuses data from a public API. "),(0,s.kt)("li",{parentName:"ul"},"Save the ones you liked (and your boss doesn't) to the database for future reference."),(0,s.kt)("li",{parentName:"ul"},"Building an app shouldn\u2019t take more than 15 minutes."),(0,s.kt)("li",{parentName:"ul"},"Use modern web dev technologies (NodeJS + React)")),(0,s.kt)("p",null,"As a result \u2013 we\u2019ll get a simple and fun pet project. You can find the complete codebase ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/wasp-lang/wasp/tree/release/examples/tutorials/ItWaspsOnMyMachine"},"here"),". "),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"Final result",src:n(61045).Z,width:"996",height:"568"})),(0,s.kt)("h2",{id:"theres-an-issue-with-the-third-party-library"},"There\u2019s an issue with the third party library."),(0,s.kt)("p",null,"Setting up a backbone for the project is the most frustrating part of building any application. "),(0,s.kt)("p",null,"We are installing dependencies, tying up the back-end and front-end, setting up a database, managing connection strings, and so on. Avoiding this part will save us a ton of time and effort. So let\u2019s find ourselves an excuse to skip the initial project setup."),(0,s.kt)("p",null,"Ideally \u2013 use a framework that will create a project infrastructure quickly with the best defaults so that we\u2019ll focus on the business logic. A perfect candidate is ",(0,s.kt)("a",{parentName:"p",href:"https://wasp-lang.dev/"},"Wasp"),". It\u2019s an open-source, declarative DSL for building web apps in React and Node.js with no boilerplate"),(0,s.kt)("p",null,"How it works: developer starts from a single config file that specifies the app architecture. Routes, CRUD API, auth, and so on. Then adds React/Node.js code for the specific business logic. Behind the scenes, Wasp compiler will produce the entire source code of the app - back-end, front-end, deployment template, database migrations and everything else you\u2019ve used to have in any other full-stack app. "),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"Wasp architecture",src:n(43454).Z,width:"1525",height:"696"})),(0,s.kt)("p",null,"So let\u2019s jump right in."),(0,s.kt)("h2",{id:"maybe-somethings-wrong-with-the-environment"},"Maybe something's wrong with the environment."),(0,s.kt)("p",null,"Wasp intentionally works with the LTS Node.js version since it guarantees stability and active maintenance. As for now, it\u2019s Node 16 and NPM 8. If you need another Node version for some other project \u2013 there\u2019s a possibility to ",(0,s.kt)("a",{parentName:"p",href:"https://wasp-lang.dev/docs#1-requirements"},"use NVM")," to manage multiple Node versions on your computer at the same time."),(0,s.kt)("p",null,"Installing Wasp on Linux (for Mac/Windows, please ",(0,s.kt)("a",{parentName:"p",href:"https://wasp-lang.dev/docs#2-installation"},"check the docs"),"):"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre"},"curl -sSL https://get.wasp-lang.dev/installer.sh | sh\n")),(0,s.kt)("p",null,"Now let\u2019s create a new web app named ItWaspsOnMyMachine."),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre"},"wasp new ItWaspsOnMyMachine\n")),(0,s.kt)("p",null,"Changing the working directory:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre"},"cd ItWaspsOnMyMachine\n")),(0,s.kt)("p",null,"Starting the app:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre"},"wasp start\n")),(0,s.kt)("p",null,"Now your default browser should open up with a simple predefined text message. That\u2019s it! \ud83e\udd73 We\u2019ve built and run a NodeJS + React application. And for now \u2013 the codebase consists of only two files! ",(0,s.kt)("inlineCode",{parentName:"p"},"main.wasp")," is the config file that defines the application\u2019s functionality. And ",(0,s.kt)("inlineCode",{parentName:"p"},"MainPage.js")," is the front-end."),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"Initial page",src:n(63559).Z,width:"1891",height:"1043"})),(0,s.kt)("h2",{id:"that-worked-perfectly-when-i-developed-it"},"That worked perfectly when I developed it."),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"1) Let\u2019s add some additional configuration to our ",(0,s.kt)("inlineCode",{parentName:"strong"},"main.wasp")," file. So it will look like this:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="main.wasp | Defining Excuse entity, queries and action"',title:'"main.wasp',"|":!0,Defining:!0,Excuse:!0,"entity,":!0,queries:!0,and:!0,'action"':!0},'\n// Main declaration, defines a new web app.\napp ItWaspsOnMyMachine {\n // Wasp compiler configuration\n wasp: {\n version: "^0.6.0"\n },\n\n // Used as a browser tab title. \n title: "It Wasps On My Machine",\n\n head: [\n // Adding Tailwind to make our UI prettier\n " - - + +
-
By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more
By Martin Sosic
12 min read

On Importance of Naming in Programming

Read more →

By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more →

By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more →

By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more →

By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more →

By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

By Matija Sosic
4 min read

What can you build with Wasp?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

By Matija Sosic
6 min read

Wasp Beta - May 2023

Read more →

By Vinny
6 min read

Hackathon #2: Results & Review

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Maksym Khamrovskyi
8 min read

Building an app to find an excuse for our sloppy work

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more →

By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more →

By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more →

By Martin Sosic
7 min read

Wasp - language for developing full-stack Javascript web apps with no boilerplate

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

By Martin Sosic
6 min read

Hello Wasp!

Read more →

- - +
By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more
By Martin Sosic
12 min read

On the Importance of Naming in Programming

Read more →

By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more →

By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more →

By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more →

By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more →

By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

By Matija Sosic
4 min read

What can you build with Wasp?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

By Matija Sosic
6 min read

Wasp Beta - May 2023

Read more →

By Vinny
6 min read

Hackathon #2: Results & Review

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Maksym Khamrovskyi
8 min read

Building an app to find an excuse for our sloppy work

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more →

By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more →

By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more →

By Martin Sosic
7 min read

Wasp - language for developing full-stack Javascript web apps with no boilerplate

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

By Martin Sosic
6 min read

Hello Wasp!

Read more →

+ + \ No newline at end of file diff --git a/blog/2019/09/01/hello-wasp.html b/blog/2019/09/01/hello-wasp.html index c31ce5e93f..fc76e23211 100644 --- a/blog/2019/09/01/hello-wasp.html +++ b/blog/2019/09/01/hello-wasp.html @@ -19,13 +19,13 @@ - - + +
-

Hello Wasp!

· 6 min read
Martin Sosic

About a year or so ago, brother and I started discussing how awesome it would be to have a programming language that would understand what “web app” means. Such language would, on one hand, serve as an expressive specification of the web app, while on the other hand, it would take care of “boring” work for us, while we could focus on the business logic specific for our web app.

Step by step, the idea has started to take a more concrete shape, and Wasp (Web Application SPecification language) came to life! While still very early, we are writing this blog post to explain why are we building Wasp, what is the current status and what the future may hold.

More specification, less implementation

Imagine you want to create a simple Todo web app.

You would explain it like this to your best buddy web developer: “I want to create a web app with the title ‘Todo App’ that has a single page with a list of tasks. Each task has a description and can be either marked as done or not done. The list starts as empty and tasks can be added, deleted or marked as done. I will send you designs for this. Also, I want a user to be required to register/log in.”

Now, let’s take a look at what needs to be done to implement such an app. We need to choose technologies we are going to use (frontend, backend, database, …), figure out the project file structure, set up the build toolchain, configure linting/auto-formatting/style-guide, set up tests (unit/integration, e2e), set up deployment (production, staging), set up code sharing between frontend and backend, … . Then, once everything is set up, we need to implement basic CRUD functionality (components on frontend and API on the backend), user management, probably some kind of menu on the frontend, …

We can easily see that explanation to web developer (specification) is short and concise because many details are implicit or assumed to be handled in a reasonable default way. On the other hand, implementation is complicated since it has to take care of all the details, many of them not unique for the web app we are building but common for most of the web apps. Also, if we consider the specification through time, it would look the same now and 5 years ago. On the other hand, implementation would be different, due to the new technologies that have emerged in the meantime.

So if the specification is time-resilient, short and relatively simple to describe, while implementation is complex, volatile and requires a lot of expert knowledge, how great would it be to write more of specification and less of implementation when building a web app? For that, we need more powerful languages, that will be able to express more in less code. This is where Wasp comes in.

Wasp!

The idea behind Wasp is to take everything repetitive and common in the development of a typical web app and have Wasp take care of those parts for us. Ideally, programming in Wasp would very much look like describing the specification to the web developer, therefore writing more specification and less implementation. Wasp is the one who will keep evolving and making sure your specification is implemented in the best possible technology using the industry best practices.

To achieve that, we made Wasp as a DSL (domain-specific language) that understands common concepts of a web app like pages, routes, frontend and backend and their relationship, entities, user and roles/permissions, etc. Other parts, those that are specific for our web app (business logic), we can still write in html/css/js/…, and then plug them into Wasp, combining the power of Wasp with the flexibility of existing technologies.

What’s up?

We are currently working on the first version of Wasp compiler, and are planning to soon have very first, MVP version ready. It will be just the first step of our vision of what Wasp could be, but the sooner we get it out there, the sooner we can start collecting feedback and further shaping Wasp together with the community.

We believe it will take significant effort to bring Wasp to the level where a big portion of developers will be able to build the whole app with Wasp without feeling restrained by missing flexibility or options, while on the other hand, we don’t want to wait too long until people can start using Wasp. Therefore, we decided to build it from start in such a way that a developer can at any moment “eject” from Wasp and continue on their own, where “ejecting” would mean that Wasp would generate the source code of web app that you can continue working on. That is why compiler for Wasp that we are building is actually a transpiler whose output is web app written with best practices, that you can at any moment take and continue from there if you feel too limited by Wasp. It is like having a senior developer guide you through writing a web app!

This poses the following question: “In which technologies will web app that Wasp transpiler produces be implemented?”. Well, while our vision is to offer multiple flavors here, so that you can choose the combination of technologies that you want to use, for a start we are going with one fixed technology stack, based on most popular technologies: React, Redux, NodeJS, and Mongo.

Moar

One thing that we are very excited about regarding Wasp is that Wasp understands the way web app is built. So, once you describe it in Wasp, there are many things we could be able to do with it. We could automatically generate tests since we understand the requirements. We could suggest solutions on how to improve the design of the web app. Also, since Wasp should make building web apps easier, we could build solutions on top of it, for example, a visual builder that generates Wasp code, that in turn generates a web app.

We are still very early in the Wasp journey but we are very excited about the opportunities that we imagine it could bring and about the possibilities it could unlock. We hope that this blog post will inspire others to discuss this concept and that together we will create something amazing and learn a lot on the way!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Hello Wasp!

· 6 min read
Martin Sosic

About a year or so ago, brother and I started discussing how awesome it would be to have a programming language that would understand what “web app” means. Such language would, on one hand, serve as an expressive specification of the web app, while on the other hand, it would take care of “boring” work for us, while we could focus on the business logic specific for our web app.

Step by step, the idea has started to take a more concrete shape, and Wasp (Web Application SPecification language) came to life! While still very early, we are writing this blog post to explain why are we building Wasp, what is the current status and what the future may hold.

More specification, less implementation

Imagine you want to create a simple Todo web app.

You would explain it like this to your best buddy web developer: “I want to create a web app with the title ‘Todo App’ that has a single page with a list of tasks. Each task has a description and can be either marked as done or not done. The list starts as empty and tasks can be added, deleted or marked as done. I will send you designs for this. Also, I want a user to be required to register/log in.”

Now, let’s take a look at what needs to be done to implement such an app. We need to choose technologies we are going to use (frontend, backend, database, …), figure out the project file structure, set up the build toolchain, configure linting/auto-formatting/style-guide, set up tests (unit/integration, e2e), set up deployment (production, staging), set up code sharing between frontend and backend, … . Then, once everything is set up, we need to implement basic CRUD functionality (components on frontend and API on the backend), user management, probably some kind of menu on the frontend, …

We can easily see that explanation to web developer (specification) is short and concise because many details are implicit or assumed to be handled in a reasonable default way. On the other hand, implementation is complicated since it has to take care of all the details, many of them not unique for the web app we are building but common for most of the web apps. Also, if we consider the specification through time, it would look the same now and 5 years ago. On the other hand, implementation would be different, due to the new technologies that have emerged in the meantime.

So if the specification is time-resilient, short and relatively simple to describe, while implementation is complex, volatile and requires a lot of expert knowledge, how great would it be to write more of specification and less of implementation when building a web app? For that, we need more powerful languages, that will be able to express more in less code. This is where Wasp comes in.

Wasp!

The idea behind Wasp is to take everything repetitive and common in the development of a typical web app and have Wasp take care of those parts for us. Ideally, programming in Wasp would very much look like describing the specification to the web developer, therefore writing more specification and less implementation. Wasp is the one who will keep evolving and making sure your specification is implemented in the best possible technology using the industry best practices.

To achieve that, we made Wasp as a DSL (domain-specific language) that understands common concepts of a web app like pages, routes, frontend and backend and their relationship, entities, user and roles/permissions, etc. Other parts, those that are specific for our web app (business logic), we can still write in html/css/js/…, and then plug them into Wasp, combining the power of Wasp with the flexibility of existing technologies.

What’s up?

We are currently working on the first version of Wasp compiler, and are planning to soon have very first, MVP version ready. It will be just the first step of our vision of what Wasp could be, but the sooner we get it out there, the sooner we can start collecting feedback and further shaping Wasp together with the community.

We believe it will take significant effort to bring Wasp to the level where a big portion of developers will be able to build the whole app with Wasp without feeling restrained by missing flexibility or options, while on the other hand, we don’t want to wait too long until people can start using Wasp. Therefore, we decided to build it from start in such a way that a developer can at any moment “eject” from Wasp and continue on their own, where “ejecting” would mean that Wasp would generate the source code of web app that you can continue working on. That is why compiler for Wasp that we are building is actually a transpiler whose output is web app written with best practices, that you can at any moment take and continue from there if you feel too limited by Wasp. It is like having a senior developer guide you through writing a web app!

This poses the following question: “In which technologies will web app that Wasp transpiler produces be implemented?”. Well, while our vision is to offer multiple flavors here, so that you can choose the combination of technologies that you want to use, for a start we are going with one fixed technology stack, based on most popular technologies: React, Redux, NodeJS, and Mongo.

Moar

One thing that we are very excited about regarding Wasp is that Wasp understands the way web app is built. So, once you describe it in Wasp, there are many things we could be able to do with it. We could automatically generate tests since we understand the requirements. We could suggest solutions on how to improve the design of the web app. Also, since Wasp should make building web apps easier, we could build solutions on top of it, for example, a visual builder that generates Wasp code, that in turn generates a web app.

We are still very early in the Wasp journey but we are very excited about the opportunities that we imagine it could bring and about the possibilities it could unlock. We hope that this blog post will inspire others to discuss this concept and that together we will create something amazing and learn a lot on the way!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2021/02/23/journey-to-ycombinator.html b/blog/2021/02/23/journey-to-ycombinator.html index db68a0c0ac..e388199ba7 100644 --- a/blog/2021/02/23/journey-to-ycombinator.html +++ b/blog/2021/02/23/journey-to-ycombinator.html @@ -19,19 +19,19 @@ - - + +
-

Journey to YCombinator

· 4 min read
Martin Sosic

Martin & Matija at YCombinator HQ

Wasp became part of Winter 2021 YCombinator batch!

Here we describe our journey and how we got in after applying for the third time.

The beginning

About 2 years ago (start of 2019) brother and I first started thinking about the idea of a (domain specific) language that is specialized for full-stack web app development - language that removes boilerplate and makes web development simpler. +

Journey to YCombinator

· 4 min read
Martin Sosic

Martin & Matija at YCombinator HQ

Wasp became part of Winter 2021 YCombinator batch!

Here we describe our journey and how we got in after applying for the third time.

The beginning

About 2 years ago (start of 2019) brother and I first started thinking about the idea of a (domain specific) language that is specialized for full-stack web app development - language that removes boilerplate and makes web development simpler. We named it Wasp (Web App SPecification).

After working on it for about a year as a side-project (researching the space, talking with potential users, building a prototype, learning), we realized it will take our full-time dedication to make something serious out of it, so we quit the current job and went all-in into Wasp, bootstrapping ourselves while working on it, to see how far we can get.

The journey to YCombinator

Due to the nature of Wasp (open-source, web framework / language), we were aware that we will need to raise funds at some point if we want to survive. We had a startup of our own previously, and we worked in multiple startups in the past, so we already knew quite a bit about how to go about it and what to expect.

Therefore, as soon as we went full-time into it (start of 2020), we immediately applied for YCombinator (top startup accelerator in the world). Soon, we got invited to the USA (we are from Europe) for the final on-site interview!

We spent weeks preparing for the interview, polishing our pitch, vision, business plan, our understanding of our users, doing mock interviews - all for those crucial 10 minutes (yes, interview lasts only 10 minutes!). At the end we didn’t pass the final interview, however we got encouraging feedback that, although we are too early, we have potential and should try applying again when we make more progress. This made a lot of sense to us, since we had only a very basic prototype and little traction.

We decided to continue working on Wasp for some longer time and continue applying to YC and talking with other interesting accelerators/investors, and see where that gets us - if nothing else, we will learn a lot on the way :)!

Half a year later, after making progress on multiple sides, we went for a second interview (this time online due to Covid) and while we felt it was really close, we still didn’t get in - they wanted to see more traction, more proof that people want it.

Finally, by the autumn of 2020, we were in a position where we had released an early-alpha version of Wasp, managed to build an initial community (>50 people on Discord, 500 Github stars) and made it to “Product of the day” on the Product Hunt. With all that we applied for the YC for the third time and made it in!

Interesting fact is that if you applied to YC previously and got rejected, that is actually a plus when you apply the next time (it show persistence, and they can see your progress). Also, while we did spend significant time preparing for the YC interviews, all that preparation also helped us get a better understanding of our idea, what our users(developers) really need and how to properly present it, so it was worth it regardless of the result of the interviews.

What now?

Right now (Feb 2020) we are in the middle of the YCombinator program, building community, talking with developers and developing Wasp toward beta.

It is still just the two of us and Wasp is in early stage, but with amazing community members on our side and with YC backing us up, we are not afraid to dream big!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/03/02/wasp-alpha.html b/blog/2021/03/02/wasp-alpha.html index 37fb9c58b7..e5ede9cef8 100644 --- a/blog/2021/03/02/wasp-alpha.html +++ b/blog/2021/03/02/wasp-alpha.html @@ -19,12 +19,12 @@ - - + +
-

Wasp - language for developing full-stack Javascript web apps with no boilerplate

· 7 min read
Martin Sosic

Wasp logo

For the last year and a half, my twin brother and I have been working on Wasp: a new programming language for developing full-stack web apps with less code.

Wasp is a simple declarative language that makes developing web apps easy while still allowing you to use the latest technologies like React, Node.js, and Prisma.

In this post, I will share with you why we believe Wasp could be a big thing for web development, how it works, where we are right now and what is the plan for the future!

Why Wasp?

You know how to use React, know your way around HTML/CSS/…, know how to write business logic on the backend (e.g. in Node), but when you want to build an actual web app and deploy it for others to use, you drown in all the details and extra work - responsive UI, proper error handling, security, building, deployment, authentication, managing server state on the client, managing database, different environments, ....

Iceberg of web app development

Jose Aguinaga described in a fun way the unexpected complexity of web app development in his blog post "How it feels to learn JavaScript in 2016", which still feels relevant 4 years later.

We are building Wasp because even though we are both experienced developers and have worked on multiple complex web apps in various technologies (JQuery -> Backbone -> Angular -> React, own scripts / makefile -> Grunt -> Gulp -> Webpack, PHP -> Java -> Node.js, …), we still feel building web apps is harder than it should be, due to a lot of boilerplate and repetitive work involved in the process.

The main insight for us was that while the tech stack keeps advancing rapidly, the core requirements of the apps are mostly remaining the same (auth, routing, data model CRUD, ACL, …).

That is why almost 2 years ago we started thinking about separating web app specification (what it should do) from its implementation (how it should do it).
+

Wasp - language for developing full-stack Javascript web apps with no boilerplate

· 7 min read
Martin Sosic

Wasp logo

For the last year and a half, my twin brother and I have been working on Wasp: a new programming language for developing full-stack web apps with less code.

Wasp is a simple declarative language that makes developing web apps easy while still allowing you to use the latest technologies like React, Node.js, and Prisma.

In this post, I will share with you why we believe Wasp could be a big thing for web development, how it works, where we are right now and what is the plan for the future!

Why Wasp?

You know how to use React, know your way around HTML/CSS/…, know how to write business logic on the backend (e.g. in Node), but when you want to build an actual web app and deploy it for others to use, you drown in all the details and extra work - responsive UI, proper error handling, security, building, deployment, authentication, managing server state on the client, managing database, different environments, ....

Iceberg of web app development

Jose Aguinaga described in a fun way the unexpected complexity of web app development in his blog post "How it feels to learn JavaScript in 2016", which still feels relevant 4 years later.

We are building Wasp because even though we are both experienced developers and have worked on multiple complex web apps in various technologies (JQuery -> Backbone -> Angular -> React, own scripts / makefile -> Grunt -> Gulp -> Webpack, PHP -> Java -> Node.js, …), we still feel building web apps is harder than it should be, due to a lot of boilerplate and repetitive work involved in the process.

The main insight for us was that while the tech stack keeps advancing rapidly, the core requirements of the apps are mostly remaining the same (auth, routing, data model CRUD, ACL, …).

That is why almost 2 years ago we started thinking about separating web app specification (what it should do) from its implementation (how it should do it).
This led us to the idea of extracting common web app features and concepts into a special specification language (Wasp), while the implementation details are still described via a modern stack (right now React, Node.js, Prisma).

Our vision with Wasp is to create a powerful but simple language where you can describe your web app as humanly as possible. We want to make the top of that iceberg on the image above as pleasant as possible while making the bottom part much smaller.
In such language, with just a few words, you can specify pages and their routes, specify which type of authentication you want, define basic entities / data models, describe basic data flow, choose where you want to deploy, implement specific details in React/Node, and let Wasp take care of connecting it all, building it and deploying it.

Example of wasp code describing part of a simple full-stack web app.
app todoApp {
title: "ToDo App" /* visible in tab */
}

route "/" -> page Main
page Main {
component: import Main from "@ext/Main.js" /* Import your React code. */
}

auth { /* full-stack auth out-of-the-box */
userEntity: User,
methods: {
usernameAndPassword: {}
}
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}

Check here for the complete example.

Why a language (DSL), aren’t frameworks solving this already?

Frameworks (like e.g. Ruby on Rails or Meteor) are a big inspiration to us. @@ -34,7 +34,7 @@ Currently, Wasp supports only Javascript, but we plan to add Typescript soon.
Technical note: Wasp compiler is implemented in Haskell.

Wasp compilation diagram

While right now only React and Node.js are supported, we plan to support multiple other technologies in the future.

Generated code is human readable and can easily be inspected and even ejected if Wasp becomes too limiting. If not ejecting, there is no need for you to ever look at the generated code - it is generated by Wasp in the background.

Wasp is used via wasp CLI - to run wasp project in development, all you need to do is run wasp start.

Wasp CLI output

Where is Wasp now and where is it going?

Our big vision is to move as much of the web app domain knowledge as possible into the Wasp language itself, giving Wasp more power and flexibility.

Ultimately, since Wasp would have such a deep understanding of the web app's requirements, we could generate a visual editor on top of it - allowing non-developers to participate in development alongside developers.

Also, Wasp wouldn't be tied to the specific technology but rather support multiple technologies (React/Angular/..., Node/Go/...**.

Wasp is currently in Alpha and some features are still rough or missing, there are things we haven’t solved yet and others that will probably change as we progress, but you can try it out and build and deploy web apps!

What Wasp currently supports:

  • ✅ full-stack auth (username & password)
  • ✅ pages & routing
  • ✅ blurs the line between client & server - define your server actions and queries and call them directly in your client code (RPC)!
  • ✅ smart caching of server actions and queries (automatic cache invalidation)
  • ✅ entity (data model) definition with Prisma.io
  • ✅ ACL on frontend
  • ✅ importing NPM dependencies

What is coming:

  • ⏳ ACL on backend
  • ⏳ one-click deployment
  • ⏳ more auth methods (Google, Linkedin, ...**
  • ⏳ tighter integration of entities with other features
  • ⏳ themes and layouts
  • ⏳ support for explicitly defined server API
  • ⏳ inline JS - the ability to mix JS code with Wasp code!
  • ⏳ Typescript support
  • ⏳ server-side rendering
  • ⏳ Visual Editor
  • ⏳ support for different languages on the backend
  • ⏳ richer wasp language with better tooling

You can check out our repo at https://github.com/wasp-lang/wasp and give it a try at https://wasp-lang.dev/docs -> we are always looking for feedback and suggestions on how to shape Wasp!

We also have a community on Discord, where we chat about Wasp-related stuff - join us to see what we are up to, share your opinions or get help with your Wasp project.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/04/29/discord-bot-introduction.html b/blog/2021/04/29/discord-bot-introduction.html index 6845db4aed..8980110ae6 100644 --- a/blog/2021/04/29/discord-bot-introduction.html +++ b/blog/2021/04/29/discord-bot-introduction.html @@ -19,12 +19,12 @@ - - + +
-

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

· 9 min read
Martin Sosic

Guest introducing themselves and getting full-access.
A Guest user getting access by introducing themselves in the "introductions" channel.

At Wasp, we have a Discord server for our community, where we talk with people interested in and using Wasp - Waspeteers!

In the beginning, we knew everybody in the community by their name, but as it started growing, we had a lot of people joining that never wrote anything, and the community started feeling less homey, less intimate.

This was when we decided to make it required for the new members to introduce themselves to gain access to the community. +

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

· 9 min read
Martin Sosic

Guest introducing themselves and getting full-access.
A Guest user getting access by introducing themselves in the "introductions" channel.

At Wasp, we have a Discord server for our community, where we talk with people interested in and using Wasp - Waspeteers!

In the beginning, we knew everybody in the community by their name, but as it started growing, we had a lot of people joining that never wrote anything, and the community started feeling less homey, less intimate.

This was when we decided to make it required for the new members to introduce themselves to gain access to the community. We knew that with this kind of barrier we would probably lose some potential new Waspeteers, but those that would go through it would be more engaged and better integrated.

We found no other way to accomplish this automatically but to implement our own Discord bot. In this post I will describe in detail how we did it.

High-level approach

We want the following: when a new user comes to our Discord server, they should be able to access only "public" channels, like rules, contributing, and most importantly, introductions, where they could introduce themselves.

Once they introduced themselves in the introductions channel, they would get access to the rest of the channels.

Channels user can see when Guest vs when full member.
Left: what Guest sees; Right: what Waspeteer sees.

In Discord, access control is performed via roles. There are two ways to accomplish what we need:

  1. Adding a role that grants access. When they join, they have no roles. Once they introduce themselves, they are granted a role (e.g. Member or Waspeteer) that is required to access the rest of the server.
  2. Removing a role that forbids access. When they join, they are automatically assigned a role Guest, for which we configured the non-public channels to deny access. Once they introduce themselves, the role Guest gets removed and they gain access to the rest of the server.

We decided to go with the second approach since it means we don't have to assign all the existing members with a new role. From now on, we will be talking about how to get this second approach working.

To get this going, we need to do the following:

  1. Create role Guest.
  2. Ensure that the Guest role has permissions to access only "public" channels. One convenient way to go about this is to disable "View Channels" permission for the role Guest at the level of Category, so it propagates to all the channels in it, instead of doing it for every single channel. @@ -41,7 +41,7 @@ !intro makes it easy for our bot to know when to act (in Discord, bot commands often start with !<something>).

    Let's add the needed code to bot.js:

    bot.js
    ...

    const INTRODUCTIONS_CHANNEL_ID = "<YOU_WILL_HAVE_TO_FIND_THIS_ON_DISCORD_SERVER>"

    bot.on('message', async msg => {
    if (msg.content.startsWith('!intro ')) {
    if (msg.channel.id.toString() !== INTRODUCTIONS_CHANNEL_ID) {
    const introductionsChannelName =
    msg.guild.channels.resolve(INTRODUCTIONS_CHANNEL_ID).name
    return msg.reply(
    `Please use !intro command in the ${introductionsChannelName} channel!`
    )
    }

    const introMsg = msg.content.substring('!intro '.length).trim()
    const minMsgLength = 20
    if (introMsg.length < minMsgLength) {
    return msg.reply(
    `Please write introduction at least ${minMsgLength} characters long!`
    )
    }

    return msg.reply(`Yay successful introduction!`)
    }
    })

    One thing to notice is that you will have to obtain the ID of the introductions channel and paste it in your code where I put the placeholder above. You can find out this ID by going to your Discord server in the Discord app, right-clicking on the introductions channel, and clicking on Copy ID. For this to work, you will first have to enable the "Developer Mode" (under "User Settings" > "Advanced").

    Removing the "Guest" role upon successful introduction

    What is missing is removing the Guest role upon successful introduction, so let's do that:

    bot.js
    ...

    const INTRODUCTIONS_CHANNEL_ID = "<YOU_WILL_HAVE_TO_FIND_THIS_ON_DISCORD_SERVER>"
    const GUEST_ROLE_ID = "<YOU_WILL_HAVE_TO_FIND_THIS_ON_DISCORD_SERVER>"

    bot.on('message', async msg => {
    if (msg.content.startsWith('!intro ')) {
    if (msg.channel.id.toString() !== INTRODUCTIONS_CHANNEL_ID) {
    const introductionsChannelName =
    msg.guild.channels.resolve(INTRODUCTIONS_CHANNEL_ID).name
    return msg.reply(
    `Please use !intro command in the ${introductionsChannelName} channel!`
    )
    }

    const introMsg = msg.content.substring('!intro '.length).trim()
    const minMsgLength = 20
    if (introMsg.length < minMsgLength) {
    return msg.reply(
    `Please write introduction at least ${minMsgLength} characters long!`
    )
    }

    const member = msg.guild.member(msg.author)
    try {
    if (member.roles.cache.get(GUEST_ROLE_ID)) {
    await member.roles.remove(GUEST_ROLE_ID)
    return msg.reply(
    'Nice getting to know you! You are no longer a guest' +
    ' and have full access, welcome!'
    )
    }
    } catch (error) {
    return msg.reply(`Error: ${error}`)
    }
    }
    })

    Same as with the ID of the introductions channel, now you will also need to find out the ID of the Guest role (which you should have created at some point). You can do it by finding it in the server settings, under the list of roles, right-clicking on it, and then "Copy ID".

    This is it! You can now run the bot with

    DISCORD_BOT=<TOKEN_OF_YOUR_DISCORD_BOT> node bot.js

    and if you assign yourself a Guest role on the Discord server and then type !intro Hi this is my introduction, I am happy to be here. in the introductions channel, you should see yourself getting full access together with an appropriate message from your bot.

    Deploying the bot

    note

    Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

    As such, we have updated our Deployment docs with new recommendations: https://wasp-lang.dev/docs/deploying

    While there are many ways to deploy the Discord bot, I will shortly describe how we did it via Heroku.

    We created a Heroku app wasp-discord-bot and set up the "Automatic deploys" feature on Heroku to automatically deploy every push to the production branch (our bot is on Github).

    On Heroku, we set the environment variable DISCORD_BOT to the token of our bot.

    Finally, we added Procfile to our project:

    Procfile
    worker: node bot.js

    That is it! On every push to the production branch, our bot gets deployed.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/09/01/haskell-forall-tutorial.html b/blog/2021/09/01/haskell-forall-tutorial.html index 63f44406be..3e2a77a4e7 100644 --- a/blog/2021/09/01/haskell-forall-tutorial.html +++ b/blog/2021/09/01/haskell-forall-tutorial.html @@ -19,12 +19,12 @@ - - + +
-

Tutorial: `forall` in Haskell

· 9 min read
Martin Sosic

Find out what Haskell's forall is all about.

You might have seen forall being used in Haskell like this:

f :: forall a. [a] -> [a]
f xs = ys ++ ys
where ys :: [a]
ys = reverse xs

or

liftPair :: (forall x. x -> f x) -> (a, b) -> (f a, f b)

or

data Showable = forall s. (Show s) => Showable s

forall is something called "type quantifier", and it gives extra meaning to polymorphic type signatures (e.g. :: a, :: a -> b, :: a -> Int, ...).

While normaly forall plays a role of the "universal quantifier", it can also play a role of the "existential quantifier" (depends on the situation).

What does all this mean and how can forall be used in Haskell? Read on to find out!

NOTE: we assume you are comfortable with basic polymorphism in Haskell.

Quick math/logic reminder

In mathematical logic, we have

  • universal quantifier
    • symbol: ∀x
    • interpretation: "for all", "given any"
    • example: ∀x P(x) means "for all x predicate P(x) is true".
  • existential quantifier
    • symbol: ∃x
    • interpretation: "there exists", "there is at least one", "for some"
    • example: ∃x P(x) means "there is some x for which predicate P(x) is true".

Vanilla Haskell (no extensions)

In Haskell, all polymorphic type signatures are considered to be implicitly prefixed with forall.

Therefore, if you have

f :: a -> a
g :: a -> (a -> b) -> b

it is really the same as

f :: forall a. a -> a
g :: forall a b. a -> (a -> b) -> b

What forall here does is play the role of universal quantifier. +

Tutorial: `forall` in Haskell

· 9 min read
Martin Sosic

Find out what Haskell's forall is all about.

You might have seen forall being used in Haskell like this:

f :: forall a. [a] -> [a]
f xs = ys ++ ys
where ys :: [a]
ys = reverse xs

or

liftPair :: (forall x. x -> f x) -> (a, b) -> (f a, f b)

or

data Showable = forall s. (Show s) => Showable s

forall is something called "type quantifier", and it gives extra meaning to polymorphic type signatures (e.g. :: a, :: a -> b, :: a -> Int, ...).

While normaly forall plays a role of the "universal quantifier", it can also play a role of the "existential quantifier" (depends on the situation).

What does all this mean and how can forall be used in Haskell? Read on to find out!

NOTE: we assume you are comfortable with basic polymorphism in Haskell.

Quick math/logic reminder

In mathematical logic, we have

  • universal quantifier
    • symbol: ∀x
    • interpretation: "for all", "given any"
    • example: ∀x P(x) means "for all x predicate P(x) is true".
  • existential quantifier
    • symbol: ∃x
    • interpretation: "there exists", "there is at least one", "for some"
    • example: ∃x P(x) means "there is some x for which predicate P(x) is true".

Vanilla Haskell (no extensions)

In Haskell, all polymorphic type signatures are considered to be implicitly prefixed with forall.

Therefore, if you have

f :: a -> a
g :: a -> (a -> b) -> b

it is really the same as

f :: forall a. a -> a
g :: forall a b. a -> (a -> b) -> b

What forall here does is play the role of universal quantifier. For function f, it means it is saying "for all types, this function takes that type and returns the same type.". Other way to put it would be "this funtion can be called with value of any type as its first argument, and it will return the value of that same type".

Since forall is already implicit, writing it explicitly doesn't really do anything!

Not only that, but without any extensions, you can't even write forall explicitly, you will get a syntax error, since forall is not a keyword in Haskell.

So what is the purpose of forall then? Well, obviously to be used with extensions :)!

The simplest extension is ExplicitForAll, which allows you to explicitly write forall (as we did above). This is not useful on its own though, since as we said above, explicitly writing forall doesn't change anything, it was already implicitly there.

However, there are other extensions that make use of forall keyword, like: ScopedTypeVariables, RankNTypes, ExistentialQuantification. @@ -41,7 +41,7 @@ There would be no way to write its type signature without using RankNTypes.

forall and extension ExistentialQuantification

ExistentialQuantification enables us to use forall in the type signature of data constructors.

This is useful because it enables us to define heterogeneous data types, which then allows us to store different types in a single data collection (which normally you can't do in Haskell, e.g. you can't have different types in a list).

For example, if we have

data Showable = forall s. (Show s) => Showable s

now we can do

someShowables :: [Showable]
someShowables = [Showable "Hi", Showable 5, Showable (1, 2)]

printShowables :: [Showable] -> IO ()
printShowables ss = mapM_ (\(Showable s) -> print s) ss

main :: IO ()
main = printShowables someShowables

In this example this allowed us to create a heterogeneous list, but only thing we can do with the contents of it is show them.

What is interesting is that in this case, forall plays the role of an existential quantifier (therefore the name of extension, ExistentialQuantification), unlike the role of universal quantifier it normally plays.

GADTs

Alternative approach to ExistentialQuantification is to use the GADTs extension, like this:

{-# LANGUAGE GADTs #-}
data Showable where
Showable :: (Show s) => s -> Showable

In this case forall is not needed, as it is implicit.

forall and extension TypeApplications

TypeApplications does not change how forall works like the extensions above do, but it does have an interesting interaction with forall, so we will mention it here.

TypeApplications allows you to specify values of types variables in a type.

For example, you can do show (read @Int "5") to specify that "5" should be interpreted as an Int. read has type signature :: Read a => String -> a, so what @Int does is say that that a in the type signature is Int. Therefore, read @Int :: String -> Int.

How does forall come into play here?

Well, if an identifier’s type signature does not include an explicit forall, the type variable arguments appear in the left-to-right order in which the variables appear in the type. So, foo :: Monad m => a b -> m (a c) will have its type variables ordered as m, a, b, c, and type applications will happen in that order: if we have foo @Maybe @Either, @Maybe will apply to m while @Either will apply to a. However, if you want to force a different order, for example a, b, c, m, so that @Maybe in foo @Maybe @Either applies to a, you can refactor the signature as foo :: forall a b c m. Monad m => a b -> m (a c), and now order of type variables in forall will be used when doing type applications!

This will require you to enable ExplicitForAll extension, if it is not already enabled.

Conclusion

This document should give a fair idea of how forall is used and what can be done with it, but it doesn't go into much depth or cover all of the ways forall is used in Haskell.

For more in-detail explanations and further investigation, here is a couple of useful resources:

This blog post originated from the notes I wrote in wasp-lang/haskell-handbook.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/11/21/seed-round.html b/blog/2021/11/21/seed-round.html index 1828da0f49..7233727ebc 100644 --- a/blog/2021/11/21/seed-round.html +++ b/blog/2021/11/21/seed-round.html @@ -19,13 +19,13 @@ - - + +
-

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

· 5 min read
Matija Sosic

After graduating from Y Combinator's Winter 2021 Batch, we are super excited to announce that Wasp raised $1.5m in our first funding round! The round is led by Lunar Ventures and joined by HV Capital. Also see it in TechCrunch.

The best thing about it is that the majority of our investors are either experienced engineers themselves (e.g. ex-Facebook, Twitter and Airbnb) or have a strong focus on investing in deep technology and developer companies. They share the vision we have with Wasp, understand and care about the problem we are solving.

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.

Besides Lunar and HV Capital, we are thrilled to welcome on board:

  • 468 Capital (led by Florian Leibert, founder of Mesosphere and ex-Twitter and Airbnb eng.)
  • Charlie Songhurst
  • Tokyo Black
  • Acequia Capital
  • Abstraction Capital
  • Ben Tossell, founder of Makerpad (acq. by Zapier)
  • Muthukrishnan Ramabadran, Senior Software Engineer at Lyft
  • Yun-Fang, ex-Facebook engineer
  • Marcel P. Lima from Heller House
  • Chris Schagen, former CMO on Contentful
  • Rahul Thathoo, Sr. Eng. Manager at Square
  • Preetha Parthasarathy
  • John Kobs

Why did we raise funding?

At its core, Wasp is an open-source project and we have full intention for it to stay that way. Open-source is one of the most powerful ways to write software and we want to make sure Wasp is freely accessible to every developer.

Wasp is a technically innovative and challenging project. Even though we are not building a new general programming language from scratch, there still exists an essential complexity of building a language and all the tooling around it. Wasp offers a lot of abstractions that are being introduced for the first time and there is no clear blueprint to follow, and this is why such an undertaking requires full-time attention and dedication. Hence, we plan on expanding the team with some amazing engineers to accelerate us on our journey.

Where are we today?

Today, Wasp is in Alpha. That means there are many features we still have to add and many that are probably going to change. But it also means you can try it out, build a full-stack web app and see what it is all about. You can also join our community and share your feedback and experience with us - we'd be happy to hear from you!

Since we launched our Alpha several months ago, we got some amazing feedback on Product Hunt and Hacker News.

We've also grown a lot and recently passed 1,000 stars on our Github repo - thank you!

Wasp GitHub Stars

To date, over 250 projects have been created with Wasp in the last couple of months and some were even deployed to production - like Farnance that ended up being a hackathon winner! Check out their source code here.

Farnance screenshot

The team

Martin and I have been working on Wasp for the last two years and together with our amazing contributors, who made us believe our vision is possible and made it what it is today. Having led development of several complex web apps in the past and continuously switching to the latest stack, we felt the pain and could also clearly see the patterns that we felt were mature and common enough to be worth extracting into a simpler, higher-level language.

The team
Martin and I during our first YC interview. Read here for more details on our journey to YC!

In case you couldn't tell from the photo and our identical glasses, we are twins (but not fraternal ones, and I'm a couple of minutes older, which makes me CEO :D)!

We are coming from the background of C++, algorithm competitions and applied algorithms in bioinformatics (Martin built edlib, his first OSS project - a popular sequence alignment library used by top bioinfo companies like PacBio) and did our internships in Google and Palantir. There we first encountered the modern web stack and went on to lead development of web platforms in fintech and bioinformatics space. We also had a startup previously (TalkBook), where we learned a lot about talking to users and building something that solves a problem they have.

What comes next?

With the funding secured, we can now fully focus on developing Wasp and the ecosystem around it. We can start planning for more long-term features that we couldn't fully commit to until now, and we can expand our team to move faster and bring more great people on board with new perspectives and enable them to fully employ their knowledge and creativity without any distractions.

Our immediate focus is to bring Wasp to Beta and then 1.0 (see our high-level roadmap here), while also building a strong foundation for our open source community. We believe community is the key to the success for Wasp and we will do everything in our power to make sure everybody feels welcome and has a fun and rewarding experience both building apps and contributing to the project. If you want to shape how millions of engineers develop the web apps of tomorrow, join our community and work with us!

Thank you for reading - we can't wait to see what you will build!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

· 5 min read
Matija Sosic

After graduating from Y Combinator's Winter 2021 Batch, we are super excited to announce that Wasp raised $1.5m in our first funding round! The round is led by Lunar Ventures and joined by HV Capital. Also see it in TechCrunch.

The best thing about it is that the majority of our investors are either experienced engineers themselves (e.g. ex-Facebook, Twitter and Airbnb) or have a strong focus on investing in deep technology and developer companies. They share the vision we have with Wasp, understand and care about the problem we are solving.

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.

Besides Lunar and HV Capital, we are thrilled to welcome on board:

  • 468 Capital (led by Florian Leibert, founder of Mesosphere and ex-Twitter and Airbnb eng.)
  • Charlie Songhurst
  • Tokyo Black
  • Acequia Capital
  • Abstraction Capital
  • Ben Tossell, founder of Makerpad (acq. by Zapier)
  • Muthukrishnan Ramabadran, Senior Software Engineer at Lyft
  • Yun-Fang, ex-Facebook engineer
  • Marcel P. Lima from Heller House
  • Chris Schagen, former CMO on Contentful
  • Rahul Thathoo, Sr. Eng. Manager at Square
  • Preetha Parthasarathy
  • John Kobs

Why did we raise funding?

At its core, Wasp is an open-source project and we have full intention for it to stay that way. Open-source is one of the most powerful ways to write software and we want to make sure Wasp is freely accessible to every developer.

Wasp is a technically innovative and challenging project. Even though we are not building a new general programming language from scratch, there still exists an essential complexity of building a language and all the tooling around it. Wasp offers a lot of abstractions that are being introduced for the first time and there is no clear blueprint to follow, and this is why such an undertaking requires full-time attention and dedication. Hence, we plan on expanding the team with some amazing engineers to accelerate us on our journey.

Where are we today?

Today, Wasp is in Alpha. That means there are many features we still have to add and many that are probably going to change. But it also means you can try it out, build a full-stack web app and see what it is all about. You can also join our community and share your feedback and experience with us - we'd be happy to hear from you!

Since we launched our Alpha several months ago, we got some amazing feedback on Product Hunt and Hacker News.

We've also grown a lot and recently passed 1,000 stars on our Github repo - thank you!

Wasp GitHub Stars

To date, over 250 projects have been created with Wasp in the last couple of months and some were even deployed to production - like Farnance that ended up being a hackathon winner! Check out their source code here.

Farnance screenshot

The team

Martin and I have been working on Wasp for the last two years and together with our amazing contributors, who made us believe our vision is possible and made it what it is today. Having led development of several complex web apps in the past and continuously switching to the latest stack, we felt the pain and could also clearly see the patterns that we felt were mature and common enough to be worth extracting into a simpler, higher-level language.

The team
Martin and I during our first YC interview. Read here for more details on our journey to YC!

In case you couldn't tell from the photo and our identical glasses, we are twins (but not fraternal ones, and I'm a couple of minutes older, which makes me CEO :D)!

We are coming from the background of C++, algorithm competitions and applied algorithms in bioinformatics (Martin built edlib, his first OSS project - a popular sequence alignment library used by top bioinfo companies like PacBio) and did our internships in Google and Palantir. There we first encountered the modern web stack and went on to lead development of web platforms in fintech and bioinformatics space. We also had a startup previously (TalkBook), where we learned a lot about talking to users and building something that solves a problem they have.

What comes next?

With the funding secured, we can now fully focus on developing Wasp and the ecosystem around it. We can start planning for more long-term features that we couldn't fully commit to until now, and we can expand our team to move faster and bring more great people on board with new perspectives and enable them to fully employ their knowledge and creativity without any distractions.

Our immediate focus is to bring Wasp to Beta and then 1.0 (see our high-level roadmap here), while also building a strong foundation for our open source community. We believe community is the key to the success for Wasp and we will do everything in our power to make sure everybody feels welcome and has a fun and rewarding experience both building apps and contributing to the project. If you want to shape how millions of engineers develop the web apps of tomorrow, join our community and work with us!

Thank you for reading - we can't wait to see what you will build!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2021/11/22/fundraising-learnings.html b/blog/2021/11/22/fundraising-learnings.html index daf91aaa5e..bfecf81883 100644 --- a/blog/2021/11/22/fundraising-learnings.html +++ b/blog/2021/11/22/fundraising-learnings.html @@ -19,15 +19,15 @@ - - + +
-

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

· 8 min read
Matija Sosic

Wasp fundraise chart

Wasp was part of Y Combinator’s W21 batch, which took place from January of 2021 until the end of March.

We want to share what we learned during the process!

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.

At Demo Day, our product had a solid traction (200+ projects created, 1k Github stars, good ProductHunt and HackerNews feedback) but no monetisation yet, which is typical for open-source projects at this stage. Being based in the EU, we also didn't have a huge network in the Bay Area prior to the fundraise.

caution

I will try to refrain from giving "general" advice (as our fundraise is a single data point), and focus on the stats and specific things that worked for us. Keep in mind the same might not work for you - I recommend always taking advice with a pinch of salt to see what makes the most sense in your case.

As we approached our fundraise, we didn't really know what to expect. We had friends from the previous batch that raised a big round very quickly (even before Demo Day) and heard a couple of stories from a few other YC founders who were also quite successful, so we imagined it might go quickly for us too.

As you can see from the title, we had quite a journey with plenty of meetings that provided us a lot of input on how to improve our pitch, and maybe even more importantly, how to reach the right investors.

Here are our stats:

  • we spoke to 212 investors → that led to 250+ meetings
  • 98 days passed between the first and the last signed SAFE
  • 171 investor passed, 24 never responded, 17 invested

And here is how it all looked when laid out on a timeline: +

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

· 8 min read
Matija Sosic

Wasp fundraise chart

Wasp was part of Y Combinator’s W21 batch, which took place from January of 2021 until the end of March.

We want to share what we learned during the process!

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.

At Demo Day, our product had a solid traction (200+ projects created, 1k Github stars, good ProductHunt and HackerNews feedback) but no monetisation yet, which is typical for open-source projects at this stage. Being based in the EU, we also didn't have a huge network in the Bay Area prior to the fundraise.

caution

I will try to refrain from giving "general" advice (as our fundraise is a single data point), and focus on the stats and specific things that worked for us. Keep in mind the same might not work for you - I recommend always taking advice with a pinch of salt to see what makes the most sense in your case.

As we approached our fundraise, we didn't really know what to expect. We had friends from the previous batch that raised a big round very quickly (even before Demo Day) and heard a couple of stories from a few other YC founders who were also quite successful, so we imagined it might go quickly for us too.

As you can see from the title, we had quite a journey with plenty of meetings that provided us a lot of input on how to improve our pitch, and maybe even more importantly, how to reach the right investors.

Here are our stats:

  • we spoke to 212 investors → that led to 250+ meetings
  • 98 days passed between the first and the last signed SAFE
  • 171 investor passed, 24 never responded, 17 invested

And here is how it all looked when laid out on a timeline: Wasp fundraise chart

Here are some of the things that worked for us:

We treated fundraising as a sales process (and stuck to it)

Wasp fundraise funnel

This means we had a typical sales funnel - lead generation, selling (pitching) and following up:

  • Lead generation: it started with Demo Day of course, from which we got 100+ leads but none of them ended up investing (more on that below). After that we mainly relied on our YC batchmates to identify relevant investors and get the intros.
  • Pitching: we did a conversational pitch without the deck, but we had a Notion one-pager from which I would drop links during the conversation (to e.g. our traction chart, user testimonials etc.). It also worked well as investors would typically find it interesting and keep scrolling through as we talked, asking follow-up questions.
  • Following-up: we followed up once per week. I would usually "batch process" it each Wednesday. We used Streak to identify all the leads that I haven't heard from in over 7 days (there is a filter for that) and then manually emailed them.

We started with tracking everything in Google Sheets, but with the volume of leads it soon became hard to navigate them through the funnel. Then we switched to Streak (used their fundraising template, and modified it a bit) and that worked great. The most helpful thing for me was having a CRM that is integrated with gmail, that made the process much more seamless and gave us better overview of the funnel. As soon as I would receive an email I could see in which stage the investor is, and it was also super easy to add new investors straight from gmail - it saved us from the dreaded context switching and kept us focused.

Our pitch became much better after ~50 meetings

We kept being critical of our pitch and kept a list of questions that we felt needed more work. We called it "creating narratives", e.g. why the right time for our product is now, presenting the team, or how we plan to monetise. We talked to other companies in the same space (devtools, OSS), investigated comparatives (big companies we compared ourselves too), talked to our angels who were domain experts and used all that to build a more convincing story.

I never intended to learn our pitch by heart, but after delivering it for 100s of times just that happened - both me and Martin (my brother and cofounder, who wasn't pitching but was always sitting behind me and provided feedback, especially in the beginning) knew it word by word and I realised how much more polished it sounds and how much more confident I felt compared to when we just started.

Our goal was to get to 100 no's

After about 50 meetings (and about 20 VCs having passed on us) we started feeling a bit disheartened, as things didn't seem to go so easy as we initially expected. Then I chatted to a friend who also recently finished their fundraise and he gave me a tour of Streak - I saw their numbers and that over 150 investors passed on them! With that I realised our 20 passes were just the beginning and that instead of chasing yeses we should actually chase no's :) - they are more predictable, you'll get plenty of them and they will clearly show your progress.

We had 100+ leads from Demo Day - none of them invested

This is probably pretty specific for our case, but it's how it went. Connecting with a startup on Demo Day is a very low-cost action for investors. Also, as many investors as there are on Demo Day, there are even more of them who aren't.

When we sorted through the connections we got, about 20% were a really good fit for us, meaning they invest in deep tech / OSS companies, have invested recently, invest in our stage etc.

We still met with pretty much all the interested leads, but we quickly realised that due to our product being deeply technical and the company being pre-revenue, only investors with engineering backgrounds were really interested because they could understand and get excited about what we do. That informed us to generate our leads with much narrower focus.

We looked at other OSS & dev tools companies in our batch, looked at who invested in them and asked for intros. Our batchmates were also in the fundraising mode, they knew how hard it can be and they wanted to help, so everything moved very quickly.

We learned not to spend time on non-believers

As we learned to focus on the highly qualified leads, we also learned that it is very hard (impossible) to change somebody's mind. Plenty of investors liked u and what we do, but they were skeptical about e.g. market size or monetisation potential and made that clear from the start. Many of them were keen to keep chatting, wanted to meet our angel investors etc., but none of that helped change their mind and it was very distracting for us. I believe it is very hard to change somebody's worldview, especially in the seed stage when there is often no strong factual evidence to do so.

Passing through the "valley of death"

As you can see on the chart, about two months in we barely passed $300k, and we had a whole month with no progress. At the same time, we felt that our pitch got significantly better and we were reaching investors much better suited for us. It was one of the most difficult times, seeing others close their rounds, but we decided to trust in the process and keep going until we have used all the resources we had. It was also the time our lead investor took time to do their own pretty extensive due diligence on Wasp, so although it looks like no progress was made from the outside, a lot of stuff was actually happening behind the scenes.

Suddenly, a few things clicked together from multiple sides and our round was quickly closed, even oversubscribed! It was truly a magical feeling to start closing investors in a single day, even during the first call, when previously it took us weeks to close our first $50k check. The big factor was also that our round was getting filled up and that of course motivated investors to move faster.

We compared ourselves to big, successful companies

This is one of the best pieces of advice we got from YC partners about fundraising. In the beginning we didn't understand how important this was, but once the meetings started we realised this was one of the best ways to explain the potential of our company to investors. With the innovation in technology that isn't easy to grasp, they needed something to hold on to understand how the business model and distribution could work, and it sounds much more doable if there is a playbook we can follow rather than us reinventing that as well. We kept working on finding a good comparable (we had a few) and explaining in which ways we are similar and why.

Good luck - you can do it!

I hope you found this helpful and that our story will motivate you to keep going once things get hard! We wish you the best of luck and also feel free to reach out if you'll have any questions.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/12/02/waspello.html b/blog/2021/12/02/waspello.html index 8122fe042f..f9f9135470 100644 --- a/blog/2021/12/02/waspello.html +++ b/blog/2021/12/02/waspello.html @@ -19,15 +19,15 @@ - - + +
-

How we built a Trello clone with Wasp - Waspello!

· 10 min read
Matija Sosic

Enter Waspello

Try Waspello here! | See the code

We've built a Trello clone using Wasp! Read on to learn how it went and how you can contribute.

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.

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

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

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())
username 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=}

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 "@server/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]
}

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:

src/server/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 }
})
}

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 "@client/MainPage.js"
}

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 src/client/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:

src/client/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 (
...
)
}

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.
+

How we built a Trello clone with Wasp - Waspello!

· 10 min read
Matija Sosic

Enter Waspello

Try Waspello here! | See the code

We've built a Trello clone using Wasp! Read on to learn how it went and how you can contribute.

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.

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

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

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())
username 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=}

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 "@server/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]
}

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:

src/server/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 }
})
}

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 "@client/MainPage.js"
}

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 src/client/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:

src/client/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 (
...
)
}

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:

Waspello - no optimistic UI update
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

A typical approach for solving this issue is to make the client a bit more self-confident, in a way that it doesn't wait for the confirmation from the server but rather immediately updates the UI, at the same time or even before the API request is fired. If it then turns out something went wrong on the server (which typically shouldn't happen), it reverses the change and shows an error message. Thus the name optimistic UI update, since the client assumes in advance that everything will go well to provide a nicer UX.

Waspello - the client being brave
The client when performing an optimistic UI update

This is one of the most complex and error-prone features when developing web apps today and that is why we are super excited to tackle it in Wasp and make the experience as smooth as possible! We are currently in the "figuring out the solution" stage and you can track/join the discussion on GitHub!

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!

Waspello propaganda
Lightweight Waspello propaganda

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!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2021/12/21/shayne-intro.html b/blog/2021/12/21/shayne-intro.html index 3cc91f352c..b55a5a02ce 100644 --- a/blog/2021/12/21/shayne-intro.html +++ b/blog/2021/12/21/shayne-intro.html @@ -19,13 +19,13 @@ - - + +
-

Meet the team - Shayne Czyzewski, Founding Engineer

· 4 min read
Matija Sosic

Welcome Shayne!

Find Shayne on Twitter and GitHub.

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 super excited to introduce Shayne, the first person to join the Wasp team! Shayne is a battle-tested veteran engineer, with experiences ranging from leading teams at high-growth startups to working at enterprise giants such as Red Hat and NetApp. Along with that, he is super nice and incredibly pleasant to work with - we are beyond thrilled that he chose Wasp for his next adventure with him and can't wait for you to meet him in our Discord community!

Why did you join Wasp?

I have always been excited about high-quality dev tooling and web frameworks, and I am also interested in Haskell/compilers. The technology, problem space, and team were just too compelling to pass up. I was also excited to be on the ground floor of a YC startup, where I can have a significant impact and help build a broad, welcoming, open-source community of Wasp developers.

What did you do before?

I have been a professional developer for over a decade, mostly in backend web development, with experience from Lockheed Martin, Morgan Stanley, NetApp, and Red Hat. Most recently, I was the head of engineering at an edtech company called LearnPlatform, where we were handling a quarter of a billion incoming events per day with the goal of understanding and improving student access to technology that works best for them.

What is your favorite language/framework?

My favorite framework is probably Ruby on Rails, for the elegance of ideas and seamless implementation. I never had an actual favorite programming language, as I enjoy different aspects of Ruby, Elixir, JavaScript, C#, and others. My least favorite has always been Java. My current favorite language is fast becoming Haskell. :)

The most interesting niche programming language I have used professionally was Ada at Lockheed Martin. We used it to build distributed, real-time, full-motion flight simulators for the military (think multi-million dollar, hyperrealistic multiplayer video games).

What are you most excited about in Wasp?

As web developers, I think we have gotten accustomed to a certain level of complexity that is not associated with the problem we are solving but the boilerplate of the process. This lack of nuance between accidental and essential complexity has recently led to less than ideal low-code approaches. Wasp, in my view, takes the better approach of a higher-level DSL to abstract some of the typical details using best practices, leaving you to focus on your problem by writing actual code that produces a real web app without any vendor lock-in. That is pretty amazing to me!

How did you start coding?

Probably by creating some basic LAMP apps in the late 90s while in high school. Growing up, our parents wanted us to have summer jobs to earn money we could spend during the rest of the year. I quickly found that freelance web development on Elance, and similar sites, was more enjoyable and profitable than the alternatives available to 15-year-olds. From then on, I was hooked.

What is your dev setup?

MacBook Air M1 with an external Dell display, Magic Trackpad, and a split mechanical keyboard from UHK (Ultimate Hacking Keyboard).

camelCase or snake_case?

I default to whatever the language or codebase conventions are. Visually, I prefer snake case, though (and definitely spaces over tabs). ;)

What's one piece of advice you'd give to an aspiring developer?

One of the biggest differentiators I have found between good and great engineers is that the great ones possess a continuous desire to learn and grow. They view challenges as fun opportunities to expand their knowledge and skills, recognizing that they always have room for improvement. The corollary is that impostor syndrome is real and never goes away, so try not to be too hard on yourself along the way!

This post was the first of several new hire announcements in the months to come, so stay tuned and reach out if you want to work with Martin, Shayne, and myself!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Meet the team - Shayne Czyzewski, Founding Engineer

· 4 min read
Matija Sosic

Welcome Shayne!

Find Shayne on Twitter and GitHub.

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 super excited to introduce Shayne, the first person to join the Wasp team! Shayne is a battle-tested veteran engineer, with experiences ranging from leading teams at high-growth startups to working at enterprise giants such as Red Hat and NetApp. Along with that, he is super nice and incredibly pleasant to work with - we are beyond thrilled that he chose Wasp for his next adventure with him and can't wait for you to meet him in our Discord community!

Why did you join Wasp?

I have always been excited about high-quality dev tooling and web frameworks, and I am also interested in Haskell/compilers. The technology, problem space, and team were just too compelling to pass up. I was also excited to be on the ground floor of a YC startup, where I can have a significant impact and help build a broad, welcoming, open-source community of Wasp developers.

What did you do before?

I have been a professional developer for over a decade, mostly in backend web development, with experience from Lockheed Martin, Morgan Stanley, NetApp, and Red Hat. Most recently, I was the head of engineering at an edtech company called LearnPlatform, where we were handling a quarter of a billion incoming events per day with the goal of understanding and improving student access to technology that works best for them.

What is your favorite language/framework?

My favorite framework is probably Ruby on Rails, for the elegance of ideas and seamless implementation. I never had an actual favorite programming language, as I enjoy different aspects of Ruby, Elixir, JavaScript, C#, and others. My least favorite has always been Java. My current favorite language is fast becoming Haskell. :)

The most interesting niche programming language I have used professionally was Ada at Lockheed Martin. We used it to build distributed, real-time, full-motion flight simulators for the military (think multi-million dollar, hyperrealistic multiplayer video games).

What are you most excited about in Wasp?

As web developers, I think we have gotten accustomed to a certain level of complexity that is not associated with the problem we are solving but the boilerplate of the process. This lack of nuance between accidental and essential complexity has recently led to less than ideal low-code approaches. Wasp, in my view, takes the better approach of a higher-level DSL to abstract some of the typical details using best practices, leaving you to focus on your problem by writing actual code that produces a real web app without any vendor lock-in. That is pretty amazing to me!

How did you start coding?

Probably by creating some basic LAMP apps in the late 90s while in high school. Growing up, our parents wanted us to have summer jobs to earn money we could spend during the rest of the year. I quickly found that freelance web development on Elance, and similar sites, was more enjoyable and profitable than the alternatives available to 15-year-olds. From then on, I was hooked.

What is your dev setup?

MacBook Air M1 with an external Dell display, Magic Trackpad, and a split mechanical keyboard from UHK (Ultimate Hacking Keyboard).

camelCase or snake_case?

I default to whatever the language or codebase conventions are. Visually, I prefer snake case, though (and definitely spaces over tabs). ;)

What's one piece of advice you'd give to an aspiring developer?

One of the biggest differentiators I have found between good and great engineers is that the great ones possess a continuous desire to learn and grow. They view challenges as fun opportunities to expand their knowledge and skills, recognizing that they always have room for improvement. The corollary is that impostor syndrome is real and never goes away, so try not to be too hard on yourself along the way!

This post was the first of several new hire announcements in the months to come, so stay tuned and reach out if you want to work with Martin, Shayne, and myself!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/01/27/waspleau.html b/blog/2022/01/27/waspleau.html index e4e137d108..6deea1ae7e 100644 --- a/blog/2022/01/27/waspleau.html +++ b/blog/2022/01/27/waspleau.html @@ -19,13 +19,13 @@ - - + +
-

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

· 5 min read
Shayne Czyzewski

Hello, Waspleau

See Waspleau here! | See the code

We've built a dashboard powered by a job queue using Wasp!

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.

Measure all the metrics!

Hello, Waspleau!

Let’s face it - metrics are all around us. Wouldn’t it be great if there was a quick and easy way to build a nice-looking metrics dashboard from data pulled in by HTTP calls to many different sources, cache the data in-memory, and periodically update it via background jobs? Why yes, yes it would... so we made an example Wasp app called Waspleau that does just that!

Here is what it looks like live: https://waspleau.netlify.app/ There is also a screenshot at the top of this post for those who refrain from clicking on any unknown web links for fear of being Rickrolled. Respect.

“Show me the code”

So, what do we need to get started? First, we need a way to schedule and run jobs; for this, we decided to use Bull. Ok, let’s wire it up. This should be easy, right? We can add external NPM dependencies in our Wasp files like so:

main.wasp
app waspleau {
title: "Waspleau",

dependencies: [
("bull", "4.1.1"),
("axios", "^0.21.1")
]
}

But where do we declare our queue and processing callback functions in Wasp? Uh oh...

Sad

server.setupFn for queue setup

Thankfully, Waspleau can leverage a powerful and flexible hook supplied by Wasp called server.setupFn. This declares a JavaScript function that will be executed on server start. Yahoo! This means we can do things like the following:

main.wasp
app waspleau {
...

server: {
setupFn: import serverSetup from "@server/serverSetup.js"
}
}
src/server/serverSetup.js
import Queue from 'bull'

const queue = new Queue('waspleau', process.env.REDIS_URL || 'redis://127.0.0.1:6379',
{ defaultJobOptions: { removeOnComplete: true } }
)

queue.process('*', async (job) => { ... })

export default async () => {
// To initially populate the queue, we can do:
await queue.add({ ... }) // first run, one-off job
await queue.add({ ... }, { repeat: { cron: '*/10 * * * *' } }) // recurring job
}

Abstracting workers and job processing

Awesome, we can now enqueue and process background jobs, but how can we make it easy to create many different kinds of jobs and schedule them to run at different intervals? For Waspleau, we created our own type of worker object convention to help standardize and simplify adding more:

src/server/workers/template.js
const workerFunction = async (opts) => {
return [
{ name: 'Metric 1 name', value: 'foo', updatedAt: ... },
{ name: 'Metric 2 name', value: 'bar', updatedAt: ... },
]
}

export const workerTemplate = { name: 'Job Name', fn: workerFunction, schedule: '*/10 * * * *' }

With this workerFunction setup, we can return one or more metrics per worker type. Waspleau can easily use any module that exports this shape. Here is a real example from the demo that makes HTTP calls to GitHub’s API with Axios:

src/server/workers/github.js
import axios from 'axios'

const workerFunction = async (opts) => {
console.log('github.js workerFunction')

const now = Date.now()

try {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

return [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count, updatedAt: now },
{ name: 'Wasp GitHub Language', value: response.data.language, updatedAt: now },
{ name: 'Wasp GitHub Forks', value: response.data.forks, updatedAt: now },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues, updatedAt: now },
]
} catch (error) {
console.error(error)
return []
}
}

export const githubWorker = { name: 'GitHub API', fn: workerFunction, schedule: '*/10 * * * *' }

Note: Please see the actual serverSetup.js file for how we use this abstraction in practice.

Server → client

We now have jobs running and data updating at regular intervals, nice, but we still need a way to send that data down the wire. Here, we expose the in-memory data from our server.setupFn module so our queries can also use it:

main.wasp
...

query dashboard {
fn: import { refreshDashboardData } from "@server/dashboard.js"
}
src/server/dashboard.js
import { getDashboardData } from './serverSetup.js'

export const refreshDashboardData = async (_args, _context) => {
return getDashboardData()
}
src/server/serverSetup.js
...

const dashboardData = {} // This is updated in the queue process callback
export const getDashboardData = () => Object.values(dashboardData).flat()

From there, we can request it on the frontend in React components as usual and also set a one-minute client-side refetch interval just for good measure:

src/client/MainPage.js
...

const { data: dashboardData, isFetching, error } = useQuery(refreshDashboardData, null, { refetchInterval: 60 * 1000 })

...

Congratulations, let’s dance!

Whew, we did it! If you’d like to deploy your own customized version of this dashboard, please clone our repo and check out the Waspleau example README.md for tips on getting started. You can also check out our docs to dive deeper into anything.

Rickroll

Still got ya! :D

2022 is going to be exciting 🚀

While this functionality currently exists outside of Wasp, keep an eye on our roadmap as we head toward 1.0. We will be busy adding lots of great features to our Wasp DSL in the coming months that will supercharge your web development experience! Thanks for reading, and please feel free to connect with us in Discord about using Wasp on your next project.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

· 5 min read
Shayne Czyzewski

Hello, Waspleau

See Waspleau here! | See the code

We've built a dashboard powered by a job queue using Wasp!

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.

Measure all the metrics!

Hello, Waspleau!

Let’s face it - metrics are all around us. Wouldn’t it be great if there was a quick and easy way to build a nice-looking metrics dashboard from data pulled in by HTTP calls to many different sources, cache the data in-memory, and periodically update it via background jobs? Why yes, yes it would... so we made an example Wasp app called Waspleau that does just that!

Here is what it looks like live: https://waspleau-app-client.fly.dev/ There is also a screenshot at the top of this post for those who refrain from clicking on any unknown web links for fear of being Rickrolled. Respect.

“Show me the code”

So, what do we need to get started? First, we need a way to schedule and run jobs; for this, we decided to use Bull. Ok, let’s wire it up. This should be easy, right? We can add external NPM dependencies in our Wasp files like so:

main.wasp
app waspleau {
title: "Waspleau",

dependencies: [
("bull", "4.1.1"),
("axios", "^0.21.1")
]
}

But where do we declare our queue and processing callback functions in Wasp? Uh oh...

Sad

server.setupFn for queue setup

Thankfully, Waspleau can leverage a powerful and flexible hook supplied by Wasp called server.setupFn. This declares a JavaScript function that will be executed on server start. Yahoo! This means we can do things like the following:

main.wasp
app waspleau {
...

server: {
setupFn: import serverSetup from "@server/serverSetup.js"
}
}
src/server/serverSetup.js
import Queue from 'bull'

const queue = new Queue('waspleau', process.env.REDIS_URL || 'redis://127.0.0.1:6379',
{ defaultJobOptions: { removeOnComplete: true } }
)

queue.process('*', async (job) => { ... })

export default async () => {
// To initially populate the queue, we can do:
await queue.add({ ... }) // first run, one-off job
await queue.add({ ... }, { repeat: { cron: '*/10 * * * *' } }) // recurring job
}

Abstracting workers and job processing

Awesome, we can now enqueue and process background jobs, but how can we make it easy to create many different kinds of jobs and schedule them to run at different intervals? For Waspleau, we created our own type of worker object convention to help standardize and simplify adding more:

src/server/workers/template.js
const workerFunction = async (opts) => {
return [
{ name: 'Metric 1 name', value: 'foo', updatedAt: ... },
{ name: 'Metric 2 name', value: 'bar', updatedAt: ... },
]
}

export const workerTemplate = { name: 'Job Name', fn: workerFunction, schedule: '*/10 * * * *' }

With this workerFunction setup, we can return one or more metrics per worker type. Waspleau can easily use any module that exports this shape. Here is a real example from the demo that makes HTTP calls to GitHub’s API with Axios:

src/server/workers/github.js
import axios from 'axios'

const workerFunction = async (opts) => {
console.log('github.js workerFunction')

const now = Date.now()

try {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

return [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count, updatedAt: now },
{ name: 'Wasp GitHub Language', value: response.data.language, updatedAt: now },
{ name: 'Wasp GitHub Forks', value: response.data.forks, updatedAt: now },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues, updatedAt: now },
]
} catch (error) {
console.error(error)
return []
}
}

export const githubWorker = { name: 'GitHub API', fn: workerFunction, schedule: '*/10 * * * *' }

Note: Please see the actual serverSetup.js file for how we use this abstraction in practice.

Server → client

We now have jobs running and data updating at regular intervals, nice, but we still need a way to send that data down the wire. Here, we expose the in-memory data from our server.setupFn module so our queries can also use it:

main.wasp
...

query dashboard {
fn: import { refreshDashboardData } from "@server/dashboard.js"
}
src/server/dashboard.js
import { getDashboardData } from './serverSetup.js'

export const refreshDashboardData = async (_args, _context) => {
return getDashboardData()
}
src/server/serverSetup.js
...

const dashboardData = {} // This is updated in the queue process callback
export const getDashboardData = () => Object.values(dashboardData).flat()

From there, we can request it on the frontend in React components as usual and also set a one-minute client-side refetch interval just for good measure:

src/client/MainPage.js
...

const { data: dashboardData, isFetching, error } = useQuery(refreshDashboardData, null, { refetchInterval: 60 * 1000 })

...

Congratulations, let’s dance!

Whew, we did it! If you’d like to deploy your own customized version of this dashboard, please clone our repo and check out the Waspleau example README.md for tips on getting started. You can also check out our docs to dive deeper into anything.

Rickroll

Still got ya! :D

2022 is going to be exciting 🚀

While this functionality currently exists outside of Wasp, keep an eye on our roadmap as we head toward 1.0. We will be busy adding lots of great features to our Wasp DSL in the coming months that will supercharge your web development experience! Thanks for reading, and please feel free to connect with us in Discord about using Wasp on your next project.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/05/31/filip-intro.html b/blog/2022/05/31/filip-intro.html index d75afbb7ee..35a6619399 100644 --- a/blog/2022/05/31/filip-intro.html +++ b/blog/2022/05/31/filip-intro.html @@ -19,12 +19,12 @@ - - + +
-

Meet the team - Filip Sodić, Founding Engineer

· 6 min read
Matija Sosic

Welcome Filip!

Find Filip on GitHub.

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 immensely excited to welcome Filip, our latest Founding Software +

Meet the team - Filip Sodić, Founding Engineer

· 6 min read
Matija Sosic

Welcome Filip!

Find Filip on GitHub.

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 immensely excited to welcome Filip, our latest Founding Software Engineer! Filip is an experienced engineer and a passionate computer scientist - his two biggest passions are building compilers/designing programming languages and web development (what a lucky coincidence, right? @@ -78,7 +78,7 @@ projects because you think they aren’t ready yet. Good enough sometimes truly is good enough and things can often be considered done before you consider them done.

I still occasionally need to give this advice to myself :).

Lastly, where can people find or connect with you online?

GitHub: https://github.com/sodic

LinkedIn: https://www.linkedin.com/in/filipsodic/

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/06/01/gitpod-hackathon-guide.html b/blog/2022/06/01/gitpod-hackathon-guide.html index 5a6cfae846..e9d1b554af 100644 --- a/blog/2022/06/01/gitpod-hackathon-guide.html +++ b/blog/2022/06/01/gitpod-hackathon-guide.html @@ -19,13 +19,13 @@ - - + +
-

How to win a hackathon. Brief manual.

· 4 min read

Wasp app deploye to Gitpod

"All good thoughts and ideas mean nothing without the proper tools to achieve them."
>Jason Statham

TL;DR: Wasp allows you to build and deploy a full-stack JS web app with a single config file. Gitpod spins up fresh, automated developer environments in the cloud, in seconds. A perfect tandem to win a hackathon and enjoy free pizza even before other teams even started to set up their coding env and realized they need to update their node version.

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.

Intro:

Usually, every hackathon starts from similar activities:

  1. setting up a local dev environment, especially if all the team members use different operating systems. There are always issues with the SDK/packages/compiler, etc.
  2. building project backbone (folder structure, basic services, CRUD APIs, and so on).

Both of them are time-consuming, boring, and cause issues.

Dealing with routine might be frustrating

Thankfully, those issues can be avoided! Gitpod allows you to spin up a clean, already pre-set dev environment. And Wasp enables you to build a full-stack JS web app with a single config file (alongside your React and Node.js code). But first things first.

Pennywise luring into his openspace

Dev environment setup:

Gitpod spins up a bespoke dev environment in the cloud for any git branch (once you configured it for your project), on-demand. So you can start coding right away. Build, debug, commit and push your code in seconds, without any local SDK issues. After you’ve finished – you can host your app after a couple of clicks and share the project with your teammate. You can even make changes to the same project simultaneously, leveraging a pair programming approach.

Since Gitpod is a cloud-based workspace – spinning up a new application takes a couple of clicks.

  1. Fork https://github.com/gitpod-io/template-wasp and give it a meaningful name, e.g. “My Awesome Recipes App” -> this is now a repo for your new web app.
  2. In your newly created repo, check the Readme and click the “Open in Gitpod” button
  3. Login via Github
  4. Allow pop-ups
  5. That’s it! Enjoy your fresh cloud-based dev environment!

Pennywise luring to take part in hackathon

An optional thing might be enabling the “Share” option to make the app accessible from the external internet.

How to share a workspace

You can pick up one of the following IDE’s, switch between light/dark themes and you can even install all your favorite extensions.

Gitpod IDE types

So, eventually, the workflow can look like this: someone from the team forks the template repo and shares it with others. Teammates open this repo in Gitpod, creating their own dev branches.

Voila! 🥳

The whole team is ready to code in a matter of seconds. After the team is done with the development, someone can pull all the changes, share the project, and present it to the judges.

No need to fix local issues, ensure the Node version is aligned, or configure the deployment pipeline for DigitalOcean. Gitpod does all development preparations. The only thing the team has to do – is to implement the idea ASAP. And here Wasp comes into play!

Building project backbone:

Ok, we’ve successfully set up a shared dev environment. It’s time to create a production-ready web app with just a few lines of code. Based on your needs – you can declare separate pages, routes, database models, etc. - it’s super easy and intuitive!

The ideal case would be to:

  1. Check out the language overview: https://wasp-lang.dev/docs/general/language
  2. Follow a 20-minutes tutorial on how to build a To-Do app with Wasp: https://wasp-lang.dev/docs/tutorial/create

It may seem a bit inconvenient: why spend time on learning, when you already can start building something meaningful? The short answer is: time-saving. Wasp’s main point is to set you free from building time-consuming boilerplate. So even if you’ll spend half of an hour learning the basics – you’ll still be able to outrun other hackathon participants. While they will be copy-pasting CRUD API methods – you’ll be building business logic.

And 20 minutes is time well spent to become more productive. Setting up each team member's environment locally likely takes more than 20 minutes if you don't use Gitpod.

To wrap up:

We think that Wasp + Gitpod is a powerful toolset for speedrunning any hackathon. No matter how complex or ambitious your project is. If it’s built with Node and React – nothing can stop you from winning. Good luck, have fun, and enjoy that pizza 🍕!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How to win a hackathon. Brief manual.

· 4 min read

Wasp app deploye to Gitpod

"All good thoughts and ideas mean nothing without the proper tools to achieve them."
>Jason Statham

TL;DR: Wasp allows you to build and deploy a full-stack JS web app with a single config file. Gitpod spins up fresh, automated developer environments in the cloud, in seconds. A perfect tandem to win a hackathon and enjoy free pizza even before other teams even started to set up their coding env and realized they need to update their node version.

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.

Intro:

Usually, every hackathon starts from similar activities:

  1. setting up a local dev environment, especially if all the team members use different operating systems. There are always issues with the SDK/packages/compiler, etc.
  2. building project backbone (folder structure, basic services, CRUD APIs, and so on).

Both of them are time-consuming, boring, and cause issues.

Dealing with routine might be frustrating

Thankfully, those issues can be avoided! Gitpod allows you to spin up a clean, already pre-set dev environment. And Wasp enables you to build a full-stack JS web app with a single config file (alongside your React and Node.js code). But first things first.

Pennywise luring into his openspace

Dev environment setup:

Gitpod spins up a bespoke dev environment in the cloud for any git branch (once you configured it for your project), on-demand. So you can start coding right away. Build, debug, commit and push your code in seconds, without any local SDK issues. After you’ve finished – you can host your app after a couple of clicks and share the project with your teammate. You can even make changes to the same project simultaneously, leveraging a pair programming approach.

Since Gitpod is a cloud-based workspace – spinning up a new application takes a couple of clicks.

  1. Fork https://github.com/gitpod-io/template-wasp and give it a meaningful name, e.g. “My Awesome Recipes App” -> this is now a repo for your new web app.
  2. In your newly created repo, check the Readme and click the “Open in Gitpod” button
  3. Login via Github
  4. Allow pop-ups
  5. That’s it! Enjoy your fresh cloud-based dev environment!

Pennywise luring to take part in hackathon

An optional thing might be enabling the “Share” option to make the app accessible from the external internet.

How to share a workspace

You can pick up one of the following IDE’s, switch between light/dark themes and you can even install all your favorite extensions.

Gitpod IDE types

So, eventually, the workflow can look like this: someone from the team forks the template repo and shares it with others. Teammates open this repo in Gitpod, creating their own dev branches.

Voila! 🥳

The whole team is ready to code in a matter of seconds. After the team is done with the development, someone can pull all the changes, share the project, and present it to the judges.

No need to fix local issues, ensure the Node version is aligned, or configure the deployment pipeline for DigitalOcean. Gitpod does all development preparations. The only thing the team has to do – is to implement the idea ASAP. And here Wasp comes into play!

Building project backbone:

Ok, we’ve successfully set up a shared dev environment. It’s time to create a production-ready web app with just a few lines of code. Based on your needs – you can declare separate pages, routes, database models, etc. - it’s super easy and intuitive!

The ideal case would be to:

  1. Check out the language overview: https://wasp-lang.dev/docs/general/language
  2. Follow a 20-minutes tutorial on how to build a To-Do app with Wasp: https://wasp-lang.dev/docs/tutorial/create

It may seem a bit inconvenient: why spend time on learning, when you already can start building something meaningful? The short answer is: time-saving. Wasp’s main point is to set you free from building time-consuming boilerplate. So even if you’ll spend half of an hour learning the basics – you’ll still be able to outrun other hackathon participants. While they will be copy-pasting CRUD API methods – you’ll be building business logic.

And 20 minutes is time well spent to become more productive. Setting up each team member's environment locally likely takes more than 20 minutes if you don't use Gitpod.

To wrap up:

We think that Wasp + Gitpod is a powerful toolset for speedrunning any hackathon. No matter how complex or ambitious your project is. If it’s built with Node and React – nothing can stop you from winning. Good luck, have fun, and enjoy that pizza 🍕!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/06/15/jobs-feature-announcement.html b/blog/2022/06/15/jobs-feature-announcement.html index 59029a729a..da81608215 100644 --- a/blog/2022/06/15/jobs-feature-announcement.html +++ b/blog/2022/06/15/jobs-feature-announcement.html @@ -19,13 +19,13 @@ - - + +
-

Feature Announcement - Wasp Jobs

· 7 min read
Shayne Czyzewski

You get a job!

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.

Storytime

Storytime

Imagine you are working on the next unicorn SaaS web app and need to send a user an email, manipulate an uploaded image via an external API call, or recalculate some internal metrics every night. (Or, maybe you’re doing some fancy blockchain thing for that frothy investment multiple; :D whatever it is, just envision an operation that may take a significant amount of time and/or fail.) How would you implement this?

Spinning!

You wouldn’t want the server to delay sending its HTTP response until those are done (unless you are one of those people who love seeing the Mac spinning icon), so you'll need something out-of-band from the normal request-response flow. Even in an event-loop-based system like Node.js, just calling an async function isn't ideal since you will need to handle failures, retries, and throttling, amongst other concerns. And sometimes we need to schedule tasks to run in the future, or repeatedly, so we need a completely different toolset.

The typical solution here is to use a job queue of some kind. They are not impossible to set up, of course, but there is a fair amount of boilerplate involved, some operational expertise/overhead required, and moving from one system to another when you outgrow it is usually a challenge. These are the exact areas where we feel Wasp can provide real value, so we are happy to introduce Wasp Jobs to help out with this!

src/server/workers/github.js
import axios from 'axios'
import { upsertMetric } from './utils.js'

export async function workerFunction() {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

const metrics = [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count },
{ name: 'Wasp GitHub Language', value: response.data.language },
{ name: 'Wasp GitHub Forks', value: response.data.forks },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues },
]

await Promise.all(metrics.map(upsertMetric))

return metrics
}

Wasp allows you to write a regular async JavaScript function (like the one above that gathers GitHub metrics and stores them in the DB) and have it run within the context of a job queue system, which we call an executor. You can manually submit work to be done on the server, or specify a cron schedule to have your job automatically invoked. And, best of all, as we add more job executors in the future, you can change where it runs on a single line in your .wasp file.

Most jobs have a boss. Our first job executor is a... pg-boss. 😅

Eeek
Me trying to lay off the job-related puns. Ok, ok, I’ll quit. Ahhh!

In my prior life as a Ruby on Rails developer, the decision of how to implement jobs was pretty simple. You had Active Job at your disposal, and for backends, you would use something like Sidekiq or Delayed Job. In a similarly paved path, Python developers would have likely looked first to Celery.

In the JavaScript world, Bull is quite popular these days. However, we decided to use pg-boss, as it too provides persistence, delayed jobs, and schedules (plus many other features). But critically, pg-boss uses PostgreSQL instead of Redis (like Bull) for storage and coordination, and this was important since we did not want to introduce any new infrastructure dependencies to our existing production stack.

But isn’t a database as a queue an anti-pattern, you may ask? Well, historically I’d probably say yes. However, PostgreSQL 9.5 added SKIP LOCKED, which it specifically mentions can aid in avoiding lock contention with multiple consumer queue-like workloads [https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE]. So for the low-volume background job workloads that many apps have, we feel using a database as a queue is a great compromise and starting point for many users from a benefit vs. complexity perspective.

However, we will also continue to expand the number of job execution runtimes we support. Let us know in Discord what you’d like to see next!

Real Example - Updating Waspleau

If you are a regular reader of this blog (thank you, you deserve a raise! 😊), you may recall we created an example app of a metrics dashboard called Waspleau that used workers in the background to make periodic HTTP calls for data. In that example, we didn’t yet have access to recurring jobs in Wasp, so we used Bull for scheduled jobs instead. To set up our queue-related logic we had to have this huge setupFn wiring it all up; but now, we can remove all that code and simply use jobs instead! Here is what the new DSL looks like:

main.wasp
// A cron job for fetching GitHub stats
job getGithubStats {
executor: PgBoss,
perform: {
fn: import { workerFunction } from "@server/workers/github.js"
},
schedule: {
cron: "*/10 * * * *"
}
}

// A cron job to measure how long a webpage takes to load
job calcPageLoadTime {
executor: PgBoss,
perform: {
fn: import { workerFunction } from "@server/workers/loadTime.js"
},
schedule: {
cron: "*/5 * * * *",
args: {=json {
"url": "https://wasp-lang.dev",
"name": "wasp-lang.dev Load Time"
} json=}
}
}

And here is an example of how you can reference and invoke jobs on the server. Note: We did not even need to do this step since jobs with a schedule are automatically configured to run at the desired time.

src/server/serverSetup.js
/**
* These Jobs are automatically scheduled by Wasp.
* However, let's kick them off on server setup to ensure we have data right away.
*/
import { github } from '@wasp/jobs/getGithubStats.js'
import { loadTime } from '@wasp/jobs/calcPageLoadTime.js'

export default async function () {
await github.submit()
await loadTime.submit({
url: "https://wasp-lang.dev",
name: "wasp-lang.dev Load Time"
})
}

And voila, it is really that simple. Wasp takes care of setting up pg-boss and hooking up all your job callbacks, leaving you to focus on what matters- your own code. Here is a visual of what is happening behind the scenes:

Architecture

For those interested, check out the full diff here and weep with joy for all those boilerplate lines of code we fired! We were also able to ax Redis from our infrastructure!

Looks neat! What’s next?

First off, please check out our docs for Jobs. There, you will find all the info you need to start using them. Next, if you want to see the code for this example in full, you can find it here: https://github.com/wasp-lang/wasp/tree/release/examples/waspleau

In the future, we plan to add more job executors, including support for polyglot workers (imagine running your Python ML function from Wasp!). We are also open to any other ideas on how jobs can become more useful to you (like client-side access to server-side jobs, or client-side jobs using similar abstractions?). Let us know what you think!


Special thanks to Tim Jones for his hard work building an amazing OSS library, pg-boss, and for reviewing this post. Please consider supporting that project if it solves your needs!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Feature Announcement - Wasp Jobs

· 7 min read
Shayne Czyzewski

You get a job!

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.

Storytime

Storytime

Imagine you are working on the next unicorn SaaS web app and need to send a user an email, manipulate an uploaded image via an external API call, or recalculate some internal metrics every night. (Or, maybe you’re doing some fancy blockchain thing for that frothy investment multiple; :D whatever it is, just envision an operation that may take a significant amount of time and/or fail.) How would you implement this?

Spinning!

You wouldn’t want the server to delay sending its HTTP response until those are done (unless you are one of those people who love seeing the Mac spinning icon), so you'll need something out-of-band from the normal request-response flow. Even in an event-loop-based system like Node.js, just calling an async function isn't ideal since you will need to handle failures, retries, and throttling, amongst other concerns. And sometimes we need to schedule tasks to run in the future, or repeatedly, so we need a completely different toolset.

The typical solution here is to use a job queue of some kind. They are not impossible to set up, of course, but there is a fair amount of boilerplate involved, some operational expertise/overhead required, and moving from one system to another when you outgrow it is usually a challenge. These are the exact areas where we feel Wasp can provide real value, so we are happy to introduce Wasp Jobs to help out with this!

src/server/workers/github.js
import axios from 'axios'
import { upsertMetric } from './utils.js'

export async function workerFunction() {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

const metrics = [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count },
{ name: 'Wasp GitHub Language', value: response.data.language },
{ name: 'Wasp GitHub Forks', value: response.data.forks },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues },
]

await Promise.all(metrics.map(upsertMetric))

return metrics
}

Wasp allows you to write a regular async JavaScript function (like the one above that gathers GitHub metrics and stores them in the DB) and have it run within the context of a job queue system, which we call an executor. You can manually submit work to be done on the server, or specify a cron schedule to have your job automatically invoked. And, best of all, as we add more job executors in the future, you can change where it runs on a single line in your .wasp file.

Most jobs have a boss. Our first job executor is a... pg-boss. 😅

Eeek
Me trying to lay off the job-related puns. Ok, ok, I’ll quit. Ahhh!

In my prior life as a Ruby on Rails developer, the decision of how to implement jobs was pretty simple. You had Active Job at your disposal, and for backends, you would use something like Sidekiq or Delayed Job. In a similarly paved path, Python developers would have likely looked first to Celery.

In the JavaScript world, Bull is quite popular these days. However, we decided to use pg-boss, as it too provides persistence, delayed jobs, and schedules (plus many other features). But critically, pg-boss uses PostgreSQL instead of Redis (like Bull) for storage and coordination, and this was important since we did not want to introduce any new infrastructure dependencies to our existing production stack.

But isn’t a database as a queue an anti-pattern, you may ask? Well, historically I’d probably say yes. However, PostgreSQL 9.5 added SKIP LOCKED, which it specifically mentions can aid in avoiding lock contention with multiple consumer queue-like workloads [https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE]. So for the low-volume background job workloads that many apps have, we feel using a database as a queue is a great compromise and starting point for many users from a benefit vs. complexity perspective.

However, we will also continue to expand the number of job execution runtimes we support. Let us know in Discord what you’d like to see next!

Real Example - Updating Waspleau

If you are a regular reader of this blog (thank you, you deserve a raise! 😊), you may recall we created an example app of a metrics dashboard called Waspleau that used workers in the background to make periodic HTTP calls for data. In that example, we didn’t yet have access to recurring jobs in Wasp, so we used Bull for scheduled jobs instead. To set up our queue-related logic we had to have this huge setupFn wiring it all up; but now, we can remove all that code and simply use jobs instead! Here is what the new DSL looks like:

main.wasp
// A cron job for fetching GitHub stats
job getGithubStats {
executor: PgBoss,
perform: {
fn: import { workerFunction } from "@server/workers/github.js"
},
schedule: {
cron: "*/10 * * * *"
}
}

// A cron job to measure how long a webpage takes to load
job calcPageLoadTime {
executor: PgBoss,
perform: {
fn: import { workerFunction } from "@server/workers/loadTime.js"
},
schedule: {
cron: "*/5 * * * *",
args: {=json {
"url": "https://wasp-lang.dev",
"name": "wasp-lang.dev Load Time"
} json=}
}
}

And here is an example of how you can reference and invoke jobs on the server. Note: We did not even need to do this step since jobs with a schedule are automatically configured to run at the desired time.

src/server/serverSetup.js
/**
* These Jobs are automatically scheduled by Wasp.
* However, let's kick them off on server setup to ensure we have data right away.
*/
import { github } from '@wasp/jobs/getGithubStats.js'
import { loadTime } from '@wasp/jobs/calcPageLoadTime.js'

export default async function () {
await github.submit()
await loadTime.submit({
url: "https://wasp-lang.dev",
name: "wasp-lang.dev Load Time"
})
}

And voila, it is really that simple. Wasp takes care of setting up pg-boss and hooking up all your job callbacks, leaving you to focus on what matters- your own code. Here is a visual of what is happening behind the scenes:

Architecture

For those interested, check out the full diff here and weep with joy for all those boilerplate lines of code we fired! We were also able to ax Redis from our infrastructure!

Looks neat! What’s next?

First off, please check out our docs for Jobs. There, you will find all the info you need to start using them. Next, if you want to see the code for this example in full, you can find it here: https://github.com/wasp-lang/wasp/tree/release/examples/waspleau

In the future, we plan to add more job executors, including support for polyglot workers (imagine running your Python ML function from Wasp!). We are also open to any other ideas on how jobs can become more useful to you (like client-side access to server-side jobs, or client-side jobs using similar abstractions?). Let us know what you think!


Special thanks to Tim Jones for his hard work building an amazing OSS library, pg-boss, and for reviewing this post. Please consider supporting that project if it solves your needs!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-future.html b/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-future.html index d4c9ff1a8a..1135f2c382 100644 --- a/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-future.html +++ b/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-future.html @@ -19,13 +19,13 @@ - - + +
-

ML code generation vs. coding by hand - what we think programming is going to look like

· 11 min read
Matija Sosic

We are working on a config language / DSL for building web apps that integrates with React & Node.js. A number of times we've been asked “Why are you bothering creating a new language for web app development? Isn’t Github Copilot* soon going to be generating all the code for developers anyhow?”.

This is on our take on the situation and what we think things might look like in the future.

Trending post!

This post was trending on HackerNews - you can see the discussion here.

Why (ML) code generation?

In order to make development faster, we came up with IDE autocompletion - e.g. if you are using React and start typing componentDid, IDE will automatically offer to complete it to componentDidMount() or componentDidLoad(). Besides saving keystrokes, maybe even more valuable is being able to see what methods/properties are available to us within a current scope. IDE being aware of the project structure and code hierarchy also makes refactoring much easier.

Although that’s already great, how do we take it to the next level? Traditional IDE support is based on rules written by humans and if we e.g. wanted to make IDE capable of implementing common functions for us, there would be just too many of them to catalogize and maintain by hand.

If there was only a way for a computer to analyze all the code we’ve written so far and learn by itself how to autocomplete our code and what to do about humanity in general, instead of us doing all the hard work ...

Delicious and moist cake aside, we actually have this working! Thanks to the latest advances in machine learning, IDEs can now do some really cool things like proposing the full implementation of a function, based on its name and the accompanying comments:

Copilot example - text sentiment
GitHub Copilot generating a whole function body based on its signature and the comments on top of it.

This is pretty amazing! The example above is powered by Github Copilot - it’s essentially a neural network trained on a huge amount of publicly available code. I will not get into the technical details of how it works under the hood, but there are lots of great articles covering the science behind it.

Seeing this, questions arise - what does this mean for the future of programming? Is this just IDE autocompletion on steroids or something more? Do we need to keep bothering with manually writing code, if we can just type in the comments what we want and that’s it?

Who maintains the code once it’s generated?

When thinking about how ML code generation affects the overall development process, there is one thing to consider that often doesn’t immediately spring to mind when looking at the impressive Copilot examples.

note

For the purposes of this post, I will not delve into the questions of code quality, security, legal & privacy issues, pricing, and others of similar character that are often brought up in these early days of ML code generation. Let’s just assume all this is sorted out and see what happens next.

The question is - what happens with the code once it is generated? Who is responsible for it and who will maintain and refactor it in the future?

Devs still need to maintain generated code

Although ML code generation helps with getting the initial code written, it cannot do much beyond that - if that code is to be maintained and changed in the future (and if anyone uses the product, it is), the developer still needs to fully own and understand it.

Imagine all we had was an assembly language, but IDE completion worked really well for it, and you could say “implement a function that sorts an array, ascending” and it would produce the required code perfectly. Would that still be something you’d like to return to in the future once you need to change your sort to descending 😅 ?

In other words, it means Copilot and similar solutions do not reduce the code complexity nor the amount of knowledge required to build features, they just help write the initial code faster, and bring the knowledge/examples closer to the code (which is really helpful). If a developer accepts the generated code blindly, they are just creating tech debt and pushing it forward.

Meet the big A - Abstraction 👆

If Github Copilot and others cannot solve all our troubles of learning how to code and understanding in detail how session management via JWT works, what can?

Abstraction - that’s how programmers have been dealing with the code repetition and reducing complexity for decades - by creating libraries, frameworks, and languages. It is how we advanced from vanilla JS and direct DOM manipulation to jQuery and finally to UI libraries such as React and Vue.

Introducing abstractions inevitably means giving up on a certain amount of power and flexibility (e.g. when summing numbers in Python you don’t get to exactly specify which CPU registers are going to be used for it), but the point is that, if done right, you don’t need nor want such power in the majority of the cases.

Abstraction equals less responsibility
What Uncle Ben actually meant: avoiding responsibility is the main benefit of abstraction! (Peter totally missed the point, unfortunately, and became Spiderman instead of learning how to code)

The only way not to be responsible for a piece of code is that it doesn’t exist in the first place.

Because as soon as pixels on the screen change their color it’s something you have to worry about, and that is why the main benefit of all frameworks, languages, etc. is less code == less decisions == less responsibility.

The only way to have less code is to make less decisions and provide fewer details to the computer on how to do a certain task - ideally, we’d just state what we want and we wouldn’t even care about how it is done, as long as it’s within the time/memory/cost boundaries we have (so we might need to state those as well).

Let’s take a look at the very common (and everyone’s favorite) feature in the world of web apps - authentication (yaay ☠️ 🔫)! The typical code for it will look something like this:

Auth on the backend in Node.js - example
import jwt from 'jsonwebtoken'
import SecurePassword from 'secure-password'
import util from 'util'

import prisma from '../dbClient.js'
import { handleRejection } from '../utils.js'
import config from '../config.js'

const jwtSign = util.promisify(jwt.sign)
const jwtVerify = util.promisify(jwt.verify)

const JWT_SECRET = config.auth.jwtSecret

export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options)
export const verify = (token) => jwtVerify(token, JWT_SECRET)

const auth = handleRejection(async (req, res, next) => {
const authHeader = req.get('Authorization')
if (!authHeader) {
return next()
}

if (authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7, authHeader.length)

let userIdFromToken
try {
userIdFromToken = (await verify(token)).id
} catch (error) {
if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) {
return res.status(401).send()
} else {
throw error
}
}

const user = await prisma.user.findUnique({ where: { id: userIdFromToken } })
if (!user) {
return res.status(401).send()
}

const { password, ...userView } = user

req.user = userView
} else {
return res.status(401).send()
}

next()
})

const SP = new SecurePassword()

export const hashPassword = async (password) => {
const hashedPwdBuffer = await SP.hash(Buffer.from(password))
return hashedPwdBuffer.toString("base64")
}

export const verifyPassword = async (hashedPassword, password) => {
try {
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
} catch (error) {
console.error(error)
return false
}
}

And this is just a portion of the backend code (and for the username & password method only)! As you can see, we have quite a lot of flexibility here and get to do/specify things like:

  • choose the implementation method for auth (e.g. session or JWT-based)
  • choose the exact npm packages we want to use for the token (if going with JWT) and password management
  • parse the auth header and specify for each value (Authorization, Bearer, …) how to respond
  • choose the return code (e.g. 401, 403) for each possible outcome
  • choose how the password is decoded/encoded (base64)

On one hand, it’s really cool to have that level of control and flexibility in our code, but on the other hand, it’s quite a lot of decisions (== mistakes) to be made, especially for something as common as authentication!

If somebody later asks “so why exactly did you choose secure-password npm package, or why exactly base64 encoding?” it’s something we should probably answer with something else rather than “well, there was that SO post from 2012 that seemed pretty legit, it had almost 50 upvotes. Hmm, can’t find it now though. Plus, it has ‘secure’ in the name, that sounds good, right?

Another thing to keep in mind is that we should also track how things change over time, and make sure that after a couple of years we’re still using the best practices and that the packages are regularly updated.

If we try to apply the principles from above (less code, less detailed instructions, stating what we want instead of how it needs to be done), the code for auth might look something like this:

auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/dashboard"
}

Based on this, the computer/compiler could take care of all the stuff mentioned above, and then depending on the level of abstraction, provide some sort of interface (e.g. form components, or functions) to “hook” in with our own e.g. React/Node.js code (btw this is how it actually works in Wasp).

We don’t need to care what exact packages or encryption methods are used beneath the hood - it is the responsibility we trust with the authors and maintainers of the abstraction layer, just like we trust that Python knows the best how to sum two numbers on the assembly level and that it is kept in sync with the latest advancements in the field. The same happens when we rely on the built-in data structures or count on the garbage collector to manage our program’s memory well.

But my beautiful generated codez 😿💻! What happens with it then?

Don’t worry, it’s all still here and you can generate all the code you wish! The main point to understand here is that ML code generation and framework/language development complement rather than replace each other and are here to stay, which is ultimately a huge win for the developer community - they will keep making our lives easier and allow us to do more fun stuff (instead of implementing auth or CRUD API for the n-th time)!

I see the evolution here as a cycle (or an upward spiral in fact, but that’s beyond my drawing capabilities):

  1. language/framework exists, is mainstream, and a lot of people use it
  2. patterns start emerging (e.g. implementing auth, or making an API call) → ML captures them, offers via autocomplete
  3. some of those patterns mature and become stable → candidates for abstraction
  4. new, more abstract, language/framework emerges
  5. back to step 1.

Language evolution lifecycle
It’s the circle of (language) life, and it moves us all - Ingonyama nengw' enamabala, …

Conclusion

This means we are winning on both sides - when the language is mainstream we can benefit from ML code generation, helping us write the code faster. On the other hand, when the patterns of code we don’t want to repeat/deal with emerge and become stable we get a whole new language or framework that allows us to write even less code and care about fewer implementation details!

Fizz Buzz with Copilot - stop
The future is now, old man.

*Not to be biased, there are also other solutions offering similar functionality - e.g. TabNine, Webstorm has its own, Kite, GPT Code Clippy (OSS attempt) et al., but Github Copilot recently made the biggest splash.

Writing that informed this post

Thanks to the reviewers

Jeremy Howard, Maxi Contieri, Mario Kostelac, Vladimir Blagojevic, Ido Nov, Krystian Safjan, Favour Kelvin, Filip Sodic, Shayne Czyzewski and Martin Sosic - thank you for your generous comments, ideas and suggestions! You made this post better and made sure I don't go overboard with memes :).

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

ML code generation vs. coding by hand - what we think programming is going to look like

· 11 min read
Matija Sosic

We are working on a config language / DSL for building web apps that integrates with React & Node.js. A number of times we've been asked “Why are you bothering creating a new language for web app development? Isn’t Github Copilot* soon going to be generating all the code for developers anyhow?”.

This is on our take on the situation and what we think things might look like in the future.

Trending post!

This post was trending on HackerNews - you can see the discussion here.

Why (ML) code generation?

In order to make development faster, we came up with IDE autocompletion - e.g. if you are using React and start typing componentDid, IDE will automatically offer to complete it to componentDidMount() or componentDidLoad(). Besides saving keystrokes, maybe even more valuable is being able to see what methods/properties are available to us within a current scope. IDE being aware of the project structure and code hierarchy also makes refactoring much easier.

Although that’s already great, how do we take it to the next level? Traditional IDE support is based on rules written by humans and if we e.g. wanted to make IDE capable of implementing common functions for us, there would be just too many of them to catalogize and maintain by hand.

If there was only a way for a computer to analyze all the code we’ve written so far and learn by itself how to autocomplete our code and what to do about humanity in general, instead of us doing all the hard work ...

Delicious and moist cake aside, we actually have this working! Thanks to the latest advances in machine learning, IDEs can now do some really cool things like proposing the full implementation of a function, based on its name and the accompanying comments:

Copilot example - text sentiment
GitHub Copilot generating a whole function body based on its signature and the comments on top of it.

This is pretty amazing! The example above is powered by Github Copilot - it’s essentially a neural network trained on a huge amount of publicly available code. I will not get into the technical details of how it works under the hood, but there are lots of great articles covering the science behind it.

Seeing this, questions arise - what does this mean for the future of programming? Is this just IDE autocompletion on steroids or something more? Do we need to keep bothering with manually writing code, if we can just type in the comments what we want and that’s it?

Who maintains the code once it’s generated?

When thinking about how ML code generation affects the overall development process, there is one thing to consider that often doesn’t immediately spring to mind when looking at the impressive Copilot examples.

note

For the purposes of this post, I will not delve into the questions of code quality, security, legal & privacy issues, pricing, and others of similar character that are often brought up in these early days of ML code generation. Let’s just assume all this is sorted out and see what happens next.

The question is - what happens with the code once it is generated? Who is responsible for it and who will maintain and refactor it in the future?

Devs still need to maintain generated code

Although ML code generation helps with getting the initial code written, it cannot do much beyond that - if that code is to be maintained and changed in the future (and if anyone uses the product, it is), the developer still needs to fully own and understand it.

Imagine all we had was an assembly language, but IDE completion worked really well for it, and you could say “implement a function that sorts an array, ascending” and it would produce the required code perfectly. Would that still be something you’d like to return to in the future once you need to change your sort to descending 😅 ?

In other words, it means Copilot and similar solutions do not reduce the code complexity nor the amount of knowledge required to build features, they just help write the initial code faster, and bring the knowledge/examples closer to the code (which is really helpful). If a developer accepts the generated code blindly, they are just creating tech debt and pushing it forward.

Meet the big A - Abstraction 👆

If Github Copilot and others cannot solve all our troubles of learning how to code and understanding in detail how session management via JWT works, what can?

Abstraction - that’s how programmers have been dealing with the code repetition and reducing complexity for decades - by creating libraries, frameworks, and languages. It is how we advanced from vanilla JS and direct DOM manipulation to jQuery and finally to UI libraries such as React and Vue.

Introducing abstractions inevitably means giving up on a certain amount of power and flexibility (e.g. when summing numbers in Python you don’t get to exactly specify which CPU registers are going to be used for it), but the point is that, if done right, you don’t need nor want such power in the majority of the cases.

Abstraction equals less responsibility
What Uncle Ben actually meant: avoiding responsibility is the main benefit of abstraction! (Peter totally missed the point, unfortunately, and became Spiderman instead of learning how to code)

The only way not to be responsible for a piece of code is that it doesn’t exist in the first place.

Because as soon as pixels on the screen change their color it’s something you have to worry about, and that is why the main benefit of all frameworks, languages, etc. is less code == less decisions == less responsibility.

The only way to have less code is to make less decisions and provide fewer details to the computer on how to do a certain task - ideally, we’d just state what we want and we wouldn’t even care about how it is done, as long as it’s within the time/memory/cost boundaries we have (so we might need to state those as well).

Let’s take a look at the very common (and everyone’s favorite) feature in the world of web apps - authentication (yaay ☠️ 🔫)! The typical code for it will look something like this:

Auth on the backend in Node.js - example
import jwt from 'jsonwebtoken'
import SecurePassword from 'secure-password'
import util from 'util'

import prisma from '../dbClient.js'
import { handleRejection } from '../utils.js'
import config from '../config.js'

const jwtSign = util.promisify(jwt.sign)
const jwtVerify = util.promisify(jwt.verify)

const JWT_SECRET = config.auth.jwtSecret

export const sign = (id, options) => jwtSign({ id }, JWT_SECRET, options)
export const verify = (token) => jwtVerify(token, JWT_SECRET)

const auth = handleRejection(async (req, res, next) => {
const authHeader = req.get('Authorization')
if (!authHeader) {
return next()
}

if (authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7, authHeader.length)

let userIdFromToken
try {
userIdFromToken = (await verify(token)).id
} catch (error) {
if (['TokenExpiredError', 'JsonWebTokenError', 'NotBeforeError'].includes(error.name)) {
return res.status(401).send()
} else {
throw error
}
}

const user = await prisma.user.findUnique({ where: { id: userIdFromToken } })
if (!user) {
return res.status(401).send()
}

const { password, ...userView } = user

req.user = userView
} else {
return res.status(401).send()
}

next()
})

const SP = new SecurePassword()

export const hashPassword = async (password) => {
const hashedPwdBuffer = await SP.hash(Buffer.from(password))
return hashedPwdBuffer.toString("base64")
}

export const verifyPassword = async (hashedPassword, password) => {
try {
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
} catch (error) {
console.error(error)
return false
}
}

And this is just a portion of the backend code (and for the username & password method only)! As you can see, we have quite a lot of flexibility here and get to do/specify things like:

  • choose the implementation method for auth (e.g. session or JWT-based)
  • choose the exact npm packages we want to use for the token (if going with JWT) and password management
  • parse the auth header and specify for each value (Authorization, Bearer, …) how to respond
  • choose the return code (e.g. 401, 403) for each possible outcome
  • choose how the password is decoded/encoded (base64)

On one hand, it’s really cool to have that level of control and flexibility in our code, but on the other hand, it’s quite a lot of decisions (== mistakes) to be made, especially for something as common as authentication!

If somebody later asks “so why exactly did you choose secure-password npm package, or why exactly base64 encoding?” it’s something we should probably answer with something else rather than “well, there was that SO post from 2012 that seemed pretty legit, it had almost 50 upvotes. Hmm, can’t find it now though. Plus, it has ‘secure’ in the name, that sounds good, right?

Another thing to keep in mind is that we should also track how things change over time, and make sure that after a couple of years we’re still using the best practices and that the packages are regularly updated.

If we try to apply the principles from above (less code, less detailed instructions, stating what we want instead of how it needs to be done), the code for auth might look something like this:

auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/dashboard"
}

Based on this, the computer/compiler could take care of all the stuff mentioned above, and then depending on the level of abstraction, provide some sort of interface (e.g. form components, or functions) to “hook” in with our own e.g. React/Node.js code (btw this is how it actually works in Wasp).

We don’t need to care what exact packages or encryption methods are used beneath the hood - it is the responsibility we trust with the authors and maintainers of the abstraction layer, just like we trust that Python knows the best how to sum two numbers on the assembly level and that it is kept in sync with the latest advancements in the field. The same happens when we rely on the built-in data structures or count on the garbage collector to manage our program’s memory well.

But my beautiful generated codez 😿💻! What happens with it then?

Don’t worry, it’s all still here and you can generate all the code you wish! The main point to understand here is that ML code generation and framework/language development complement rather than replace each other and are here to stay, which is ultimately a huge win for the developer community - they will keep making our lives easier and allow us to do more fun stuff (instead of implementing auth or CRUD API for the n-th time)!

I see the evolution here as a cycle (or an upward spiral in fact, but that’s beyond my drawing capabilities):

  1. language/framework exists, is mainstream, and a lot of people use it
  2. patterns start emerging (e.g. implementing auth, or making an API call) → ML captures them, offers via autocomplete
  3. some of those patterns mature and become stable → candidates for abstraction
  4. new, more abstract, language/framework emerges
  5. back to step 1.

Language evolution lifecycle
It’s the circle of (language) life, and it moves us all - Ingonyama nengw' enamabala, …

Conclusion

This means we are winning on both sides - when the language is mainstream we can benefit from ML code generation, helping us write the code faster. On the other hand, when the patterns of code we don’t want to repeat/deal with emerge and become stable we get a whole new language or framework that allows us to write even less code and care about fewer implementation details!

Fizz Buzz with Copilot - stop
The future is now, old man.

*Not to be biased, there are also other solutions offering similar functionality - e.g. TabNine, Webstorm has its own, Kite, GPT Code Clippy (OSS attempt) et al., but Github Copilot recently made the biggest splash.

Writing that informed this post

Thanks to the reviewers

Jeremy Howard, Maxi Contieri, Mario Kostelac, Vladimir Blagojevic, Ido Nov, Krystian Safjan, Favour Kelvin, Filip Sodic, Shayne Czyzewski and Martin Sosic - thank you for your generous comments, ideas and suggestions! You made this post better and made sure I don't go overboard with memes :).

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joining.html b/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joining.html index 8d9d8585ae..4c36c2e724 100644 --- a/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joining.html +++ b/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joining.html @@ -19,13 +19,13 @@ - - + +
-

How to communicate why your startup is worth joining

· 31 min read
Vasili Shynkarenka

Except for a handful of companies who send people to Mars or develop AGI, most startups don’t seem to offer a good reason to join them. You go to their websites and all you see is vague, baseless, overly generic mission-schmission/values-schvalues HR nonsense that supposedly should turn you into a raving fan of whatever they’re doing and make you hit that “Join” button until their servers crash. Well…

Some people think that’s because most startups aren’t worth joining. I disagree. This argument generalizes one’s own reasons for joining a startup onto every other human being out there, which is unlikely to be true. I think most startups, no matter how ordinary, do have a reason to join them; a good reason; even many good reasons — they just fail to communicate them well. They’re like a shy nerd on Tinder with an empty bio and no profile pic: a kind, intelligent, and thoughtful human being who, unfortunately, will be ruthlessly swiped left — not because he’s a bad match but because his profile doesn’t show why he’s a good one.

Visually, this “Tinder profile problem” looks like this:

Illustration of candidates not seeing why to join a startup

Now, look what would happen if a startup communicated a bit better. Suddenly, our candidates could see a reason to join. If the reason is good, they might even swipe right.

Illustration of candidates seeing one reason to join a startup

But most startups have many good reasons to join them. If only they communicated well, the outcome would be something like this:

Illustration of candidates seeing many reasons to join; one candidate already running for it

Now, you’re probably wondering just what exactly those reasons are.

Here’s a rough list:

  1. The founders are interesting / fun / smart / human / you name it
  2. The team is great
  3. The culture is amazing
  4. The business is doing well

However, if you just copy this list and paste it on your jobs page, you will accomplish nothing. The candidates will never believe you. What you need to do instead is to supply them with a system of concretes (facts) from which their minds will form these abstract conclusions.

For example:

  • Instead of declaring that “the founders are reflective, thoughtful, and persistent,” show them how so, like Sarah from Canny does by writing comprehensive year-in-review blog posts for four years in a row.
  • Instead of proclaiming that “the founders are humble and can have fun,” show them how so, like Michael from Fibery did by becoming a hero of this hilarious page. (No businessy founder would ever agree to make this public. Michael did.)
  • Instead of purporting that “the team is great” or “you’ll work alongside very smart people” (God, I hate that one!), show them who exactly those people are, as PostHog does here and Wasp does here and here.

In the rest of the post, I’ll go through the four broad reasons to join a startup one by one and show real-life examples of communicating them well. In the end, I will explain how these four reasons, communicated well, fuse into two compelling messages that will interest any candidate.

One last thing. For the sake of clarity and comprehension, I will write in the second person. Instead of saying “candidates would never believe them,” I will say “you would never believe them.” It’s much easier to read and understand.

Possible reasons why your startup is worth joining, and how to communicate them well

1. FOUNDERS — or, the founders are interesting / fun / smart / human / you name it

Most startups have curious, interesting, ambitious, terribly smart founders; the kind most of us would love to work for if we had a chance. Sadly, only a few leverage this asset. In most cases, all you get is a small round pic with a fancy title and a few abstract, high-level sentences that cause no excitement whatsoever. What a shame!

How Canny commmunicates who their founders are

Founder Stories blog category

The first notable thing Canny does is the Founder Stories category in their blog. By quickly skimming the posts, you can understand that Sarah and Andrew (the founders):

If they just pinned this list of virtues to their Jobs page, you would never believe them. Instead, Sarah and Andrew show what actions they take, how they work, how they think, how they live — and you make up their own mind about what kind of people Sarah and Andrew are from seeing all that. The difference is enormous.

Note their writing style. They don’t claim to be know-it-alls with titles like “How to bootstrap your startup.” Instead, they write “How we Bootstrapped our SaaS Startup to Ramen Profitability.” They cover only what they know instead of overgeneralizing. This shows both expertise and humility.

A screenshot of Canny's Founder Stories blog category

Personal Instagram

The second thing Sarah and Andrew do well to communicate who they are is their Instagram. They don’t post glamorous keynote appearances, as many entrepreneurs do. They share the actual day-to-day working life — both the fun and the struggle. It gives you a good idea of what they’re after in life. (Not keynotes.) That’s why it works, and that’s why people love it.

A photo from Sarah and Andrew's personal Instagram

Side note: Sarah explains how she develops the Canny brand in this post. If you want to build a good one, give it a read. She also wrote about how they attract top talent. You can read it here.

How Fibery communicates who their founder is

Startup Diary blog post series

While you can get a pretty good idea of Michael (the founder) from the hilarious “Remote” page Fibery shipped last year, his Startup Diary post series offers an even better insight into his soul. In these monthly posts, Michael honestly shares everything that’s going on with Fibery, including the good, the bad, and the ugly: firing people for poor performance, losing important customers, and failing to reach product-market fit. The fact that he’s already written 45 of those (as of Aug 2022) is also telling. And he’s not a native English speaker. If he can do that, why can’t you?

A screenshot of Fibery's Startup Diary blog category

Crazy challenges

Besides writing the Startup Diary, Michael also embarks on crazy challenges like writing 100 posts about products. Only a passionate, driven person would commit to such a thing. You cannot help but respect him for it. (Before this challenge, he wrote 100 Medium posts in 100 days in 2018. You can read them here. Just scroll a few screens to reach the old stuff.)

A screenshot of Fibery's 100 posts about products blog category

If you look carefully, you’ll notice that Michael’s thinking about building a company is different from Sarah’s. For example, he despises the gentle, soothing “Oh don’t worry that it didn’t work out; you did such a good work!” approach, which is ubiquitous in the modern startup world. Instead, he states that dissatisfaction leads to progress, referring to the famous “Not quite my tempo” scene from Whiplash. Does that make you like him more than Sarah?

It depends. If you believe that being soft and balanced is better, you’ll go with Sarah; if you believe that real progress comes only from working yourself to the bone, you’ll go with Michael (or Elon). The important thing is that both founders have their own, unique viewpoints of how things should be done, and that they communicate these viewpoints as-is instead of chopping their legs off to fit the latest Procrustean fad.

In-depth, original blog posts about the industry

Some entrepreneurs say that doing a startup is like “jumping off a cliff and building your wings on the way down.” Some of it might be true. But if you want reasonable people to jump with you, you better tell them that you have a degree in engineering and know how to assemble wings in a free fall. Otherwise, the only team you’ll recruit is a suicide squad looking for a splashy hit.

To communicate his expertise, Michael writes in-depth, original, theoretical posts about the nature of knowledge management and organizational productivity. These posts are gems, both literally and metaphorically. (They’re filed under the Gems category in the Fibery blog.)

For example:

After reading these articles, you understand not only that Michael really knows how to build wings while falling off the cliff, but that he has already jumped a few times. (Prior to Fibery, Michael had worked on knowledge management for more than a decade. He also had built a successful project management software, Targetprocess.) You know that he’s an expert who can be trusted.

Interestingly, even though Michael writes differently from Sarah, they both leverage what they’re good at. Sarah does not try to produce treatises on software development philosophy, and Michael doesn’t gush out with his personal learnings from building a startup. That, I think, is the right way to do it.

How PostHog communicates who their founders are

PostHog’s founders James and Tim don’t write 100 posts in 100 days or run a personal Instagram. But they’ve come up with something else to communicate what kind of people they are. And it’s something unique.

Well-written, concise bio

First, both founders have decent profiles in the company handbook. These bios are short, clear, and humane. They’re also very specific. Where else have you seen the name of the CEO’s cat?

A screenshot of James Hawkins' bio in the PostHog Handbook

Personal README files

Second, both James and Tim have an extensive README file (one, two) on how to work with them. These files give you an insight into their productivity habits, interests, and quirks. In fact, after reading them, you will likely have a better idea of the founders than you’d usually get from working at a company for a month!

For instance, James’s file has sections like:

  • Short bio. Includes very specific details like: “I tend to work 9am to 5pm with an hour for lunch, then I have a gap to have dinner with my family, then 9pm to around 11pm ish.”
  • Very clear areas of responsibility. No need to wonder what the hell the CEO is doing anymore!
  • Quirks. These are remarkably humble and open-minded, like:
    • “If I haven’t responded to something that you’ve sent me, that’s probably because I’ve read it and don’t feel particularly strongly - so just make a call on what to do if you don’t hear back in a reasonable time frame.”
    • “I’m a little disorganized. I compensate for this by making sure the teams I work on have this skill. Often I think this actually helps me prioritize the things that really matter.”
    • Explaining these quirks is an ingenious move. Besides explaining how to work with James, this section communicates that he’s profoundly self-aware and willing to accept and leverage his weaknesses. These qualities are very rare and incredibly valuable.
  • What I value. In stark contrast to most HR nonsense, these values are very clear, very specific, and written in English rather than HRese. (I just came up with this term: it means “legalese but for HR.”) Here are two examples:
    • “Proactivity. Do not ask me for permission to do things - I wouldn’t have hired you if I didn’t trust you. I’d rather 9 things get done well and 1 thing I disagree with than we don’t get anything done at all.”
    • “Directness impresses me. If you don’t like something please just say so. It makes for much healthier relationships.”

In addition to that, there’s also: How I can help you, How you can help me, My goals until end December 2022 (very specific!), Personal strategy, Execution todo (including “1 bike ride a week”!) and Archived todo.

In summary, this README page is a gem. I wish more founders had them.

A screenshot of James Hawkins' README in the PostHog Handbook

How we at Wasp communicate who our founders are

“Who we are” section of every job description page

Matija and Martin (the founders of Wasp) embedded a concise description of who they are right into each job description page in Notion. They knew that this is the first company artifact many candidates will see. So they saved candidates time and effort on digging up who the hell started Wasp.

Note the language and substance of this list. When you read it, you immediately get a sense of who Matija and Martin are as people — fun, easygoing, no-corporate-bullshit kinda guys. Now imagine it said something “more normal,” like: “The company was founded by seasoned entrepreneurs…” What impression would that make?

A screenshot of Wasp's job description page

2. TEAM — or, the team is great

It is startling how little most startups tell you about their teams. Often all you get is a chessboard of faces and titles, which gives you no idea who these people are as people or how working with them will feel like. Given how crucial a reason “great team” is for most candidates, improving how you communicate it seems like a low-hanging fruit.

How Canny communicates who is on their team

Decent team page

The Canny’s difference starts with a team page. It has a dense summary of who each team member is as a person and includes high-quality, lively photos of everybody.

A screenshot of Canny's Team page

Look how specific those bios are. In most cases, all you get here is a generic “developer” or “marketer” without any personal details. Bios of robots, not people. No wonder nothing comes to mind, except perhaps for Agent Smith. But Canny’s bios are different. When you read them, you can actually imagine the person! They’re Neos in the world of Smiths.

Remarkable “Why work at Canny” blog post

From there, it gets only better. Canny’s chief weapon for explaining their team is a blog post, the “Why work at Canny” blog post. Sarah wrote it back in the summer of 2021. It is full of quotes from team members and photos of their workdays and vacations. Real photos of real people. No wonder the comments section under the post abounds with raving fans willing to join the team straight away!

A screenshot of comments under the Canny's Why work at Canny blog post

Perhaps the best thing about this post is how little work it takes to create one. I imagine that collecting the data took some time, but the actual writing (it’s an 11-min read) took no more than a week. A week of work for a candidate magnet of such tremendous power? Sounds like a deal.

P.s. Sarah writes a lot more about their team in her yearly review posts, but I decided not to elaborate on those for the sake of clarity. You can check them out here: year 1, year 2, year 3, and year 4.

How Fibery communicates who is on their team

Weird About Us page

Unlike Canny and PostHog’s, Fibery’s About Us page doesn’t reveal much info about each team member. You will find no bios or README files there. But it clearly tells you one thing: the team is a bunch of weirdos. So, if weird is your thing, you’ll be attracted to Fibery like a moth to a flame. (Side note: Fibery managed to clearly explain their vision in one paragraph. This is rare.)

A screenshot of Fibery's About Us page

I’ve already mentioned Michael’s Startup Diary monthly blog series. What I didn’t say is that each post communicates something about the team: who did what that month, random Slack posts (links, quotes, tweets, and images), etc. If someone new joined that month, Michael writes a few paragraphs explaining who that person is, where they come from, what they’re going to do at Fibery, and even attaches a photo. Like Chris.

A screenshot of Fibery's Startup Diary blog post

How PostHog communicates who is on their team

Team section in the company handbook

At PostHog, every team member has a well-written, few-paragraphs-long bio and a stylish illustration on the Team section of the PostHog’s Handbook. (Which is a work of art worthy of its own blog post, by the way.) Many team members have their own README files, like the founders do. Check out Lottie Coxon’s, PostHog’s Graphic Designer’s README here, and some others here and here. Even a quick read through these bios and READMEs gives you a good idea of who PostHog has on board.

A screenshot of PostHog's team section in the handbook

Another screenshot of PostHog's team section in the handbook

Day-in-life videos from employees

In addition to bios and READMEs, PostHog has a day-in-life video of Lottie, their graphic designer. It communicates a lot more information about what kind of person she is and how working at PostHog feels like than her bio. I wish they had more of those.

A screenshot from PostHog's graphic designer day-in-life video

Finally, PostHog’s handbook offers two more sections where candidates can learn even more about the team: Culture and Team structure. All are worth a read, and each tells you something new about the company and the team, nurturing your liking and respect for these people. Definitely worth stealing.

How we at Wasp communicate who is on our team

“Meet the team” blog posts

To help candidates understand who they will be working with, we at Wasp write a blog post about each new hire:

The posts are brief enough to be read in one sitting. Yet, they are very informative. Basically, each post is an interview, presented as an article. We hope they give candidates a good idea of who they'll be spending half of their waking time with.

A screenshot of Wasp's Meet the team blog post

3. CULTURE — or, the culture is amazing

While researchers still argue about the ultimate definition, most of us understand culture as “what working here feels like” and/or “how we do things here.” We also understand how crucial it is for those looking for work. It seems glaringly obvious that startups should work hard on communicating their culture. Yet, most companies don’t. Or, even worse, they flood their websites with meaningless HR fluff, which only scares interesting people away. In short, communicating culture well is another low-hanging fruit waiting to be picked.

How Canny communicates their culture

Canny does an outstanding job at communicating their culture. The primary tool they employ is, once again, their blog. (Note how multifunctional it is: founders, expertise, team, and now culture.) The posts in the Founder Stories category convey very well what working at Canny feels like. Here are a few examples.

“Why work at Canny” blog post

I’ll risk repeating myself, but this post so beautifully explains Canny’s culture that I couldn’t resist. It mentions why and how they work remotely, how they do team retreats (with photos and a video from Lisbon!), and how they had fun together playing weird Zoom games when travel was not an option due to Covid.

Pay attention to the imagery. It communicates a lot more information than any lengthy, elaborate description would. Indeed, a picture is often worth a thousand words.

A photo of Canny's two team members hacking in Denver

“Lessons from a year of team retreats” blog post

Instead of saying that “team is our priority” or “we invest in our people,” Sarah shows what they’ve done to support their team.

Again, note how specific the imagery is.

A photo from Canny's Lessons from a year of team retreats blog post

Interestingly, Sarah’s post isn’t framed as “hey we do many team retreats, we’re awesome, come work for us.” If they wrote that, the reader would feel uneasy. They would sense bragging. That’s why the explicit message in the post is what Canny learned doing team retreats, not that they’ve done many. This explicit message, however, implies that they indeed have done many retreats! It sends a message that Canny cares for their employees without explicitly saying so. This is what true mastery looks like.

“The end of our digital nomad journey” blog post

Although this post describes Sarah and Andrew’s personal nomad experience, Sarah managed to reveal Canny’s culture through it. To do that, she described how the team worked on Canny during those nomad years. She also wrote about their communication struggles, routines, and a lot more. And, again, look at how effectively her seemingly imperfect screenshots and photos transmit the vibe!

A photo from Canny's The end of our digital nomad journey blog post

Another photo from Canny's The end of our digital nomad journey blog post

How Fibery communicates their culture

While Fibery’s culture is different from Canny’s, they also communicate it well. Their primary tool is a weird, quirky website full of special projects that give you a sense of how they do things at Fibery and what working there feels like.

Anxiety page

The first project is Fibery’s /anxiety page. Launched in 2019, it mocks every serious enterprise software out there with puns like “Yet another collaboration tool” as the page title, “Mistake” as a sign-up button text, and, my favorite, “Try—Suffer—Quit” page structure.

A screenshot of Fibery's /anxiety page

One day three years ago, someone submitted this page to Hacker News. The post surged to the top of the frontpage, stayed there for many hours, and got 705 upvotes and 145 comments from people all over the world relating to Fibery’s culture. Why? Because it felt real.

Here’s a glimpse of what people wrote in the comments:

A screenshot of Hacker News comments on Fibery's /anxiety page

Another screenshot of Hacker News comments on Fibery's /anxiety page

Remote page

The second special project Fibery did to communicate their culture is the /remote page. It shows what working from home is really like. It’s the funniest thing I’ve ever seen done by a software startup. (Have you ever seen a CEO being licked by a dog?) It also shows how the Fibery team works and even how they use Fibery to build Fibery. Like Canny’s “Lessons from a year of team retreats” blog post, it does so implicitly. A true masterpiece.

Weird, humorous site

Broadly, the whole site screams that Fibery is a place for misfits, rebels, and trouble makers; the place where such people will be valued and will feel like home; the place built around brutal honesty and spicy humor.

The “What (non-)customers say” section is worth a mention. Over my nine years in startups, I haven’t seen a site that a) lists bad customer reviews; and b) uses 💩 emoji as a filter. Again, this is telling. It says a lot about who they are as people: humble, real, and fond of humor.

A screenshot of Fibery's About Us page, What non-customers say section

How PostHog communicates their culture

Comprehensive company handbook covering all-things culture

PostHog’s way of communicating their culture is the most explicit of all four examples, yet very effective. Their primary tool is the PostHog Handbook, which covers virtually every aspect of what working at PostHog feels like: interviews, onboarding, training, management, communication, and even firing. (They call it offboarding.)

The handbook goes all the way up to the high-level strategy, which is very clear. Notably, PostHog’s strategy section not only puts forth ambitious goals but actually explains how exactly the company will get there.

The values section is very specific; perhaps the most specific I’ve ever seen. PostHog does not merely list their values as meaningless abstractions but supports them with evidence. Some values have many paragraphs of examples demonstrating how the team follows them.

A screenshot of the Values section in the PostHog's handbook

They also have a specific Culture page with a 5-minute video from the CEO explaining how they designed PostHog for remote work from day one, which nicely complements the text.

A screenshot from James Hawkins's video

In summary, if Canny’s weapon of choice is the blog and Fibery’s is the website, then PostHog’s is definitely the handbook. It’s a work of art.

How we at Wasp communicate our culture

Easygoing vibe from memes, copy, and imagery

Unlike Posthog, we at Wasp don’t (yet) have a dedicated Culture page. We are too small for that. But that doesn’t stop us from showing what working at Wasp feels like. We just use different tools.

Our Twitter, blog, and monthly updates abound with memes, GIFs, and hilarious imagery. Plus, we write them in a humorous, lighthearted, easygoing style. By just scrolling through these things for a few minutes, candidates can understand that we aren’t some corporate bros. And if they like working on interesting things while having fun, they won’t help but feel an inkling to reach out.

A funny image from Wasp's blog post about GitHub Copilot

A photo of Wasp's team packing t-shirts for users

4. PROGRESS — or, the business is doing well

When you just closed an $80 million Series B or signed Facebook as a customer, communicating progress is easy. You just state these facts. However, most companies need to attract great people way before Series B. In fact, it is these very people who’re going to get you there. As most startups are secretive about how things are going, communicating that things are going somehow — no matter how negligible your progress in contrast to the big guys — becomes quite an advantage. It immediately de-risks the opportunity in the candidate’s eyes. So, if EXPERTISE is about convincing candidates that you know how to build the wings, PROGRESS is about showing them the half-built carcass on your way down. Both are important if you want great people to jump off the cliff with you.

How Canny communicates their progress

To give candidates a sense that things are moving, that this company is not some long slog but a place where progress is made every day, that they can become a part of something that’s growing and, therefore, can grow themselves, to do all that, Canny does two things.

“Year in review” blog posts

The first one is their “Year in review” blog post series. Such comprehensive, thoughtful reviews are rare in the startup world. What is even rarer is when these posts span over four consecutive years. It sends a message that the founders are persistent and devoted to making this company successful.

Below are all Canny’s year-in-review posts in a sequential order:

A screenshot of Canny's Year in review blog post

Important revenue milestones blog posts

In addition to year-in-review posts, Sarah writes about hitting notable revenue milestones. Like with yearly reviews, such transparency is rare. It attracts attention, causes liking, and builds trust.

For example:

A screenshot of Canny's How we built a $1m ARR SaaS startup blog post

Short tweets with progress summary

Finally, Sarah occasionally tweets short summaries of their progress, like this one. These tweets work like ads. Over time, a candidate’s brain fuses them into a broader idea like “Canny is growing” or “Canny is doing well.” Then, once a candidate decides to change jobs, it nudges the candidate to consider Canny.

A screenshot of Sarah’s tweet with progress update

How Fibery communicates their progress

Startup Diary blog posts

The most notable thing Fibery does to communicate their progress is the Startup Diary blog posts series written by the founder, Michael, every month, for the past 45 months. It’s the longest series of monthly updates I know. In these posts, Michael honestly shares everything that’s going on with the company: the good, the bad, and the ugly.

Below are just a few examples, selected by me. You can study all Fibery’s monthly updates here.

  • #2 Slow September 2018 — Fibery startup progress in September 2018. Slow month with not so many news. First positive feedback. Company name selection.
  • #6 Planning Private Beta in January 2019 — Fibery startup progress in January 2019: Private beta goals, selecting a market positioning (hard), apps re-design.
  • #10 Burn in May 2019 — Several people burned out, new features are delivered, public release will be sooner (we hope) (despite ill fortune).
  • #16 Crazy November 2019 — Fibery 1.0 is silently launched. Silence is hard to keep. HackerNews front page. Twitter madness. 3000 registered accounts.
  • #17 Fragmented December 2019 — Public announcements moved to January. +Lena. Tons of feedback. First money! Hype is over. We consider rising a ~$4M round.
  • #35 Raised $3.1M in July 2021 — TLDR: We closed $3.1M seed round. Building a second brain for teams. Fibery mission. Building in Public. Automation rules. Documents and Rich Text history.
  • #36 20k MRR in August 2021 — Special Startup Diary edition. 20k MRR & 15 new customers! +Chris. +Sales agency. 4 case studies. Airtable integration & notify people action.
  • ($30K MRR) #42 Connecting the dots in April 2022 — TLDR: 🇺🇦 Ukrainian war affected our performance. $30K MRR 🐌. 69 reviews in G2 ❤️. Marketing for customer-built products is hard 🥉. 12 customer stories 👻. 2 hours downtime 🥲. New navigation ⛵️. My Space 🔒.

Imagine a candidate who is considering two or more similar startups. Guess what might convince them to go with Fibery? Progress. Or, more exactly, an understanding that Fibery is persistently making progress and, therefore, has a decent chance to become successful. Delivered through these very updates.

Last year, Michael (Fibery’s CEO) started writing year-in-review posts too. I didn’t mention them because there’s just one post for now. You can read his 2021 review here.

Open Startup page with metrics

The second tool that Fibery employs to share their progress is the /open-startup page. Like monthly updates, it gives candidates a good idea of how the business is doing. This understanding, however, comes from a different source: pure numbers. And numbers often speak louder than words.

A screenshot of Fibery's Open startup page

How PostHog communicates their progress

Story page in the handbook

In the PostHog’s handbook, they have a page called Story. It succinctly shows the milestones the company has hit so far. For each milestone, they offer a clear and concise explanation of what happened, sometimes no longer than a sentence. As a result, candidates can get a good idea of how things are going in less than a minute. That’s something to aspire to.

Here’s the section titles:

  • Jan 2020: The start
  • Feb 2020: Launch
  • Apr 2020: $3M Seed round
  • May 2020: First 1,000 users
  • Oct 2020: Billions of events supported
  • Nov 2020: Building a platform
  • Dec 2020: $9M Series A
  • Jun 2021: $15M Series B
  • Sep 2021: Product Market fit achieved for PostHog Scale

A screenshot of PostHog's Story page

How we at Wasp communicate our progress

Blog posts covering big milestones (YC, $1.5m seed)

For each milestone, Matija and Martin (Wasp founders) write a blog post describing not only what they accomplished but also how they did it.

For example, when Wasp got into YC, they didn’t just post the news on Twitter. They wrote a blog about their journey to Y Combinator. It got thousands of views.

Same with fundraising. When Wasp closed a $1.5m seed, Matija documented and shared their fundraising learnings in a blog post. It ended up on the HN frontpage. (Incidentally, this post communicates something important about the founders. It takes persistence to run 250+ meetings in 98 days.)

A screenshot of Wasp's fundraising learnings blog post

Monthly newsletter with updates

To keep the momentum, Matija also writes a monthly newsletter. It’s similar to Michael’s Startup Diary in substance, but has a different style. Wasp style. (Which, again, communicates our culture.)

Like PostHog’s Story page, Wasp’s monthly updates give candidates a bird’s eye view over everything that’s happened in the past two years. To anyone interested in connecting the dots, this page is a gem.

A screenshot of Wasp's monthly newsletter archives

So, why should people join your startup?

The founders are interesting / fun / smart / human / you name it

The team is great

The culture is amazing

The business is doing well

By communicating all these reasons well, what Canny, Fibery, PostHog, and (we hope!) Wasp really end up transmitting is two powerful messages:

  • The company is likely to succeed
  • Working there will be awesome

These two messages are the real answer to “why people should join your company.” The trick, however, and the reason why I wrote this post, is that you can only transmit them indirectly. You can’t say “our founders are great.” You need to provide candidates with many-many facts about the founders, which their minds will then fuse into this abstract conclusion. Ditto for expertise, team, culture, and progress. Eventually, these first-level abstractions will blend into still broader ones: “the company is likely to succeed” and “working there will be awesome.”

Thus, there’s no single, ultimate answer to “why people should join your company.” There’s only a complex system of concrete, specific units of information from which candidates make the answer themselves. In other words, you can’t teach them why your company is likely to succeed and why working here will be awesome. But you can outline the facts and let them learn for themselves. I hope this post shows how to do that outlining well, and I hope you will apply this knowledge to bring talented people onboard and build great things.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How to communicate why your startup is worth joining

· 31 min read
Vasili Shynkarenka

Except for a handful of companies who send people to Mars or develop AGI, most startups don’t seem to offer a good reason to join them. You go to their websites and all you see is vague, baseless, overly generic mission-schmission/values-schvalues HR nonsense that supposedly should turn you into a raving fan of whatever they’re doing and make you hit that “Join” button until their servers crash. Well…

Some people think that’s because most startups aren’t worth joining. I disagree. This argument generalizes one’s own reasons for joining a startup onto every other human being out there, which is unlikely to be true. I think most startups, no matter how ordinary, do have a reason to join them; a good reason; even many good reasons — they just fail to communicate them well. They’re like a shy nerd on Tinder with an empty bio and no profile pic: a kind, intelligent, and thoughtful human being who, unfortunately, will be ruthlessly swiped left — not because he’s a bad match but because his profile doesn’t show why he’s a good one.

Visually, this “Tinder profile problem” looks like this:

Illustration of candidates not seeing why to join a startup

Now, look what would happen if a startup communicated a bit better. Suddenly, our candidates could see a reason to join. If the reason is good, they might even swipe right.

Illustration of candidates seeing one reason to join a startup

But most startups have many good reasons to join them. If only they communicated well, the outcome would be something like this:

Illustration of candidates seeing many reasons to join; one candidate already running for it

Now, you’re probably wondering just what exactly those reasons are.

Here’s a rough list:

  1. The founders are interesting / fun / smart / human / you name it
  2. The team is great
  3. The culture is amazing
  4. The business is doing well

However, if you just copy this list and paste it on your jobs page, you will accomplish nothing. The candidates will never believe you. What you need to do instead is to supply them with a system of concretes (facts) from which their minds will form these abstract conclusions.

For example:

  • Instead of declaring that “the founders are reflective, thoughtful, and persistent,” show them how so, like Sarah from Canny does by writing comprehensive year-in-review blog posts for four years in a row.
  • Instead of proclaiming that “the founders are humble and can have fun,” show them how so, like Michael from Fibery did by becoming a hero of this hilarious page. (No businessy founder would ever agree to make this public. Michael did.)
  • Instead of purporting that “the team is great” or “you’ll work alongside very smart people” (God, I hate that one!), show them who exactly those people are, as PostHog does here and Wasp does here and here.

In the rest of the post, I’ll go through the four broad reasons to join a startup one by one and show real-life examples of communicating them well. In the end, I will explain how these four reasons, communicated well, fuse into two compelling messages that will interest any candidate.

One last thing. For the sake of clarity and comprehension, I will write in the second person. Instead of saying “candidates would never believe them,” I will say “you would never believe them.” It’s much easier to read and understand.

Possible reasons why your startup is worth joining, and how to communicate them well

1. FOUNDERS — or, the founders are interesting / fun / smart / human / you name it

Most startups have curious, interesting, ambitious, terribly smart founders; the kind most of us would love to work for if we had a chance. Sadly, only a few leverage this asset. In most cases, all you get is a small round pic with a fancy title and a few abstract, high-level sentences that cause no excitement whatsoever. What a shame!

How Canny commmunicates who their founders are

Founder Stories blog category

The first notable thing Canny does is the Founder Stories category in their blog. By quickly skimming the posts, you can understand that Sarah and Andrew (the founders):

If they just pinned this list of virtues to their Jobs page, you would never believe them. Instead, Sarah and Andrew show what actions they take, how they work, how they think, how they live — and you make up their own mind about what kind of people Sarah and Andrew are from seeing all that. The difference is enormous.

Note their writing style. They don’t claim to be know-it-alls with titles like “How to bootstrap your startup.” Instead, they write “How we Bootstrapped our SaaS Startup to Ramen Profitability.” They cover only what they know instead of overgeneralizing. This shows both expertise and humility.

A screenshot of Canny's Founder Stories blog category

Personal Instagram

The second thing Sarah and Andrew do well to communicate who they are is their Instagram. They don’t post glamorous keynote appearances, as many entrepreneurs do. They share the actual day-to-day working life — both the fun and the struggle. It gives you a good idea of what they’re after in life. (Not keynotes.) That’s why it works, and that’s why people love it.

A photo from Sarah and Andrew's personal Instagram

Side note: Sarah explains how she develops the Canny brand in this post. If you want to build a good one, give it a read. She also wrote about how they attract top talent. You can read it here.

How Fibery communicates who their founder is

Startup Diary blog post series

While you can get a pretty good idea of Michael (the founder) from the hilarious “Remote” page Fibery shipped last year, his Startup Diary post series offers an even better insight into his soul. In these monthly posts, Michael honestly shares everything that’s going on with Fibery, including the good, the bad, and the ugly: firing people for poor performance, losing important customers, and failing to reach product-market fit. The fact that he’s already written 45 of those (as of Aug 2022) is also telling. And he’s not a native English speaker. If he can do that, why can’t you?

A screenshot of Fibery's Startup Diary blog category

Crazy challenges

Besides writing the Startup Diary, Michael also embarks on crazy challenges like writing 100 posts about products. Only a passionate, driven person would commit to such a thing. You cannot help but respect him for it. (Before this challenge, he wrote 100 Medium posts in 100 days in 2018. You can read them here. Just scroll a few screens to reach the old stuff.)

A screenshot of Fibery's 100 posts about products blog category

If you look carefully, you’ll notice that Michael’s thinking about building a company is different from Sarah’s. For example, he despises the gentle, soothing “Oh don’t worry that it didn’t work out; you did such a good work!” approach, which is ubiquitous in the modern startup world. Instead, he states that dissatisfaction leads to progress, referring to the famous “Not quite my tempo” scene from Whiplash. Does that make you like him more than Sarah?

It depends. If you believe that being soft and balanced is better, you’ll go with Sarah; if you believe that real progress comes only from working yourself to the bone, you’ll go with Michael (or Elon). The important thing is that both founders have their own, unique viewpoints of how things should be done, and that they communicate these viewpoints as-is instead of chopping their legs off to fit the latest Procrustean fad.

In-depth, original blog posts about the industry

Some entrepreneurs say that doing a startup is like “jumping off a cliff and building your wings on the way down.” Some of it might be true. But if you want reasonable people to jump with you, you better tell them that you have a degree in engineering and know how to assemble wings in a free fall. Otherwise, the only team you’ll recruit is a suicide squad looking for a splashy hit.

To communicate his expertise, Michael writes in-depth, original, theoretical posts about the nature of knowledge management and organizational productivity. These posts are gems, both literally and metaphorically. (They’re filed under the Gems category in the Fibery blog.)

For example:

After reading these articles, you understand not only that Michael really knows how to build wings while falling off the cliff, but that he has already jumped a few times. (Prior to Fibery, Michael had worked on knowledge management for more than a decade. He also had built a successful project management software, Targetprocess.) You know that he’s an expert who can be trusted.

Interestingly, even though Michael writes differently from Sarah, they both leverage what they’re good at. Sarah does not try to produce treatises on software development philosophy, and Michael doesn’t gush out with his personal learnings from building a startup. That, I think, is the right way to do it.

How PostHog communicates who their founders are

PostHog’s founders James and Tim don’t write 100 posts in 100 days or run a personal Instagram. But they’ve come up with something else to communicate what kind of people they are. And it’s something unique.

Well-written, concise bio

First, both founders have decent profiles in the company handbook. These bios are short, clear, and humane. They’re also very specific. Where else have you seen the name of the CEO’s cat?

A screenshot of James Hawkins' bio in the PostHog Handbook

Personal README files

Second, both James and Tim have an extensive README file (one, two) on how to work with them. These files give you an insight into their productivity habits, interests, and quirks. In fact, after reading them, you will likely have a better idea of the founders than you’d usually get from working at a company for a month!

For instance, James’s file has sections like:

  • Short bio. Includes very specific details like: “I tend to work 9am to 5pm with an hour for lunch, then I have a gap to have dinner with my family, then 9pm to around 11pm ish.”
  • Very clear areas of responsibility. No need to wonder what the hell the CEO is doing anymore!
  • Quirks. These are remarkably humble and open-minded, like:
    • “If I haven’t responded to something that you’ve sent me, that’s probably because I’ve read it and don’t feel particularly strongly - so just make a call on what to do if you don’t hear back in a reasonable time frame.”
    • “I’m a little disorganized. I compensate for this by making sure the teams I work on have this skill. Often I think this actually helps me prioritize the things that really matter.”
    • Explaining these quirks is an ingenious move. Besides explaining how to work with James, this section communicates that he’s profoundly self-aware and willing to accept and leverage his weaknesses. These qualities are very rare and incredibly valuable.
  • What I value. In stark contrast to most HR nonsense, these values are very clear, very specific, and written in English rather than HRese. (I just came up with this term: it means “legalese but for HR.”) Here are two examples:
    • “Proactivity. Do not ask me for permission to do things - I wouldn’t have hired you if I didn’t trust you. I’d rather 9 things get done well and 1 thing I disagree with than we don’t get anything done at all.”
    • “Directness impresses me. If you don’t like something please just say so. It makes for much healthier relationships.”

In addition to that, there’s also: How I can help you, How you can help me, My goals until end December 2022 (very specific!), Personal strategy, Execution todo (including “1 bike ride a week”!) and Archived todo.

In summary, this README page is a gem. I wish more founders had them.

A screenshot of James Hawkins' README in the PostHog Handbook

How we at Wasp communicate who our founders are

“Who we are” section of every job description page

Matija and Martin (the founders of Wasp) embedded a concise description of who they are right into each job description page in Notion. They knew that this is the first company artifact many candidates will see. So they saved candidates time and effort on digging up who the hell started Wasp.

Note the language and substance of this list. When you read it, you immediately get a sense of who Matija and Martin are as people — fun, easygoing, no-corporate-bullshit kinda guys. Now imagine it said something “more normal,” like: “The company was founded by seasoned entrepreneurs…” What impression would that make?

A screenshot of Wasp's job description page

2. TEAM — or, the team is great

It is startling how little most startups tell you about their teams. Often all you get is a chessboard of faces and titles, which gives you no idea who these people are as people or how working with them will feel like. Given how crucial a reason “great team” is for most candidates, improving how you communicate it seems like a low-hanging fruit.

How Canny communicates who is on their team

Decent team page

The Canny’s difference starts with a team page. It has a dense summary of who each team member is as a person and includes high-quality, lively photos of everybody.

A screenshot of Canny's Team page

Look how specific those bios are. In most cases, all you get here is a generic “developer” or “marketer” without any personal details. Bios of robots, not people. No wonder nothing comes to mind, except perhaps for Agent Smith. But Canny’s bios are different. When you read them, you can actually imagine the person! They’re Neos in the world of Smiths.

Remarkable “Why work at Canny” blog post

From there, it gets only better. Canny’s chief weapon for explaining their team is a blog post, the “Why work at Canny” blog post. Sarah wrote it back in the summer of 2021. It is full of quotes from team members and photos of their workdays and vacations. Real photos of real people. No wonder the comments section under the post abounds with raving fans willing to join the team straight away!

A screenshot of comments under the Canny's Why work at Canny blog post

Perhaps the best thing about this post is how little work it takes to create one. I imagine that collecting the data took some time, but the actual writing (it’s an 11-min read) took no more than a week. A week of work for a candidate magnet of such tremendous power? Sounds like a deal.

P.s. Sarah writes a lot more about their team in her yearly review posts, but I decided not to elaborate on those for the sake of clarity. You can check them out here: year 1, year 2, year 3, and year 4.

How Fibery communicates who is on their team

Weird About Us page

Unlike Canny and PostHog’s, Fibery’s About Us page doesn’t reveal much info about each team member. You will find no bios or README files there. But it clearly tells you one thing: the team is a bunch of weirdos. So, if weird is your thing, you’ll be attracted to Fibery like a moth to a flame. (Side note: Fibery managed to clearly explain their vision in one paragraph. This is rare.)

A screenshot of Fibery's About Us page

I’ve already mentioned Michael’s Startup Diary monthly blog series. What I didn’t say is that each post communicates something about the team: who did what that month, random Slack posts (links, quotes, tweets, and images), etc. If someone new joined that month, Michael writes a few paragraphs explaining who that person is, where they come from, what they’re going to do at Fibery, and even attaches a photo. Like Chris.

A screenshot of Fibery's Startup Diary blog post

How PostHog communicates who is on their team

Team section in the company handbook

At PostHog, every team member has a well-written, few-paragraphs-long bio and a stylish illustration on the Team section of the PostHog’s Handbook. (Which is a work of art worthy of its own blog post, by the way.) Many team members have their own README files, like the founders do. Check out Lottie Coxon’s, PostHog’s Graphic Designer’s README here, and some others here and here. Even a quick read through these bios and READMEs gives you a good idea of who PostHog has on board.

A screenshot of PostHog's team section in the handbook

Another screenshot of PostHog's team section in the handbook

Day-in-life videos from employees

In addition to bios and READMEs, PostHog has a day-in-life video of Lottie, their graphic designer. It communicates a lot more information about what kind of person she is and how working at PostHog feels like than her bio. I wish they had more of those.

A screenshot from PostHog's graphic designer day-in-life video

Finally, PostHog’s handbook offers two more sections where candidates can learn even more about the team: Culture and Team structure. All are worth a read, and each tells you something new about the company and the team, nurturing your liking and respect for these people. Definitely worth stealing.

How we at Wasp communicate who is on our team

“Meet the team” blog posts

To help candidates understand who they will be working with, we at Wasp write a blog post about each new hire:

The posts are brief enough to be read in one sitting. Yet, they are very informative. Basically, each post is an interview, presented as an article. We hope they give candidates a good idea of who they'll be spending half of their waking time with.

A screenshot of Wasp's Meet the team blog post

3. CULTURE — or, the culture is amazing

While researchers still argue about the ultimate definition, most of us understand culture as “what working here feels like” and/or “how we do things here.” We also understand how crucial it is for those looking for work. It seems glaringly obvious that startups should work hard on communicating their culture. Yet, most companies don’t. Or, even worse, they flood their websites with meaningless HR fluff, which only scares interesting people away. In short, communicating culture well is another low-hanging fruit waiting to be picked.

How Canny communicates their culture

Canny does an outstanding job at communicating their culture. The primary tool they employ is, once again, their blog. (Note how multifunctional it is: founders, expertise, team, and now culture.) The posts in the Founder Stories category convey very well what working at Canny feels like. Here are a few examples.

“Why work at Canny” blog post

I’ll risk repeating myself, but this post so beautifully explains Canny’s culture that I couldn’t resist. It mentions why and how they work remotely, how they do team retreats (with photos and a video from Lisbon!), and how they had fun together playing weird Zoom games when travel was not an option due to Covid.

Pay attention to the imagery. It communicates a lot more information than any lengthy, elaborate description would. Indeed, a picture is often worth a thousand words.

A photo of Canny's two team members hacking in Denver

“Lessons from a year of team retreats” blog post

Instead of saying that “team is our priority” or “we invest in our people,” Sarah shows what they’ve done to support their team.

Again, note how specific the imagery is.

A photo from Canny's Lessons from a year of team retreats blog post

Interestingly, Sarah’s post isn’t framed as “hey we do many team retreats, we’re awesome, come work for us.” If they wrote that, the reader would feel uneasy. They would sense bragging. That’s why the explicit message in the post is what Canny learned doing team retreats, not that they’ve done many. This explicit message, however, implies that they indeed have done many retreats! It sends a message that Canny cares for their employees without explicitly saying so. This is what true mastery looks like.

“The end of our digital nomad journey” blog post

Although this post describes Sarah and Andrew’s personal nomad experience, Sarah managed to reveal Canny’s culture through it. To do that, she described how the team worked on Canny during those nomad years. She also wrote about their communication struggles, routines, and a lot more. And, again, look at how effectively her seemingly imperfect screenshots and photos transmit the vibe!

A photo from Canny's The end of our digital nomad journey blog post

Another photo from Canny's The end of our digital nomad journey blog post

How Fibery communicates their culture

While Fibery’s culture is different from Canny’s, they also communicate it well. Their primary tool is a weird, quirky website full of special projects that give you a sense of how they do things at Fibery and what working there feels like.

Anxiety page

The first project is Fibery’s /anxiety page. Launched in 2019, it mocks every serious enterprise software out there with puns like “Yet another collaboration tool” as the page title, “Mistake” as a sign-up button text, and, my favorite, “Try—Suffer—Quit” page structure.

A screenshot of Fibery's /anxiety page

One day three years ago, someone submitted this page to Hacker News. The post surged to the top of the frontpage, stayed there for many hours, and got 705 upvotes and 145 comments from people all over the world relating to Fibery’s culture. Why? Because it felt real.

Here’s a glimpse of what people wrote in the comments:

A screenshot of Hacker News comments on Fibery's /anxiety page

Another screenshot of Hacker News comments on Fibery's /anxiety page

Remote page

The second special project Fibery did to communicate their culture is the /remote page. It shows what working from home is really like. It’s the funniest thing I’ve ever seen done by a software startup. (Have you ever seen a CEO being licked by a dog?) It also shows how the Fibery team works and even how they use Fibery to build Fibery. Like Canny’s “Lessons from a year of team retreats” blog post, it does so implicitly. A true masterpiece.

Weird, humorous site

Broadly, the whole site screams that Fibery is a place for misfits, rebels, and trouble makers; the place where such people will be valued and will feel like home; the place built around brutal honesty and spicy humor.

The “What (non-)customers say” section is worth a mention. Over my nine years in startups, I haven’t seen a site that a) lists bad customer reviews; and b) uses 💩 emoji as a filter. Again, this is telling. It says a lot about who they are as people: humble, real, and fond of humor.

A screenshot of Fibery's About Us page, What non-customers say section

How PostHog communicates their culture

Comprehensive company handbook covering all-things culture

PostHog’s way of communicating their culture is the most explicit of all four examples, yet very effective. Their primary tool is the PostHog Handbook, which covers virtually every aspect of what working at PostHog feels like: interviews, onboarding, training, management, communication, and even firing. (They call it offboarding.)

The handbook goes all the way up to the high-level strategy, which is very clear. Notably, PostHog’s strategy section not only puts forth ambitious goals but actually explains how exactly the company will get there.

The values section is very specific; perhaps the most specific I’ve ever seen. PostHog does not merely list their values as meaningless abstractions but supports them with evidence. Some values have many paragraphs of examples demonstrating how the team follows them.

A screenshot of the Values section in the PostHog's handbook

They also have a specific Culture page with a 5-minute video from the CEO explaining how they designed PostHog for remote work from day one, which nicely complements the text.

A screenshot from James Hawkins's video

In summary, if Canny’s weapon of choice is the blog and Fibery’s is the website, then PostHog’s is definitely the handbook. It’s a work of art.

How we at Wasp communicate our culture

Easygoing vibe from memes, copy, and imagery

Unlike Posthog, we at Wasp don’t (yet) have a dedicated Culture page. We are too small for that. But that doesn’t stop us from showing what working at Wasp feels like. We just use different tools.

Our Twitter, blog, and monthly updates abound with memes, GIFs, and hilarious imagery. Plus, we write them in a humorous, lighthearted, easygoing style. By just scrolling through these things for a few minutes, candidates can understand that we aren’t some corporate bros. And if they like working on interesting things while having fun, they won’t help but feel an inkling to reach out.

A funny image from Wasp's blog post about GitHub Copilot

A photo of Wasp's team packing t-shirts for users

4. PROGRESS — or, the business is doing well

When you just closed an $80 million Series B or signed Facebook as a customer, communicating progress is easy. You just state these facts. However, most companies need to attract great people way before Series B. In fact, it is these very people who’re going to get you there. As most startups are secretive about how things are going, communicating that things are going somehow — no matter how negligible your progress in contrast to the big guys — becomes quite an advantage. It immediately de-risks the opportunity in the candidate’s eyes. So, if EXPERTISE is about convincing candidates that you know how to build the wings, PROGRESS is about showing them the half-built carcass on your way down. Both are important if you want great people to jump off the cliff with you.

How Canny communicates their progress

To give candidates a sense that things are moving, that this company is not some long slog but a place where progress is made every day, that they can become a part of something that’s growing and, therefore, can grow themselves, to do all that, Canny does two things.

“Year in review” blog posts

The first one is their “Year in review” blog post series. Such comprehensive, thoughtful reviews are rare in the startup world. What is even rarer is when these posts span over four consecutive years. It sends a message that the founders are persistent and devoted to making this company successful.

Below are all Canny’s year-in-review posts in a sequential order:

A screenshot of Canny's Year in review blog post

Important revenue milestones blog posts

In addition to year-in-review posts, Sarah writes about hitting notable revenue milestones. Like with yearly reviews, such transparency is rare. It attracts attention, causes liking, and builds trust.

For example:

A screenshot of Canny's How we built a $1m ARR SaaS startup blog post

Short tweets with progress summary

Finally, Sarah occasionally tweets short summaries of their progress, like this one. These tweets work like ads. Over time, a candidate’s brain fuses them into a broader idea like “Canny is growing” or “Canny is doing well.” Then, once a candidate decides to change jobs, it nudges the candidate to consider Canny.

A screenshot of Sarah’s tweet with progress update

How Fibery communicates their progress

Startup Diary blog posts

The most notable thing Fibery does to communicate their progress is the Startup Diary blog posts series written by the founder, Michael, every month, for the past 45 months. It’s the longest series of monthly updates I know. In these posts, Michael honestly shares everything that’s going on with the company: the good, the bad, and the ugly.

Below are just a few examples, selected by me. You can study all Fibery’s monthly updates here.

  • #2 Slow September 2018 — Fibery startup progress in September 2018. Slow month with not so many news. First positive feedback. Company name selection.
  • #6 Planning Private Beta in January 2019 — Fibery startup progress in January 2019: Private beta goals, selecting a market positioning (hard), apps re-design.
  • #10 Burn in May 2019 — Several people burned out, new features are delivered, public release will be sooner (we hope) (despite ill fortune).
  • #16 Crazy November 2019 — Fibery 1.0 is silently launched. Silence is hard to keep. HackerNews front page. Twitter madness. 3000 registered accounts.
  • #17 Fragmented December 2019 — Public announcements moved to January. +Lena. Tons of feedback. First money! Hype is over. We consider rising a ~$4M round.
  • #35 Raised $3.1M in July 2021 — TLDR: We closed $3.1M seed round. Building a second brain for teams. Fibery mission. Building in Public. Automation rules. Documents and Rich Text history.
  • #36 20k MRR in August 2021 — Special Startup Diary edition. 20k MRR & 15 new customers! +Chris. +Sales agency. 4 case studies. Airtable integration & notify people action.
  • ($30K MRR) #42 Connecting the dots in April 2022 — TLDR: 🇺🇦 Ukrainian war affected our performance. $30K MRR 🐌. 69 reviews in G2 ❤️. Marketing for customer-built products is hard 🥉. 12 customer stories 👻. 2 hours downtime 🥲. New navigation ⛵️. My Space 🔒.

Imagine a candidate who is considering two or more similar startups. Guess what might convince them to go with Fibery? Progress. Or, more exactly, an understanding that Fibery is persistently making progress and, therefore, has a decent chance to become successful. Delivered through these very updates.

Last year, Michael (Fibery’s CEO) started writing year-in-review posts too. I didn’t mention them because there’s just one post for now. You can read his 2021 review here.

Open Startup page with metrics

The second tool that Fibery employs to share their progress is the /open-startup page. Like monthly updates, it gives candidates a good idea of how the business is doing. This understanding, however, comes from a different source: pure numbers. And numbers often speak louder than words.

A screenshot of Fibery's Open startup page

How PostHog communicates their progress

Story page in the handbook

In the PostHog’s handbook, they have a page called Story. It succinctly shows the milestones the company has hit so far. For each milestone, they offer a clear and concise explanation of what happened, sometimes no longer than a sentence. As a result, candidates can get a good idea of how things are going in less than a minute. That’s something to aspire to.

Here’s the section titles:

  • Jan 2020: The start
  • Feb 2020: Launch
  • Apr 2020: $3M Seed round
  • May 2020: First 1,000 users
  • Oct 2020: Billions of events supported
  • Nov 2020: Building a platform
  • Dec 2020: $9M Series A
  • Jun 2021: $15M Series B
  • Sep 2021: Product Market fit achieved for PostHog Scale

A screenshot of PostHog's Story page

How we at Wasp communicate our progress

Blog posts covering big milestones (YC, $1.5m seed)

For each milestone, Matija and Martin (Wasp founders) write a blog post describing not only what they accomplished but also how they did it.

For example, when Wasp got into YC, they didn’t just post the news on Twitter. They wrote a blog about their journey to Y Combinator. It got thousands of views.

Same with fundraising. When Wasp closed a $1.5m seed, Matija documented and shared their fundraising learnings in a blog post. It ended up on the HN frontpage. (Incidentally, this post communicates something important about the founders. It takes persistence to run 250+ meetings in 98 days.)

A screenshot of Wasp's fundraising learnings blog post

Monthly newsletter with updates

To keep the momentum, Matija also writes a monthly newsletter. It’s similar to Michael’s Startup Diary in substance, but has a different style. Wasp style. (Which, again, communicates our culture.)

Like PostHog’s Story page, Wasp’s monthly updates give candidates a bird’s eye view over everything that’s happened in the past two years. To anyone interested in connecting the dots, this page is a gem.

A screenshot of Wasp's monthly newsletter archives

So, why should people join your startup?

The founders are interesting / fun / smart / human / you name it

The team is great

The culture is amazing

The business is doing well

By communicating all these reasons well, what Canny, Fibery, PostHog, and (we hope!) Wasp really end up transmitting is two powerful messages:

  • The company is likely to succeed
  • Working there will be awesome

These two messages are the real answer to “why people should join your company.” The trick, however, and the reason why I wrote this post, is that you can only transmit them indirectly. You can’t say “our founders are great.” You need to provide candidates with many-many facts about the founders, which their minds will then fuse into this abstract conclusion. Ditto for expertise, team, culture, and progress. Eventually, these first-level abstractions will blend into still broader ones: “the company is likely to succeed” and “working there will be awesome.”

Thus, there’s no single, ultimate answer to “why people should join your company.” There’s only a complex system of concrete, specific units of information from which candidates make the answer themselves. In other words, you can’t teach them why your company is likely to succeed and why working here will be awesome. But you can outline the facts and let them learn for themselves. I hope this post shows how to do that outlining well, and I hope you will apply this knowledge to bring talented people onboard and build great things.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/08/26/how-and-why-i-got-started-with-haskell.html b/blog/2022/08/26/how-and-why-i-got-started-with-haskell.html index 0e51807a04..935fe2b3bc 100644 --- a/blog/2022/08/26/how-and-why-i-got-started-with-haskell.html +++ b/blog/2022/08/26/how-and-why-i-got-started-with-haskell.html @@ -19,13 +19,13 @@ - - + +
-

How and why I got started with Haskell

· 8 min read
Shayne Czyzewski

I have been programming professionally for over a decade, using a variety of languages day-to-day including Ada, C, Java, Ruby, Elixir, and JavaScript. I’ve also tried some obscure ones, albeit less frequently and for different purposes: MIPS assembly language and OCaml for academic work (I’m a BS, MS, and PhD dropout in CS), and Zig for some side projects. In short, I like learning new languages (at least at a surface level) and have been exposed to different programming paradigms, including functional.

Yet, I have never done Haskell. I’ve wanted to learn it since my college days, but never got the time. In late 2021, though, my curiosity took over. I wanted to see for myself if the mystique and the Kool-Aid hype (or hate) around it are justified. :P So, I decided I’d start learning it on the side and also look for a company that uses it as my next gig. That’s how my Haskell journey started, and how I got into Wasp a few months later.

Why learn Haskell?

Haskell seems to have an aura of superiority around it. Many niche and heavily academically-inspired languages do. These languages seem to be used by the enlightened minds and allow you to quickly write complex programs in a fraction of the time with significantly less code. Lisp is amongst these languages, too. Yet, nobody uses them for anything real — only toy projects. (While stroking their long, grey beards under a tree, ruminating on the philosophy of computer science.) At least, that’s the impression I got in college and at work. So, what makes Haskell interesting to learn, let alone want to use professionally?

First, it is functional as it gets. While I have used lambdas and functional concepts like map in non-functional languages, the fact that these were my only choice was really interesting to me. After years of extensive OO usage, I’ve come to appreciate this epigram by Alan Perlis. I think it captures a mindset shift between the two paradigms:

“It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.” — Alan Perlis

In OO, you create lots of classes with lots of methods. In functional, you have far fewer data structures (mostly list) with a lot more functions. So basically more functions to operate on fewer nouns, whereas OO is lots of nouns, each with many bespoke methods. (The first comment on this Stack Overflow thread explains it really well.)

Besides, I liked the idea of referential transparency when writing pure functions. It means that you get the same result back every time you invoke a function, without fear of unknown side effects. (But the language does offer the flexibility to have side effects like IO, via Monads.) I also liked having only immutable data structures — they make reasoning about the system and data flow easier. There were many things like these two that I liked. The point is that thinking functionally really changes the way you structure and solve problems, so I was curious to give it a go.

Second, Haskell is lazy. While there are pros and cons to this, it feels undeniably different. Most languages are strict, in that all function arguments are evaluated before invoking a function. This is required because of side effects; to have some expectations regarding the order in which things will run. Haskell does the opposite: it delays evaluation until it’s actually needed.

One contrived yet helpful example of laziness is infinite data structures. Below, we define fibs as an infinite List of Integer values, by using references to itself! (You can find a runnable example here.)

fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]

There’s a downside to laziness, too. It makes it harder to reason about performance and resource utilization. But the idea that you can define things in a declarative way but know that they are evaluated only when needed is a pretty eye-opening way to program.

To sum up: Haskell is functional, lazy, and strongly statically typed. Just the trifecta that gets me out of bed in the morning! :D So, how did I go about learning it?

Hello Haskell!

I started by reading the canonical Haskell newbie resource, “Learn You a Haskell for Great Good!,” often abbreviated LYAH. It was very entertaining, and I learned a lot from it. At times, I wanted it to get to the point more quickly. Still, despite the amusing images and often lengthy examples, it provided me with a great conceptual foundation. I highly recommend it as your first read — it is a really well-written resource for beginners.

After I was about 80% done with LYAH, I switched to a more recent but still popular book: “Haskell Programming from First Principles.” I liked that it started with fundamentals and then moved to more complex topics, slowly but steadily developing my understanding. It was pretty long, though, and sometimes went too far into the weeds. It also had a tinge of intellectual flexing at certain points. Still, it was a good read. I’d read it again if I were starting over.

I also tried a Haskell course from Google. Despite being brief, it explains the key concepts in a relatively complete way. If videos are your thing, it might be a solid way to get up to speed.

In short, skimming an intro book to get your foundation solid would be the best bet. I’d also recommend trying out many different online resources when covering more intermediate topics, like Monad Transformers, for example. And don’t worry if it takes a while to start feeling comfortable with things that are pretty specific to Haskell! It just takes some time, and often it is more confusing to derive/deeply understand than to just start using them at first. The understanding will come over time. (Of course, sometimes pictures help!)

Setup and IDE support

Getting Haskell up and running was surprisingly straightforward, even though I ran it on an M1 MacBook Air, which was considered a pretty new architecture in 2021. Since the entire toolchain was not fully ARM-compatible back then, some of the setup advice required a bit of modification. But that was no big deal: I used ghcup, installed HLS in VS Code, and bam! — I had Haskell up and running. It was a pretty nice experience.

Some minor downsides I recall:

  • There doesn’t seem to be a consensus on which build and package management tool to use, Cabal or Stack. However, unless you’re doing something super specific, it’s not an irreversible decision. At Wasp, we started with Stack but then migrated to Cabal since it better fit our setup and workflows. It was pretty seamless.
  • One thing I do miss from other IDEs is breakpoint debugging. Technically, there’s some support for it in Haskell, but I don’t think many use it. Breakpoints and lazy evaluation don’t seem to be BFFs.

0-60 at work

For someone with experience in several different languages, it is pretty achievable to be able to solve minor bugs/features in Haskell after a few weeks of learning. At least, it was for me. I certainly struggled on best practices and such, and my code reviews involved some Haskell golfing comments for sure :) But I could make it do what I wanted it to do from the functionality perspective. Kudos to the mostly helpful compiler errors (with a bit of practice reading) and the Internet!

Hopefully, your code base demonstrates established project and Haskell patterns, so you can learn as you poke around, and your early code reviewers are supportive coworkers who can explain things as part of their suggestions. I was quite fortunate in that regard: the Wasp team values teaching and learning, and the codebase uses what is called “Simple Haskell”, which limits the use of excessive language extensions in the hopes to keep the core language and concepts as tight as possible. (Note: there are Haskell experts who view this as a severe limitation of the capabilities of the language, but as a newbie, I was happy they did it.)

So, was the juice worth the squeeze?

Learning Haskell took considerable time and effort. It was completely different from any language I had used before. Yet, I am very happy I embarked on this journey. Even if you do not intend to get a job using Haskell, I still think learning it is worthwhile just to expand your programming point of view and master functional concepts. And for a select set of project types (like writing a compiler for a full-stack web DSL), I feel it really will make you more productive over time. Give an intro to Haskell tutorial or video a try some weekend and let me know what you think! I’m at shayne at wasp-lang dot dev dot com.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How and why I got started with Haskell

· 8 min read
Shayne Czyzewski

I have been programming professionally for over a decade, using a variety of languages day-to-day including Ada, C, Java, Ruby, Elixir, and JavaScript. I’ve also tried some obscure ones, albeit less frequently and for different purposes: MIPS assembly language and OCaml for academic work (I’m a BS, MS, and PhD dropout in CS), and Zig for some side projects. In short, I like learning new languages (at least at a surface level) and have been exposed to different programming paradigms, including functional.

Yet, I have never done Haskell. I’ve wanted to learn it since my college days, but never got the time. In late 2021, though, my curiosity took over. I wanted to see for myself if the mystique and the Kool-Aid hype (or hate) around it are justified. :P So, I decided I’d start learning it on the side and also look for a company that uses it as my next gig. That’s how my Haskell journey started, and how I got into Wasp a few months later.

Why learn Haskell?

Haskell seems to have an aura of superiority around it. Many niche and heavily academically-inspired languages do. These languages seem to be used by the enlightened minds and allow you to quickly write complex programs in a fraction of the time with significantly less code. Lisp is amongst these languages, too. Yet, nobody uses them for anything real — only toy projects. (While stroking their long, grey beards under a tree, ruminating on the philosophy of computer science.) At least, that’s the impression I got in college and at work. So, what makes Haskell interesting to learn, let alone want to use professionally?

First, it is functional as it gets. While I have used lambdas and functional concepts like map in non-functional languages, the fact that these were my only choice was really interesting to me. After years of extensive OO usage, I’ve come to appreciate this epigram by Alan Perlis. I think it captures a mindset shift between the two paradigms:

“It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.” — Alan Perlis

In OO, you create lots of classes with lots of methods. In functional, you have far fewer data structures (mostly list) with a lot more functions. So basically more functions to operate on fewer nouns, whereas OO is lots of nouns, each with many bespoke methods. (The first comment on this Stack Overflow thread explains it really well.)

Besides, I liked the idea of referential transparency when writing pure functions. It means that you get the same result back every time you invoke a function, without fear of unknown side effects. (But the language does offer the flexibility to have side effects like IO, via Monads.) I also liked having only immutable data structures — they make reasoning about the system and data flow easier. There were many things like these two that I liked. The point is that thinking functionally really changes the way you structure and solve problems, so I was curious to give it a go.

Second, Haskell is lazy. While there are pros and cons to this, it feels undeniably different. Most languages are strict, in that all function arguments are evaluated before invoking a function. This is required because of side effects; to have some expectations regarding the order in which things will run. Haskell does the opposite: it delays evaluation until it’s actually needed.

One contrived yet helpful example of laziness is infinite data structures. Below, we define fibs as an infinite List of Integer values, by using references to itself! (You can find a runnable example here.)

fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]

There’s a downside to laziness, too. It makes it harder to reason about performance and resource utilization. But the idea that you can define things in a declarative way but know that they are evaluated only when needed is a pretty eye-opening way to program.

To sum up: Haskell is functional, lazy, and strongly statically typed. Just the trifecta that gets me out of bed in the morning! :D So, how did I go about learning it?

Hello Haskell!

I started by reading the canonical Haskell newbie resource, “Learn You a Haskell for Great Good!,” often abbreviated LYAH. It was very entertaining, and I learned a lot from it. At times, I wanted it to get to the point more quickly. Still, despite the amusing images and often lengthy examples, it provided me with a great conceptual foundation. I highly recommend it as your first read — it is a really well-written resource for beginners.

After I was about 80% done with LYAH, I switched to a more recent but still popular book: “Haskell Programming from First Principles.” I liked that it started with fundamentals and then moved to more complex topics, slowly but steadily developing my understanding. It was pretty long, though, and sometimes went too far into the weeds. It also had a tinge of intellectual flexing at certain points. Still, it was a good read. I’d read it again if I were starting over.

I also tried a Haskell course from Google. Despite being brief, it explains the key concepts in a relatively complete way. If videos are your thing, it might be a solid way to get up to speed.

In short, skimming an intro book to get your foundation solid would be the best bet. I’d also recommend trying out many different online resources when covering more intermediate topics, like Monad Transformers, for example. And don’t worry if it takes a while to start feeling comfortable with things that are pretty specific to Haskell! It just takes some time, and often it is more confusing to derive/deeply understand than to just start using them at first. The understanding will come over time. (Of course, sometimes pictures help!)

Setup and IDE support

Getting Haskell up and running was surprisingly straightforward, even though I ran it on an M1 MacBook Air, which was considered a pretty new architecture in 2021. Since the entire toolchain was not fully ARM-compatible back then, some of the setup advice required a bit of modification. But that was no big deal: I used ghcup, installed HLS in VS Code, and bam! — I had Haskell up and running. It was a pretty nice experience.

Some minor downsides I recall:

  • There doesn’t seem to be a consensus on which build and package management tool to use, Cabal or Stack. However, unless you’re doing something super specific, it’s not an irreversible decision. At Wasp, we started with Stack but then migrated to Cabal since it better fit our setup and workflows. It was pretty seamless.
  • One thing I do miss from other IDEs is breakpoint debugging. Technically, there’s some support for it in Haskell, but I don’t think many use it. Breakpoints and lazy evaluation don’t seem to be BFFs.

0-60 at work

For someone with experience in several different languages, it is pretty achievable to be able to solve minor bugs/features in Haskell after a few weeks of learning. At least, it was for me. I certainly struggled on best practices and such, and my code reviews involved some Haskell golfing comments for sure :) But I could make it do what I wanted it to do from the functionality perspective. Kudos to the mostly helpful compiler errors (with a bit of practice reading) and the Internet!

Hopefully, your code base demonstrates established project and Haskell patterns, so you can learn as you poke around, and your early code reviewers are supportive coworkers who can explain things as part of their suggestions. I was quite fortunate in that regard: the Wasp team values teaching and learning, and the codebase uses what is called “Simple Haskell”, which limits the use of excessive language extensions in the hopes to keep the core language and concepts as tight as possible. (Note: there are Haskell experts who view this as a severe limitation of the capabilities of the language, but as a newbie, I was happy they did it.)

So, was the juice worth the squeeze?

Learning Haskell took considerable time and effort. It was completely different from any language I had used before. Yet, I am very happy I embarked on this journey. Even if you do not intend to get a job using Haskell, I still think learning it is worthwhile just to expand your programming point of view and master functional concepts. And for a select set of project types (like writing a compiler for a full-stack web DSL), I feel it really will make you more productive over time. Give an intro to Haskell tutorial or video a try some weekend and let me know what you think! I’m at shayne at wasp-lang dot dev dot com.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/09/02/how-to-get-started-with-haskell-in-2022.html b/blog/2022/09/02/how-to-get-started-with-haskell-in-2022.html index 8d43257799..69ad951754 100644 --- a/blog/2022/09/02/how-to-get-started-with-haskell-in-2022.html +++ b/blog/2022/09/02/how-to-get-started-with-haskell-in-2022.html @@ -19,13 +19,13 @@ - - + +
-

How to get started with Haskell in 2022 (the straightforward way)

· 7 min read
Martin Sosic

Haskell is a unique and beautiful language that is worth learning, if for nothing else, then just for the concepts it introduces and their potential to expand your view on programming.

I have been programming in Haskell on and off since 2011 and professionally for the past 2 years, building a compiler. While in that time Haskell has become much more beginner-friendly, I keep seeing beginners who are overwhelmed by numerous popular options for build tools, installers, introductory educational resources, and similar. Haskell’s homepage getting a call from the previous decade to give them their UX back :D also doesn’t help!

That is why I decided to write this opinionated and practical post that will tell you exactly how to get started with Haskell in 2022 in the most standard / common way. Instead of worrying about decisions that you are not equipped to make at the moment (e.g. “what is the best build tool?”), you can focus on enjoying learning Haskell :)!

TLDR / Super opinionated summary

  1. For setup, use GHCup. Install GHC, HLS, and cabal.
  2. As a build tool, use cabal.
  3. For editor, use VS Code with Haskell extension. Or, use emacs/vim/....
  4. Join r/haskell. Feel free to ask for help!
  5. To learn the basics of Haskell, read the LYAH book and build a blog generator in Haskell. Focus on getting through stuff instead of understanding everything fully; you will come back to it later again.

1. Setup: Use GHCup for seamless installation

GHCup is a universal installer for Haskell. It will install everything you need to program in Haskell and will help you manage those installations in the future (update, switch versions, and similar). It is simple to use and works the same way on Linux, macOS, and Windows. It gives you a single central place/method to take care of your Haskell installation so that you don’t have to deal with OS-specific issues.

To install it, follow instructions at GHCup. Then, use it to install the Haskell Toolchain (aka stuff that you need to program in Haskell).

Haskell Toolchain consists of:

  1. GHC -> Haskell compiler
  2. HLS -> Haskell Language Server -> your code editor will use this to provide you with a great experience while editing Haskell code
  3. cabal -> Haskell build tool -> you will use this to organize your Haskell projects, build them, run them, define dependencies, etc.
  4. Stack -> cabal alternative, which you won’t need for now since we’ll go with cabal as our build tool of choice

2. Build tool: Use cabal

There are two popular build tools for Haskell: cabal and Stack. Both are widely used and have their pros and cons. So, one of the hard choices beginners often face is which one to use.

Some time ago, cabal was somewhat hard to use (complex, “dependency hell”). That’s why Stack was created: a user-friendly build tool that solves some of the common issues of cabal. (Interestingly, Stack uses cabal’s core library as its backend!) However, as Stack was being developed, cabal advanced, too. Many of its issues have been solved, making it a viable choice for beginners.

In 2022, I recommend cabal to beginners. I find it a bit easier to understand when starting out (no resolvers), it works well out of the box with GHCup and the rest of the ecosystem, and it seems to be better maintained lately.

3. Editor: VS Code is a safe bet

HLS (Haskell Language Server) brings all the cool IDE features to your editor. So, as long as your editor has a decent Haskell language extension that utilizes HLS, you are good.

The safest bet is to go with Visual Studio Code — it has a great Haskell extension that usually works out of the box. A lot of Haskell programmers also use Emacs and Vim. I can confirm they also have good support for Haskell.

4. Community: r/haskell and more

Haskell community is a great place to ask for help and learn about new developments in the ecosystem. I prefer r/haskell -> it tracks all the newest events and no question goes unanswered. There is also Haskell Discourse, where a lot of discussions happen, including the more official ones. A lot of Haskellers are still active on IRC, but I find it too complex and outdated to use.

Check https://www.haskell.org/community for a full list of Haskell communities.

5. Learning: You don’t need a math degree, just grab a book

There is a common myth going around that you need a special knowledge of math (PhD in category theory!) to be able to program in Haskell properly. From my experience, this is as far from the truth as it can be. It is certainly not needed, and I seriously doubt it helps even if you have it. Maybe for some very advanced Haskell stuff, but certainly not for junior/intermediate level.

Instead, learning Haskell is the same as learning other languages -> you need a healthy mix of theory and practice. The main difference is that there will be more unusual/new concepts than you are used to, which will require some additional effort. But these new concepts are also what makes learning Haskell so fun!

I recommend starting with a book for beginners, LYAH. It has an online version that you can read for free, or you can buy a printed version if you like physical books.

If you don't like LYAH, consider other popular books for beginners (none of them are free though):

  1. Haskell Programming from first principles
  2. Get Programming with Haskell
  3. Programming in Haskell

Whatever book you go with, don’t get stuck for too long on concepts that are confusing to you, especially towards the end of the book. Some concepts will just need time to click; don’t expect to grasp it all on the first try. Whatever you do grasp from the first read will likely be more than enough to get going with your first projects in Haskell. You can always come back to those complex concepts later and understand them better. Also, don’t be shy to ask the community -> there are many Haskellers out there happy to support you in your learning!

note

When I say "don't get stuck", I don't mean you should skip the difficult concept after the first hurdle. No, you should spend some hours experimenting, looking at it from different angles, playing with it, trying to crack it. But you shouldn't spend days trying to understand the same concept (e.g. function as a monad) and then feel defeated due to not grasping it 100%. Instead, if you put proper effort but stuff is not completely clicking, tap yourself on the back and move on for now.

Once you take the first pass through the book, I recommend doing a project or two. You can come up with an idea yourself, or you can follow one of the books that guide you through it.

For example:

  1. Learn Haskell by building a blog generator -> free, starts from 0 knowledge, and could even be used as the very first resource, instead of e.g. LYAH.
  2. The Simple Haskell Handbook -> not free, expects you to know the basics of Haskell already

Once you have more experience with projects, I would recommend re-reading your beginner book of choice. This time, you can skip the parts you already know and focus on what was confusing before. You will likely have a much easier time grasping those harder concepts.

p.s. If you are looking for a bit of extra motivation, check the blog post my teammate Shayne recently wrote about his journey with Haskell. He started in late 2021 and has already made huge progress!


Good luck with Haskell! If you have Haskell questions for me or the rest of the Wasp team, drop me a line at “martin” ++ “@” ++ concat [”wasp”, “-”, “lang”] <> “.dev” , or write to #haskell channel in Wasp-lang Discord server.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How to get started with Haskell in 2022 (the straightforward way)

· 7 min read
Martin Sosic

Haskell is a unique and beautiful language that is worth learning, if for nothing else, then just for the concepts it introduces and their potential to expand your view on programming.

I have been programming in Haskell on and off since 2011 and professionally for the past 2 years, building a compiler. While in that time Haskell has become much more beginner-friendly, I keep seeing beginners who are overwhelmed by numerous popular options for build tools, installers, introductory educational resources, and similar. Haskell’s homepage getting a call from the previous decade to give them their UX back :D also doesn’t help!

That is why I decided to write this opinionated and practical post that will tell you exactly how to get started with Haskell in 2022 in the most standard / common way. Instead of worrying about decisions that you are not equipped to make at the moment (e.g. “what is the best build tool?”), you can focus on enjoying learning Haskell :)!

TLDR / Super opinionated summary

  1. For setup, use GHCup. Install GHC, HLS, and cabal.
  2. As a build tool, use cabal.
  3. For editor, use VS Code with Haskell extension. Or, use emacs/vim/....
  4. Join r/haskell. Feel free to ask for help!
  5. To learn the basics of Haskell, read the LYAH book and build a blog generator in Haskell. Focus on getting through stuff instead of understanding everything fully; you will come back to it later again.

1. Setup: Use GHCup for seamless installation

GHCup is a universal installer for Haskell. It will install everything you need to program in Haskell and will help you manage those installations in the future (update, switch versions, and similar). It is simple to use and works the same way on Linux, macOS, and Windows. It gives you a single central place/method to take care of your Haskell installation so that you don’t have to deal with OS-specific issues.

To install it, follow instructions at GHCup. Then, use it to install the Haskell Toolchain (aka stuff that you need to program in Haskell).

Haskell Toolchain consists of:

  1. GHC -> Haskell compiler
  2. HLS -> Haskell Language Server -> your code editor will use this to provide you with a great experience while editing Haskell code
  3. cabal -> Haskell build tool -> you will use this to organize your Haskell projects, build them, run them, define dependencies, etc.
  4. Stack -> cabal alternative, which you won’t need for now since we’ll go with cabal as our build tool of choice

2. Build tool: Use cabal

There are two popular build tools for Haskell: cabal and Stack. Both are widely used and have their pros and cons. So, one of the hard choices beginners often face is which one to use.

Some time ago, cabal was somewhat hard to use (complex, “dependency hell”). That’s why Stack was created: a user-friendly build tool that solves some of the common issues of cabal. (Interestingly, Stack uses cabal’s core library as its backend!) However, as Stack was being developed, cabal advanced, too. Many of its issues have been solved, making it a viable choice for beginners.

In 2022, I recommend cabal to beginners. I find it a bit easier to understand when starting out (no resolvers), it works well out of the box with GHCup and the rest of the ecosystem, and it seems to be better maintained lately.

3. Editor: VS Code is a safe bet

HLS (Haskell Language Server) brings all the cool IDE features to your editor. So, as long as your editor has a decent Haskell language extension that utilizes HLS, you are good.

The safest bet is to go with Visual Studio Code — it has a great Haskell extension that usually works out of the box. A lot of Haskell programmers also use Emacs and Vim. I can confirm they also have good support for Haskell.

4. Community: r/haskell and more

Haskell community is a great place to ask for help and learn about new developments in the ecosystem. I prefer r/haskell -> it tracks all the newest events and no question goes unanswered. There is also Haskell Discourse, where a lot of discussions happen, including the more official ones. A lot of Haskellers are still active on IRC, but I find it too complex and outdated to use.

Check https://www.haskell.org/community for a full list of Haskell communities.

5. Learning: You don’t need a math degree, just grab a book

There is a common myth going around that you need a special knowledge of math (PhD in category theory!) to be able to program in Haskell properly. From my experience, this is as far from the truth as it can be. It is certainly not needed, and I seriously doubt it helps even if you have it. Maybe for some very advanced Haskell stuff, but certainly not for junior/intermediate level.

Instead, learning Haskell is the same as learning other languages -> you need a healthy mix of theory and practice. The main difference is that there will be more unusual/new concepts than you are used to, which will require some additional effort. But these new concepts are also what makes learning Haskell so fun!

I recommend starting with a book for beginners, LYAH. It has an online version that you can read for free, or you can buy a printed version if you like physical books.

If you don't like LYAH, consider other popular books for beginners (none of them are free though):

  1. Haskell Programming from first principles
  2. Get Programming with Haskell
  3. Programming in Haskell

Whatever book you go with, don’t get stuck for too long on concepts that are confusing to you, especially towards the end of the book. Some concepts will just need time to click; don’t expect to grasp it all on the first try. Whatever you do grasp from the first read will likely be more than enough to get going with your first projects in Haskell. You can always come back to those complex concepts later and understand them better. Also, don’t be shy to ask the community -> there are many Haskellers out there happy to support you in your learning!

note

When I say "don't get stuck", I don't mean you should skip the difficult concept after the first hurdle. No, you should spend some hours experimenting, looking at it from different angles, playing with it, trying to crack it. But you shouldn't spend days trying to understand the same concept (e.g. function as a monad) and then feel defeated due to not grasping it 100%. Instead, if you put proper effort but stuff is not completely clicking, tap yourself on the back and move on for now.

Once you take the first pass through the book, I recommend doing a project or two. You can come up with an idea yourself, or you can follow one of the books that guide you through it.

For example:

  1. Learn Haskell by building a blog generator -> free, starts from 0 knowledge, and could even be used as the very first resource, instead of e.g. LYAH.
  2. The Simple Haskell Handbook -> not free, expects you to know the basics of Haskell already

Once you have more experience with projects, I would recommend re-reading your beginner book of choice. This time, you can skip the parts you already know and focus on what was confusing before. You will likely have a much easier time grasping those harder concepts.

p.s. If you are looking for a bit of extra motivation, check the blog post my teammate Shayne recently wrote about his journey with Haskell. He started in late 2021 and has already made huge progress!


Good luck with Haskell! If you have Haskell questions for me or the rest of the Wasp team, drop me a line at “martin” ++ “@” ++ concat [”wasp”, “-”, “lang”] <> “.dev” , or write to #haskell channel in Wasp-lang Discord server.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/09/05/dev-excuses-app-tutrial.html b/blog/2022/09/05/dev-excuses-app-tutrial.html index b8f1ec1418..a5cf81a3c5 100644 --- a/blog/2022/09/05/dev-excuses-app-tutrial.html +++ b/blog/2022/09/05/dev-excuses-app-tutrial.html @@ -19,13 +19,13 @@ - - + +
-

Building an app to find an excuse for our sloppy work

· 8 min read

We’ll build a web app to solve every developer's most common problem – finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can’t excuse ourselves from building it!

Best excuse of all time

Best excuse of all time! Taken from here.

The requirements were unclear.

We’ll use Michele Gerarduzzi’s open-source project. It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let’s define the requirements for the project:

  • The app should be able to pull excuses data from a public API.
  • Save the ones you liked (and your boss doesn't) to the database for future reference.
  • Building an app shouldn’t take more than 15 minutes.
  • Use modern web dev technologies (NodeJS + React)

As a result – we’ll get a simple and fun pet project. You can find the complete codebase here.

Final result

There’s an issue with the third party library.

Setting up a backbone for the project is the most frustrating part of building any application.

We are installing dependencies, tying up the back-end and front-end, setting up a database, managing connection strings, and so on. Avoiding this part will save us a ton of time and effort. So let’s find ourselves an excuse to skip the initial project setup.

Ideally – use a framework that will create a project infrastructure quickly with the best defaults so that we’ll focus on the business logic. A perfect candidate is Wasp. It’s an open-source, declarative DSL for building web apps in React and Node.js with no boilerplate

How it works: developer starts from a single config file that specifies the app architecture. Routes, CRUD API, auth, and so on. Then adds React/Node.js code for the specific business logic. Behind the scenes, Wasp compiler will produce the entire source code of the app - back-end, front-end, deployment template, database migrations and everything else you’ve used to have in any other full-stack app.

Wasp architecture

So let’s jump right in.

Maybe something's wrong with the environment.

Wasp intentionally works with the LTS Node.js version since it guarantees stability and active maintenance. As for now, it’s Node 16 and NPM 8. If you need another Node version for some other project – there’s a possibility to use NVM to manage multiple Node versions on your computer at the same time.

Installing Wasp on Linux (for Mac/Windows, please check the docs):

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

Now let’s create a new web app named ItWaspsOnMyMachine.

wasp new ItWaspsOnMyMachine

Changing the working directory:

cd ItWaspsOnMyMachine

Starting the app:

wasp start

Now your default browser should open up with a simple predefined text message. That’s it! 🥳 We’ve built and run a NodeJS + React application. And for now – the codebase consists of only two files! main.wasp is the config file that defines the application’s functionality. And MainPage.js is the front-end.

Initial page

That worked perfectly when I developed it.

1) Let’s add some additional configuration to our main.wasp file. So it will look like this:

main.wasp | Defining Excuse entity, queries and action

// Main declaration, defines a new web app.
app ItWaspsOnMyMachine {
// Wasp compiler configuration
wasp: {
version: "^0.6.0"
},

// Used as a browser tab title.
title: "It Wasps On My Machine",

head: [
// Adding Tailwind to make our UI prettier
"<script src='https://cdn.tailwindcss.com'></script>"
],

dependencies: [
// Adding Axios for making HTTP requests
("axios", "^0.21.1")
]
}

// Render page MainPage on url `/` (default url).
route RootRoute { path: "/", to: MainPage }

// ReactJS implementation of our page located in `src/client/MainPage.js` as a default export.
page MainPage {
component: import Main from "@client/MainPage.js"
}

// Prisma database entity
entity Excuse {=psl
id Int @id @default(autoincrement())
text String
psl=}

// Query declaration to get a new excuse
query getExcuse {
fn: import { getExcuse } from "@server/queries.js",
entities: [Excuse]
}

// Query declaration to get all excuses
query getAllSavedExcuses {
fn: import { getAllSavedExcuses } from "@server/queries.js",
entities: [Excuse]
}

// Action to save current excuse
action saveExcuse {
fn: import { saveExcuse } from "@server/actions.js",
entities: [Excuse]
}

We’ve added Tailwind to make our UI more pretty and Axios for making API requests.

Also, we’ve declared a database entity called Excuse, queries, and action. The Excuse entity consists of the entity’s ID and the text.

Queries are here when we need to fetch/read something, while actions are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info in the docs. So let’s proceed with queries/actions.

2) Create two files: “actions.js” and “queries.js” in the src/server folder.

src/server/actions.js | Defining an action
export const saveExcuse = async (excuse, context) => {
return context.entities.Excuse.create({
data: { text: excuse.text }
})
}
src/server/queries.js | Defining queries
import axios from 'axios';

export const getExcuse = async () => {
const response = await axios.get('https://api.devexcus.es/')
return response.data
}

export const getAllSavedExcuses = async (_args, context) => {
return context.entities.Excuse.findMany()
}

Let’s add saveExcuse() action to our actions.js file. This action will save the text of our excuse to the database. Then let’s create two queries in the queries.js file. First, one getExcuse will call an external API and fetch a new excuse. The second one, named getAllSavedExcuses, will pull all the excuses we’ve saved to our database.

That’s it! We finished our back-end. 🎉 Now, let’s use those queries/actions on our UI.

3) Let’s erase everything we had in the MainPage.js file and substitute it with our new UI.

src/client/MainPage.js | Updating the UI
import React, { useState } from 'react'
import { useQuery } from '@wasp/queries'
import getExcuse from '@wasp/queries/getExcuse'
import getAllSavedExcuses from '@wasp/queries/getAllSavedExcuses'
import saveExcuse from '@wasp/actions/saveExcuse'

const MainPage = () => {
const [currentExcuse, setCurrentExcuse] = useState({ text: "" })
const { data: excuses } = useQuery(getAllSavedExcuses)

const handleGetExcuse = async () => {
try {
setCurrentExcuse(await getExcuse())
} catch (err) {
window.alert('Error while getting the excuse: ' + err.message)
}
}

const handleSaveExcuse = async () => {
if (currentExcuse.text) {
try {
await saveExcuse(currentExcuse)
} catch (err) {
window.alert('Error while saving the excuse: ' + err.message)
}
}
}

return (
<div className="grid grid-cols-2 text-3xl">
<div>
<button onClick={handleGetExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Get excuse </button>
<button onClick={handleSaveExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Save excuse </button>
<Excuse excuse={currentExcuse} />
</div>
<div>
<div className="px-6 py-2 bg-blue-600 text-white"> Saved excuses: </div>
{excuses && <ExcuseList excuses={excuses} />}
</div>
</div>
)
}

const ExcuseList = (props) => {
return props.excuses?.length ? props.excuses.map((excuse, idx) => <Excuse excuse={excuse} key={idx} />) : 'No saved excuses'
}

const Excuse = ({ excuse }) => {
return (
<div className="px-6 py-2">
{excuse.text}
</div>
)
}

export default MainPage

Our page consists of three components. MainPage, ExcuseList and Excuse. It may seem at first that this file is pretty complex. It’s not, so let’s look a bit closer.

Excuse is just a div with an excuse text, ExcuseList checks if there are any excuses. If the list is empty – show a message No saved excuses. In other case – excuses will be displayed.

MainPage contains info about the current excuses and the list of already saved excuses. Two buttons click handlers handleGetExcuse and handleSaveExcuse. Plus, the markup itself with some Tailwind flavor.

4) Before starting an app – we need to execute database migration because we changed the DB schema by adding new entities. If you’ve had something running in the terminal – stop it and run:

wasp db migrate-dev

You’ll be prompted to enter a name for the migration. Something like init will be ok. Now we can start the application!

wasp start

Final empty result

Now you can click the “Get excuse” button to receive an excuse. And save the ones you like into the DB with the “Save excuse” button. Our final project should look like this:

Final result

It would have taken twice as long to build it properly.

Now we can think of some additional improvements. For example:

  • 1) Add a unique constraint to Entity’s ID so we won’t be able to save duplicated excuses.
  • 2) Add exceptions and edge cases handling.
  • 3) Make the markup prettier.
  • 4) Optimize and polish the code

So, we’ve been able to build a full-stack application with a database and external API call in a couple of minutes. And now we have a box full of excuses for all our development needs.

Box of excuses for the win!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Building an app to find an excuse for our sloppy work

· 8 min read

We’ll build a web app to solve every developer's most common problem – finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can’t excuse ourselves from building it!

Best excuse of all time

Best excuse of all time! Taken from here.

The requirements were unclear.

We’ll use Michele Gerarduzzi’s open-source project. It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let’s define the requirements for the project:

  • The app should be able to pull excuses data from a public API.
  • Save the ones you liked (and your boss doesn't) to the database for future reference.
  • Building an app shouldn’t take more than 15 minutes.
  • Use modern web dev technologies (NodeJS + React)

As a result – we’ll get a simple and fun pet project. You can find the complete codebase here.

Final result

There’s an issue with the third party library.

Setting up a backbone for the project is the most frustrating part of building any application.

We are installing dependencies, tying up the back-end and front-end, setting up a database, managing connection strings, and so on. Avoiding this part will save us a ton of time and effort. So let’s find ourselves an excuse to skip the initial project setup.

Ideally – use a framework that will create a project infrastructure quickly with the best defaults so that we’ll focus on the business logic. A perfect candidate is Wasp. It’s an open-source, declarative DSL for building web apps in React and Node.js with no boilerplate

How it works: developer starts from a single config file that specifies the app architecture. Routes, CRUD API, auth, and so on. Then adds React/Node.js code for the specific business logic. Behind the scenes, Wasp compiler will produce the entire source code of the app - back-end, front-end, deployment template, database migrations and everything else you’ve used to have in any other full-stack app.

Wasp architecture

So let’s jump right in.

Maybe something's wrong with the environment.

Wasp intentionally works with the LTS Node.js version since it guarantees stability and active maintenance. As for now, it’s Node 16 and NPM 8. If you need another Node version for some other project – there’s a possibility to use NVM to manage multiple Node versions on your computer at the same time.

Installing Wasp on Linux (for Mac/Windows, please check the docs):

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

Now let’s create a new web app named ItWaspsOnMyMachine.

wasp new ItWaspsOnMyMachine

Changing the working directory:

cd ItWaspsOnMyMachine

Starting the app:

wasp start

Now your default browser should open up with a simple predefined text message. That’s it! 🥳 We’ve built and run a NodeJS + React application. And for now – the codebase consists of only two files! main.wasp is the config file that defines the application’s functionality. And MainPage.js is the front-end.

Initial page

That worked perfectly when I developed it.

1) Let’s add some additional configuration to our main.wasp file. So it will look like this:

main.wasp | Defining Excuse entity, queries and action

// Main declaration, defines a new web app.
app ItWaspsOnMyMachine {
// Wasp compiler configuration
wasp: {
version: "^0.6.0"
},

// Used as a browser tab title.
title: "It Wasps On My Machine",

head: [
// Adding Tailwind to make our UI prettier
"<script src='https://cdn.tailwindcss.com'></script>"
],

dependencies: [
// Adding Axios for making HTTP requests
("axios", "^0.21.1")
]
}

// Render page MainPage on url `/` (default url).
route RootRoute { path: "/", to: MainPage }

// ReactJS implementation of our page located in `src/client/MainPage.js` as a default export.
page MainPage {
component: import Main from "@client/MainPage.js"
}

// Prisma database entity
entity Excuse {=psl
id Int @id @default(autoincrement())
text String
psl=}

// Query declaration to get a new excuse
query getExcuse {
fn: import { getExcuse } from "@server/queries.js",
entities: [Excuse]
}

// Query declaration to get all excuses
query getAllSavedExcuses {
fn: import { getAllSavedExcuses } from "@server/queries.js",
entities: [Excuse]
}

// Action to save current excuse
action saveExcuse {
fn: import { saveExcuse } from "@server/actions.js",
entities: [Excuse]
}

We’ve added Tailwind to make our UI more pretty and Axios for making API requests.

Also, we’ve declared a database entity called Excuse, queries, and action. The Excuse entity consists of the entity’s ID and the text.

Queries are here when we need to fetch/read something, while actions are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info in the docs. So let’s proceed with queries/actions.

2) Create two files: “actions.js” and “queries.js” in the src/server folder.

src/server/actions.js | Defining an action
export const saveExcuse = async (excuse, context) => {
return context.entities.Excuse.create({
data: { text: excuse.text }
})
}
src/server/queries.js | Defining queries
import axios from 'axios';

export const getExcuse = async () => {
const response = await axios.get('https://api.devexcus.es/')
return response.data
}

export const getAllSavedExcuses = async (_args, context) => {
return context.entities.Excuse.findMany()
}

Let’s add saveExcuse() action to our actions.js file. This action will save the text of our excuse to the database. Then let’s create two queries in the queries.js file. First, one getExcuse will call an external API and fetch a new excuse. The second one, named getAllSavedExcuses, will pull all the excuses we’ve saved to our database.

That’s it! We finished our back-end. 🎉 Now, let’s use those queries/actions on our UI.

3) Let’s erase everything we had in the MainPage.js file and substitute it with our new UI.

src/client/MainPage.js | Updating the UI
import React, { useState } from 'react'
import { useQuery } from '@wasp/queries'
import getExcuse from '@wasp/queries/getExcuse'
import getAllSavedExcuses from '@wasp/queries/getAllSavedExcuses'
import saveExcuse from '@wasp/actions/saveExcuse'

const MainPage = () => {
const [currentExcuse, setCurrentExcuse] = useState({ text: "" })
const { data: excuses } = useQuery(getAllSavedExcuses)

const handleGetExcuse = async () => {
try {
setCurrentExcuse(await getExcuse())
} catch (err) {
window.alert('Error while getting the excuse: ' + err.message)
}
}

const handleSaveExcuse = async () => {
if (currentExcuse.text) {
try {
await saveExcuse(currentExcuse)
} catch (err) {
window.alert('Error while saving the excuse: ' + err.message)
}
}
}

return (
<div className="grid grid-cols-2 text-3xl">
<div>
<button onClick={handleGetExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Get excuse </button>
<button onClick={handleSaveExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Save excuse </button>
<Excuse excuse={currentExcuse} />
</div>
<div>
<div className="px-6 py-2 bg-blue-600 text-white"> Saved excuses: </div>
{excuses && <ExcuseList excuses={excuses} />}
</div>
</div>
)
}

const ExcuseList = (props) => {
return props.excuses?.length ? props.excuses.map((excuse, idx) => <Excuse excuse={excuse} key={idx} />) : 'No saved excuses'
}

const Excuse = ({ excuse }) => {
return (
<div className="px-6 py-2">
{excuse.text}
</div>
)
}

export default MainPage

Our page consists of three components. MainPage, ExcuseList and Excuse. It may seem at first that this file is pretty complex. It’s not, so let’s look a bit closer.

Excuse is just a div with an excuse text, ExcuseList checks if there are any excuses. If the list is empty – show a message No saved excuses. In other case – excuses will be displayed.

MainPage contains info about the current excuses and the list of already saved excuses. Two buttons click handlers handleGetExcuse and handleSaveExcuse. Plus, the markup itself with some Tailwind flavor.

4) Before starting an app – we need to execute database migration because we changed the DB schema by adding new entities. If you’ve had something running in the terminal – stop it and run:

wasp db migrate-dev

You’ll be prompted to enter a name for the migration. Something like init will be ok. Now we can start the application!

wasp start

Final empty result

Now you can click the “Get excuse” button to receive an excuse. And save the ones you like into the DB with the “Save excuse” button. Our final project should look like this:

Final result

It would have taken twice as long to build it properly.

Now we can think of some additional improvements. For example:

  • 1) Add a unique constraint to Entity’s ID so we won’t be able to save duplicated excuses.
  • 2) Add exceptions and edge cases handling.
  • 3) Make the markup prettier.
  • 4) Optimize and polish the code

So, we’ve been able to build a full-stack application with a database and external API call in a couple of minutes. And now we have a box full of excuses for all our development needs.

Box of excuses for the win!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/09/29/journey-to-1000-gh-stars.html b/blog/2022/09/29/journey-to-1000-gh-stars.html index 80f77e6666..c03d45b7cb 100644 --- a/blog/2022/09/29/journey-to-1000-gh-stars.html +++ b/blog/2022/09/29/journey-to-1000-gh-stars.html @@ -19,13 +19,13 @@ - - + +
-

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

· 12 min read
Matija Sosic

Wasp is an open-source configuration language for building full-stack web apps that integrates with React & Node.js. We launched first prototype 2 years ago, currently are at 1.9k stars on GitHub and will be releasing Beta in the coming months.

It was very hard for us to find and be able to learn from early inception stories of successful OSS projects and that's why we want to share what it looked like for Wasp.

1k stars chart

Before the stars: Is this really a problem? (1 year)

My co-founder and twin brother Martin and I got an initial idea for Wasp in 2018, while developing a web platform for running bioinformatics analysis in the cloud for one London-based startup.

It was our third or fourth time creating a full-stack app from scratch with the latest & hottest stack. This time, it was React/Node.js; for our previous projects, we went through PHP/Java/Node.js on the back-end and jQuery/Backbone/Angular on the front-end. Because Martin and I felt we were spending a lot of time relearning how to use the latest stack just to build the same features all over again (auth, CRUD, forms, async jobs, etc.), we asked ourselves: Why not abstract these common functionalities in a stack-agnostic, higher-level language (like e.g. SQL does for databases) to never reimplement them again?

Before we jumped into coding, we wanted to make sure this is a problem that actually exists and that we understand it well (enough). In our previous startup we found Customer Development (aka talking to users) extremely helpful, so we decided to do it again for Wasp.

In a month or so we conducted 25 problem interviews, probing around “What is your biggest challenge with web app development?” After we compiled the results, we identified the following four problems as the most significant ones and decided to focus on them in our v1:

  • It is hard to quickly start a new web app and make sure the best practices are being followed.
  • There is a lot of duplication/boilerplate in managing the state across front-end, back-end, and the database.
  • A lot of common features are re-implemented for every new app.
  • Developers are overwhelmed by the increasing tool complexity and don't want to be responsible for managing it.

We also clustered the answers we got by topics, so we could dive deeper and identify the areas that got the most attention:

Start and setup of a web app - problems
Interviewee problems regarding starting and setting up a new web app.

The reason why we stopped at 25 was that the answers started repeating themselves. We felt that we identified the initial patterns and were ready to move on.

0-180 ⭐️: First Contact (7 months)

After confirming and clarifying the problem with other developers, Martin and I felt we finally deserved to do some coding. (Ok, I admit, we had actually already started, but the interviews made us feel better about it 😀). We created a new repo on GitHub and started setting up the tooling & playing around with the concept.

For the next couple of months, we treated Wasp as a side project/experiment and didn’t do any marketing. However, we were well aware of how crucial external feedback is. So, once we built a very rudimentary code generation functionality, we also created a project page that we could share with others to explain what we’re working on and ask for feedback.

At that point, we came up with the first “real” name for Wasp - STIC: Specification To Implementation Compiler, as the big vision for Wasp was to be a stack-agnostic, specification language from which we could generate the actual code in e.g. React & Node.js or even some other stack.

STIC - first project page
Our first page for Wasp! Not the best at explaining what Wasp does, though.

Baby steps on Reddit and Hacker News

Our preferred way of distributing STIC project page was through relevant subreddits - r/webdev, r/coding, r/javascript, r/Haskell, r/ProgrammingLanguages, ….

This was the first Reddit post we’ve ever made about Wasp:

First Wasp post on Reddit
Our first Reddit post! We managed to get some feedback before we got banned.

One important thing we learned is that Reddit doesn’t like self-promotion. Sometimes, even if you’re only asking for feedback, the mods (and bots) will see it as self-promo and ban your post. It depends a lot on the mods, though. Reaching out to them and asking for explanation sometimes helps, but not very often. All subreddits have their own rules and guidelines that describe when or how it is OK to post about your project (e.g., /r/webdev has “Showoff Saturdays”), and we tried to follow them as best as we could.

After Reddit, we also launched on HN. This was our first ever launch there! We scored 20 points and received a few motivating comments:

First Wasp post on Reddit

Listening to users

Martin and I also followed up with the people we had previously interviewed about their problems in web dev. We showed them STIC project page and asked for comments. From all the feedback we captured, we identified the following issues:

  • Developers were not familiar with a term “DSL.” Almost all of us use a DSL on a daily basis (e.g., SQL, HCL (Terraform), HTML), but it’s not a popular term.
  • Developers feared learning a new programming language. Although our goal was never to replace e.g. Java or Typescript but to make Wasp work alongside it, we discovered that we had failed to communicate it well. Our messaging made developers feel they have to drop all their previous knowledge and start from scratch if they want to use Wasp.
  • Nobody could try Wasp yet + there wasn’t any documentation besides the project page. Our code was public, but we didn’t have a build/distribution system yet. Only a devoted Haskell developer could build it from the source. This made it hard for developers to buy into the high-level vision, as there was nothing they could hold onto. Web frameworks/languages are very “tactile” — it’s hard to judge one without trying it out.

180-300 ⭐️ : Anybody can try Wasp out + Docs = Alpha! (3 months)

After processing this feedback, we realized that the next step for us was to get Wasp into the condition where developers can easily try it out without needing any extra knowledge or facing the trouble of compiling from the source. That meant polishing things a bit, adding a few crucial features, and writing our first documentation, so that users would know how to use it.

To write our docs, we picked Docusaurus — an OSS writing platform made by Facebook. We saw several other OSS projects using it for their docs + its ability to import React in your markdown was amazing. Docusaurus gave us a lot of initial structure, design and features (e.g., search), saving us from reinventing the wheel.

First Wasp docs
Martin made sure to add a huge Alpha warning sign :D

Our M.O. at the time was to focus pretty much exclusively on one thing, either development or community. Since Wasp team consisted of only Martin and me, it was really hard to do multiple things at once. After the docs were out and Wasp was ready to be easily downloaded, we called this version “Alpha” and switched once again into the “community” mode.

300-570 ⭐️ : Big break on Reddit and Product Hunt (2 months)

Once Alpha was out, we launched again on HackerNews and drew a bit of attention (34 upvotes and 3 comments). However, that was little compared to our Reddit launches, where we scored 263 upvotes on r/javascript and 365 upvotes on r/reactjs:

Big break on Reddit
They love me! [insert Tobey Maguire as Spiderman]

Compared to the volume of attention and feedback we’ve been previously receiving, this was a big surprise for us! Here are some of the changes in messaging that we made for the Reddit launches:

  • Put prefix “declarative” in front of the “language” to convey that it’s not a regular programming language like Python or Javascript but rather something much more lightweight and specialized.
  • Emphasized that Wasp is not a standalone language that will replace your current stack but rather a “glue” between your React & Node.js code, allowing you to keep using your favourite stack.
  • Focused on the benefits like “less boilerplate,” which is a well known pain in web development.
Docs made the difference

Once we added the docs, we noticed a peculiar thing: developers became much less trigger-happy to criticize the project, especially in a non-constructive way. Our feeling was the majority of developers who were checking Wasp out still didn’t read the docs in detail (or at all), but the sheer existence of them made them feel there is more content they should go through before passing the final judgment.

Winning #1 Product of The Day on Product Hunt

After HN and Reddit, we continued with the “Alpha launch” mindset and set ourselves to launch Wasp on Product Hunt. It was our first time ever launching on PH, so we didn’t know what to expect. We googled some advice, did maybe a week of preparation (i.e., wrote the copy, asked a few friends to share their experiences with Wasp once we’re live), and that was it.

We launched Wasp on PH on Dec 6, 2020 and it ended up as Product of the day! That gave us a boost in stars and overall traction. Another benefit of PH was that Wasp also ended up in their daily newsletter, which supposedly has over a million subscribers. All this gave us quite a boost and visibility increase.

Product Hunt launch

570-1000 ⭐️ : Wasp joins YC + “Official” HN launch (2.5 months)

Soon after Product Hunt, Wasp joined Y Combinator for their W21 batch. We had applied two times before and always made it to the interviews, but did not get in. This time, the traction tipped the scales in our favour. (You can read more about our journey to YC here.)

For the first month of YC, there was a lot of admin and setup work to deal with alongside the regular program. That added a third dimension to our existing two areas of effort. Once we went past that, we could again put more focus on product and community development.

Our next milestone was to launch Wasp on Hacker News, but this time “officially” as a YC-backed company. Hacker News provides a lot of good tips on how to successfully launch and 80% of the advice applies even if your product isn’t backed by YC. I wish I had known about it before. The gist of the advice is to write in a clear and succinct way and to avoid buzzwords, superlatives, and salesy tone above all. Consider HN readers as your peers and explain what you do in a way you would talk to a friend over a drink. It really works that way.

We went through the several iterations of the text, sweated over how it’s gonna go, and when the day finally came — we launched! It went beyond all our expectations. With 222 points and 79 comments, our HN launch was one of the most successful launches (#9) out of 300+ companies in the W21 batch. Many developers and VCs that checked our launch afterwards were surprised how much positive feedback Wasp received, especially given how honest and direct HN audience can be.

HN launch brought us about 200 stars right away, and the rest came in the following weeks. As it was February and the YC program was nearing its end, we needed to shift gears again and focus on fundraising. This put all the other efforts on the back burner. (You can read about our fundraising learnings from 250+ meetings in 98 days here.) But the interest of the community remained and even without much activity from our side they kept coming and trying Wasp out.

YC HN launch

Conclusion: understanding users > number of stars

Our primary goal was never to reach X stars, but rather to understand how we can make Wasp more helpful so that developers would want to use it for their projects. As you could read above, even well before we started a repository we made sure to talk to developers and learn about their problems.

We also kept continually improving how we present Wasp - had we not pivoted our message from “Wasp is a new programming language” to “Wasp is a simple config language that works alongside React & Node.js” we wouldn’t have been where we are today.

On the other hand, stars have become an unofficial “currency” of GitHub and developers and VCs alike consider it when evaluating a project. They shouldn’t be disregarded and you should make it easy for users who like your product to express their support by starring your repo (like I’m doing right here), but that should always be a second order of concern.

Good luck!

I hope you found this helpful and that we shed some light on how things can look like in the early stages of an OSS project. Also, keep in mind this was our singular experience and that every story is different, so take everything with a grain of salt and pick only what makes sense for you and your product.

We wish you the best of luck and feel free to reach out if you'll have any questions!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

· 12 min read
Matija Sosic

Wasp is an open-source configuration language for building full-stack web apps that integrates with React & Node.js. We launched first prototype 2 years ago, currently are at 1.9k stars on GitHub and will be releasing Beta in the coming months.

It was very hard for us to find and be able to learn from early inception stories of successful OSS projects and that's why we want to share what it looked like for Wasp.

1k stars chart

Before the stars: Is this really a problem? (1 year)

My co-founder and twin brother Martin and I got an initial idea for Wasp in 2018, while developing a web platform for running bioinformatics analysis in the cloud for one London-based startup.

It was our third or fourth time creating a full-stack app from scratch with the latest & hottest stack. This time, it was React/Node.js; for our previous projects, we went through PHP/Java/Node.js on the back-end and jQuery/Backbone/Angular on the front-end. Because Martin and I felt we were spending a lot of time relearning how to use the latest stack just to build the same features all over again (auth, CRUD, forms, async jobs, etc.), we asked ourselves: Why not abstract these common functionalities in a stack-agnostic, higher-level language (like e.g. SQL does for databases) to never reimplement them again?

Before we jumped into coding, we wanted to make sure this is a problem that actually exists and that we understand it well (enough). In our previous startup we found Customer Development (aka talking to users) extremely helpful, so we decided to do it again for Wasp.

In a month or so we conducted 25 problem interviews, probing around “What is your biggest challenge with web app development?” After we compiled the results, we identified the following four problems as the most significant ones and decided to focus on them in our v1:

  • It is hard to quickly start a new web app and make sure the best practices are being followed.
  • There is a lot of duplication/boilerplate in managing the state across front-end, back-end, and the database.
  • A lot of common features are re-implemented for every new app.
  • Developers are overwhelmed by the increasing tool complexity and don't want to be responsible for managing it.

We also clustered the answers we got by topics, so we could dive deeper and identify the areas that got the most attention:

Start and setup of a web app - problems
Interviewee problems regarding starting and setting up a new web app.

The reason why we stopped at 25 was that the answers started repeating themselves. We felt that we identified the initial patterns and were ready to move on.

0-180 ⭐️: First Contact (7 months)

After confirming and clarifying the problem with other developers, Martin and I felt we finally deserved to do some coding. (Ok, I admit, we had actually already started, but the interviews made us feel better about it 😀). We created a new repo on GitHub and started setting up the tooling & playing around with the concept.

For the next couple of months, we treated Wasp as a side project/experiment and didn’t do any marketing. However, we were well aware of how crucial external feedback is. So, once we built a very rudimentary code generation functionality, we also created a project page that we could share with others to explain what we’re working on and ask for feedback.

At that point, we came up with the first “real” name for Wasp - STIC: Specification To Implementation Compiler, as the big vision for Wasp was to be a stack-agnostic, specification language from which we could generate the actual code in e.g. React & Node.js or even some other stack.

STIC - first project page
Our first page for Wasp! Not the best at explaining what Wasp does, though.

Baby steps on Reddit and Hacker News

Our preferred way of distributing STIC project page was through relevant subreddits - r/webdev, r/coding, r/javascript, r/Haskell, r/ProgrammingLanguages, ….

This was the first Reddit post we’ve ever made about Wasp:

First Wasp post on Reddit
Our first Reddit post! We managed to get some feedback before we got banned.

One important thing we learned is that Reddit doesn’t like self-promotion. Sometimes, even if you’re only asking for feedback, the mods (and bots) will see it as self-promo and ban your post. It depends a lot on the mods, though. Reaching out to them and asking for explanation sometimes helps, but not very often. All subreddits have their own rules and guidelines that describe when or how it is OK to post about your project (e.g., /r/webdev has “Showoff Saturdays”), and we tried to follow them as best as we could.

After Reddit, we also launched on HN. This was our first ever launch there! We scored 20 points and received a few motivating comments:

First Wasp post on Reddit

Listening to users

Martin and I also followed up with the people we had previously interviewed about their problems in web dev. We showed them STIC project page and asked for comments. From all the feedback we captured, we identified the following issues:

  • Developers were not familiar with a term “DSL.” Almost all of us use a DSL on a daily basis (e.g., SQL, HCL (Terraform), HTML), but it’s not a popular term.
  • Developers feared learning a new programming language. Although our goal was never to replace e.g. Java or Typescript but to make Wasp work alongside it, we discovered that we had failed to communicate it well. Our messaging made developers feel they have to drop all their previous knowledge and start from scratch if they want to use Wasp.
  • Nobody could try Wasp yet + there wasn’t any documentation besides the project page. Our code was public, but we didn’t have a build/distribution system yet. Only a devoted Haskell developer could build it from the source. This made it hard for developers to buy into the high-level vision, as there was nothing they could hold onto. Web frameworks/languages are very “tactile” — it’s hard to judge one without trying it out.

180-300 ⭐️ : Anybody can try Wasp out + Docs = Alpha! (3 months)

After processing this feedback, we realized that the next step for us was to get Wasp into the condition where developers can easily try it out without needing any extra knowledge or facing the trouble of compiling from the source. That meant polishing things a bit, adding a few crucial features, and writing our first documentation, so that users would know how to use it.

To write our docs, we picked Docusaurus — an OSS writing platform made by Facebook. We saw several other OSS projects using it for their docs + its ability to import React in your markdown was amazing. Docusaurus gave us a lot of initial structure, design and features (e.g., search), saving us from reinventing the wheel.

First Wasp docs
Martin made sure to add a huge Alpha warning sign :D

Our M.O. at the time was to focus pretty much exclusively on one thing, either development or community. Since Wasp team consisted of only Martin and me, it was really hard to do multiple things at once. After the docs were out and Wasp was ready to be easily downloaded, we called this version “Alpha” and switched once again into the “community” mode.

300-570 ⭐️ : Big break on Reddit and Product Hunt (2 months)

Once Alpha was out, we launched again on HackerNews and drew a bit of attention (34 upvotes and 3 comments). However, that was little compared to our Reddit launches, where we scored 263 upvotes on r/javascript and 365 upvotes on r/reactjs:

Big break on Reddit
They love me! [insert Tobey Maguire as Spiderman]

Compared to the volume of attention and feedback we’ve been previously receiving, this was a big surprise for us! Here are some of the changes in messaging that we made for the Reddit launches:

  • Put prefix “declarative” in front of the “language” to convey that it’s not a regular programming language like Python or Javascript but rather something much more lightweight and specialized.
  • Emphasized that Wasp is not a standalone language that will replace your current stack but rather a “glue” between your React & Node.js code, allowing you to keep using your favourite stack.
  • Focused on the benefits like “less boilerplate,” which is a well known pain in web development.
Docs made the difference

Once we added the docs, we noticed a peculiar thing: developers became much less trigger-happy to criticize the project, especially in a non-constructive way. Our feeling was the majority of developers who were checking Wasp out still didn’t read the docs in detail (or at all), but the sheer existence of them made them feel there is more content they should go through before passing the final judgment.

Winning #1 Product of The Day on Product Hunt

After HN and Reddit, we continued with the “Alpha launch” mindset and set ourselves to launch Wasp on Product Hunt. It was our first time ever launching on PH, so we didn’t know what to expect. We googled some advice, did maybe a week of preparation (i.e., wrote the copy, asked a few friends to share their experiences with Wasp once we’re live), and that was it.

We launched Wasp on PH on Dec 6, 2020 and it ended up as Product of the day! That gave us a boost in stars and overall traction. Another benefit of PH was that Wasp also ended up in their daily newsletter, which supposedly has over a million subscribers. All this gave us quite a boost and visibility increase.

Product Hunt launch

570-1000 ⭐️ : Wasp joins YC + “Official” HN launch (2.5 months)

Soon after Product Hunt, Wasp joined Y Combinator for their W21 batch. We had applied two times before and always made it to the interviews, but did not get in. This time, the traction tipped the scales in our favour. (You can read more about our journey to YC here.)

For the first month of YC, there was a lot of admin and setup work to deal with alongside the regular program. That added a third dimension to our existing two areas of effort. Once we went past that, we could again put more focus on product and community development.

Our next milestone was to launch Wasp on Hacker News, but this time “officially” as a YC-backed company. Hacker News provides a lot of good tips on how to successfully launch and 80% of the advice applies even if your product isn’t backed by YC. I wish I had known about it before. The gist of the advice is to write in a clear and succinct way and to avoid buzzwords, superlatives, and salesy tone above all. Consider HN readers as your peers and explain what you do in a way you would talk to a friend over a drink. It really works that way.

We went through the several iterations of the text, sweated over how it’s gonna go, and when the day finally came — we launched! It went beyond all our expectations. With 222 points and 79 comments, our HN launch was one of the most successful launches (#9) out of 300+ companies in the W21 batch. Many developers and VCs that checked our launch afterwards were surprised how much positive feedback Wasp received, especially given how honest and direct HN audience can be.

HN launch brought us about 200 stars right away, and the rest came in the following weeks. As it was February and the YC program was nearing its end, we needed to shift gears again and focus on fundraising. This put all the other efforts on the back burner. (You can read about our fundraising learnings from 250+ meetings in 98 days here.) But the interest of the community remained and even without much activity from our side they kept coming and trying Wasp out.

YC HN launch

Conclusion: understanding users > number of stars

Our primary goal was never to reach X stars, but rather to understand how we can make Wasp more helpful so that developers would want to use it for their projects. As you could read above, even well before we started a repository we made sure to talk to developers and learn about their problems.

We also kept continually improving how we present Wasp - had we not pivoted our message from “Wasp is a new programming language” to “Wasp is a simple config language that works alongside React & Node.js” we wouldn’t have been where we are today.

On the other hand, stars have become an unofficial “currency” of GitHub and developers and VCs alike consider it when evaluating a project. They shouldn’t be disregarded and you should make it easy for users who like your product to express their support by starring your repo (like I’m doing right here), but that should always be a second order of concern.

Good luck!

I hope you found this helpful and that we shed some light on how things can look like in the early stages of an OSS project. Also, keep in mind this was our singular experience and that every story is different, so take everything with a grain of salt and pick only what makes sense for you and your product.

We wish you the best of luck and feel free to reach out if you'll have any questions!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/10/28/farnance-hackathon-winner.html b/blog/2022/10/28/farnance-hackathon-winner.html index a292d8ad87..c725d2fc0b 100644 --- a/blog/2022/10/28/farnance-hackathon-winner.html +++ b/blog/2022/10/28/farnance-hackathon-winner.html @@ -19,13 +19,13 @@ - - + +
-

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

· 4 min read
Matija Sosic

farnance hero shot

Julian LaNeve is an engineer and data scientist who currently works at Astronomer.io as a Product Manager. In his free time, he enjoys playing poker, chess and winning data science competitions.

His project, Farnance, is a SaaS marketplace that allows farmers to transform their production into a digital asset on blockchain. Julian and his team developed Farnance as a part of the London Business School’s annual hackathon HackLBS 2021, and ended up as winners among more than 250 participants competing for 6 prizes in total!

Read on to learn why Julian chose Wasp to develop and deploy Farnance and what parts he enjoyed the most.

Finding a perfect React & Node.js hackathon setup

Julian had previous experiences with React and Node.js and loved that he could use JavaScript across the stack, but setting up a new project and making sure it uses all the latest packages (and then also figuring out how to deploy it) was always a pain. Since the hackathon only lasted for two days, he needed a quick way to get started but still have the freedom to use his favourite stack.

The power of one-line auth and No-API approach

Julian first learned about Wasp when it launched on HN and decided it would be a perfect tool for his case. The whole app setup, across the full stack, is covered out-of-the-box, simply by typing wasp new farnance, and he is ready to start writing own React & Node.js code.

Except on the app setup, the team saved a ton of time by not needing to implement the authentication and a typical CRUD API, since it is covered by Wasp as well. They could also deploy everything for free on Heroku and Netlify in just a few steps, which was a perfect fit for a hackathon.

Julian's testimonial on Discord

Farnance is still running and you can try it out here! The source code is also publicly available, although note it is running on older version of Wasp so some things are a bit different.

Spend more time developing features and less time reinventing the wheel

Julian was amazed by how fast he was able to get Farnance of the ground and share a working web app with the users! He decided to go with Google's material-ui for an UI framework which gave his app an instant professional look, although they didn’t have a dedicated designer on the team.

With all the common web app features (setup, auth, CRUD API) being taken care of by Wasp out-of-the-box they could invest all the time saved in developing and refining their unique features which in the end brought them victory!

I’ve done plenty of hackathons before where I’ve built small SaaS apps, and there’s just so much time wasted setting up common utilities - stuff like user management, databases, routing, etc. Wasp handled all that for me and let me build out our web app in record time

— Julian LaNeve - Farnance

Farnance's dashboard
Farnance dashboard in action!

Start quickly, but also scale without worries

note

Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

As such, we have updated our Deployment docs with new recommendations: https://wasp-lang.dev/docs/deploying

Since Wasp compiler generates a full-stack React & Node.js app under the hood, there aren’t any technical limitations to scaling Julian’s app as it grows and gets more users in the future. By running wasp build inside a project folder, developers gets both frontend files and a Dockerfile for the backend, which can then be deployed as any regular web app to the platform of your choice.

Wasp provides step-by step instructions on how to do it with Netlify and Fly.io for free, but we plan to add even more examples and more integrated deployment experience in the coming releases!

Deploying the wasp app was incredibly easy - I didn’t have time to stand up full infrastructure in the 2 day hackathon and don’t have an infra/devops background, but I had something running on Netlify within an hour. Other projects at the hackathon struggled to do this, and putting access in the hands of the judges certainly helped get us 1st place.

— Julian LaNeve - Farnance

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

· 4 min read
Matija Sosic

farnance hero shot

Julian LaNeve is an engineer and data scientist who currently works at Astronomer.io as a Product Manager. In his free time, he enjoys playing poker, chess and winning data science competitions.

His project, Farnance, is a SaaS marketplace that allows farmers to transform their production into a digital asset on blockchain. Julian and his team developed Farnance as a part of the London Business School’s annual hackathon HackLBS 2021, and ended up as winners among more than 250 participants competing for 6 prizes in total!

Read on to learn why Julian chose Wasp to develop and deploy Farnance and what parts he enjoyed the most.

Finding a perfect React & Node.js hackathon setup

Julian had previous experiences with React and Node.js and loved that he could use JavaScript across the stack, but setting up a new project and making sure it uses all the latest packages (and then also figuring out how to deploy it) was always a pain. Since the hackathon only lasted for two days, he needed a quick way to get started but still have the freedom to use his favourite stack.

The power of one-line auth and No-API approach

Julian first learned about Wasp when it launched on HN and decided it would be a perfect tool for his case. The whole app setup, across the full stack, is covered out-of-the-box, simply by typing wasp new farnance, and he is ready to start writing own React & Node.js code.

Except on the app setup, the team saved a ton of time by not needing to implement the authentication and a typical CRUD API, since it is covered by Wasp as well. They could also deploy everything for free on Heroku and Netlify in just a few steps, which was a perfect fit for a hackathon.

Julian's testimonial on Discord

Farnance is still running and you can try it out here! The source code is also publicly available, although note it is running on older version of Wasp so some things are a bit different.

Spend more time developing features and less time reinventing the wheel

Julian was amazed by how fast he was able to get Farnance of the ground and share a working web app with the users! He decided to go with Google's material-ui for an UI framework which gave his app an instant professional look, although they didn’t have a dedicated designer on the team.

With all the common web app features (setup, auth, CRUD API) being taken care of by Wasp out-of-the-box they could invest all the time saved in developing and refining their unique features which in the end brought them victory!

I’ve done plenty of hackathons before where I’ve built small SaaS apps, and there’s just so much time wasted setting up common utilities - stuff like user management, databases, routing, etc. Wasp handled all that for me and let me build out our web app in record time

— Julian LaNeve - Farnance

Farnance's dashboard
Farnance dashboard in action!

Start quickly, but also scale without worries

note

Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

As such, we have updated our Deployment docs with new recommendations: https://wasp-lang.dev/docs/deploying

Since Wasp compiler generates a full-stack React & Node.js app under the hood, there aren’t any technical limitations to scaling Julian’s app as it grows and gets more users in the future. By running wasp build inside a project folder, developers gets both frontend files and a Dockerfile for the backend, which can then be deployed as any regular web app to the platform of your choice.

Wasp provides step-by step instructions on how to do it with Netlify and Fly.io for free, but we plan to add even more examples and more integrated deployment experience in the coming releases!

Deploying the wasp app was incredibly easy - I didn’t have time to stand up full infrastructure in the 2 day hackathon and don’t have an infra/devops background, but I had something running on Netlify within an hour. Other projects at the hackathon struggled to do this, and putting access in the hands of the judges certainly helped get us 1st place.

— Julian LaNeve - Farnance

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/15/auth-feature-announcement.html b/blog/2022/11/15/auth-feature-announcement.html index 09d63588c5..67c08b36ce 100644 --- a/blog/2022/11/15/auth-feature-announcement.html +++ b/blog/2022/11/15/auth-feature-announcement.html @@ -19,13 +19,13 @@ - - + +
-

Feature Announcement - New auth method (Google)

· 4 min read
Shayne Czyzewski

No login for you!

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.

Prologue

We've all been there. Your app needs to support user authentication with social login, and you must now decide what to do next. Should you eschew the collective experience and wisdom of the crowd and YOLO it by rolling your own, praying you don't get pwned in prod? "Nah, I just ate some week-old sushi and can't take another risk that big anytime soon.", you rightly think.

Ok, surely you can just use a library, right? Open source software, baby! "Hmm, seems Library X, Y, and Z are all somewhat used, each with their pros/cons, nuances, and integration pain points. Oh wait, there are tutorials for each... but each says how hard they are to correctly set up and use. I scoped this feature for one day, not a one-week hair-pulling adventure (Dang scrum! Who likes it anyways? Oh yeah, PMs do. Dang PMs!)." Ok, something else. You need to brainstorm. You instead start to surf Twitter and see an ad for some unicorn auth startup.

Eureka, you can go with a third-party SaaS offering! "We shouldn't have to pay for a while (I think? hope!), and it's just another dependency, no biggie... #microservices, right?" "But what about outages, data privacy, mapping users between systems, and all that implicit trust you are placing in them?" you think. "What happens when Elon buys them next?" You gasp as if you walked by a Patagonia vest covered in that hot new Burnt Hair cologne.

"All I want is username and password auth with Google login support, why is that so hard in 2022?!? I miss Basic HTTP auth headers. I think I'll move off the grid and become a woodworker."

Easy auth setup in Wasp

Wasp helps that dev by taking care of the entire auth setup process out of the box. Adding support for username and password auth, plus Google login, is super quick and easy for Wasp apps. We think this makes adding auth fast and convenient, with no external dependencies or frustrating manual configuration. Here’s how it works:

Step 1 - Add the appropriate models

We need to store user info and the external mapping association for social logins. Here is an example you can start from and add new fields to:

./main.wasp
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
externalAuthAssociations SocialLogin[]
psl=}

entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

Step 2 - Update app.auth to use these items

./main.wasp
app authExample {
// ...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
},
onAuthFailedRedirectTo: "/login"
}
}

Step 3 - Get Google credentials and add environment variables

Follow the Google setup guide here and add the environment variables to your .env.server file.

Step 4 - Make use of the Google login button in your Login page component

./src/client/auth/Login.js
import React from 'react'
import { Link } from 'react-router-dom'

import { SignInButton as GoogleSignInButton } from '@wasp/auth/helpers/Google'
import LoginForm from '@wasp/auth/forms/Login'

const Login = () => {
return (
<div>
<div>
<LoginForm/>
</div>
<div>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</div>
<div>
<GoogleSignInButton/>
</div>
</div>
)
}

export default Login

Step 5 - Run the app!

Epilogue

No need to move off the grid out of frustration when adding authentication and social login to your web app. Here is a complete, minimal example if you want to jump right in, and here are the full docs for more info. With just a few simple steps above, we've added authentication with best practices baked into our app so we can move on to solving problems that add value to our users!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Feature Announcement - New auth method (Google)

· 4 min read
Shayne Czyzewski

No login for you!

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.

Prologue

We've all been there. Your app needs to support user authentication with social login, and you must now decide what to do next. Should you eschew the collective experience and wisdom of the crowd and YOLO it by rolling your own, praying you don't get pwned in prod? "Nah, I just ate some week-old sushi and can't take another risk that big anytime soon.", you rightly think.

Ok, surely you can just use a library, right? Open source software, baby! "Hmm, seems Library X, Y, and Z are all somewhat used, each with their pros/cons, nuances, and integration pain points. Oh wait, there are tutorials for each... but each says how hard they are to correctly set up and use. I scoped this feature for one day, not a one-week hair-pulling adventure (Dang scrum! Who likes it anyways? Oh yeah, PMs do. Dang PMs!)." Ok, something else. You need to brainstorm. You instead start to surf Twitter and see an ad for some unicorn auth startup.

Eureka, you can go with a third-party SaaS offering! "We shouldn't have to pay for a while (I think? hope!), and it's just another dependency, no biggie... #microservices, right?" "But what about outages, data privacy, mapping users between systems, and all that implicit trust you are placing in them?" you think. "What happens when Elon buys them next?" You gasp as if you walked by a Patagonia vest covered in that hot new Burnt Hair cologne.

"All I want is username and password auth with Google login support, why is that so hard in 2022?!? I miss Basic HTTP auth headers. I think I'll move off the grid and become a woodworker."

Easy auth setup in Wasp

Wasp helps that dev by taking care of the entire auth setup process out of the box. Adding support for username and password auth, plus Google login, is super quick and easy for Wasp apps. We think this makes adding auth fast and convenient, with no external dependencies or frustrating manual configuration. Here’s how it works:

Step 1 - Add the appropriate models

We need to store user info and the external mapping association for social logins. Here is an example you can start from and add new fields to:

./main.wasp
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
externalAuthAssociations SocialLogin[]
psl=}

entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

Step 2 - Update app.auth to use these items

./main.wasp
app authExample {
// ...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
},
onAuthFailedRedirectTo: "/login"
}
}

Step 3 - Get Google credentials and add environment variables

Follow the Google setup guide here and add the environment variables to your .env.server file.

Step 4 - Make use of the Google login button in your Login page component

./src/client/auth/Login.js
import React from 'react'
import { Link } from 'react-router-dom'

import { SignInButton as GoogleSignInButton } from '@wasp/auth/helpers/Google'
import LoginForm from '@wasp/auth/forms/Login'

const Login = () => {
return (
<div>
<div>
<LoginForm/>
</div>
<div>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</div>
<div>
<GoogleSignInButton/>
</div>
</div>
)
}

export default Login

Step 5 - Run the app!

Epilogue

No need to move off the grid out of frustration when adding authentication and social login to your web app. Here is a complete, minimal example if you want to jump right in, and here are the full docs for more info. With just a few simple steps above, we've added authentication with best practices baked into our app so we can move on to solving problems that add value to our users!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/16/alpha-testing-program-post-mortem.html b/blog/2022/11/16/alpha-testing-program-post-mortem.html index 2050a48516..7314ae303c 100644 --- a/blog/2022/11/16/alpha-testing-program-post-mortem.html +++ b/blog/2022/11/16/alpha-testing-program-post-mortem.html @@ -19,13 +19,13 @@ - - + +
-

Alpha Testing Program: post-mortem

· 7 min read
Matija Sosic

We are working on a new web framework that integrates with React & Node.js, and also happens to be a language. As you can probably imagine, it’s not easy to get people to use a new piece of technology, especially while still in Alpha. On the other hand, without users and their feedback, it’s impossible to know what to build.

That is why we ran Alpha Testing Program for Wasp - here is what we learned and what went both well and wrong along the way.

twitter DM - shared atp in swag groups

“Of course I know about Wasp! I just haven’t come around to trying it out yet.”

Although we hit the front page of HN several times and are about to reach 2,000 stars on GitHub, there is still a big difference between a person starring a repo and actually sitting down and building something with it.

Talking to people, we realised a lot of them had heard of Wasp, thought it was a neat idea, but hadn’t tried it out. These were the main reasons:

  • having to find 30 mins to go through our Build a Todo App tutorial - “I'm busy now, but I’ll do it next week.”
  • building a bare-bones todo app is not that exciting
  • not having an idea what else to build
  • “the product is still in alpha, so I will bookmark it for later”

These are all obvious and understandable reasons. I must admit, I’m much the same — maybe even worse — when it comes to trying out something new/unproven. It just isn’t a priority, and without a push that will help me overcome all these objections, I usually don’t have an incentive to go through with it.

Having realised all that, we understood we needed to give people a reason to try Wasp out now, because that’s when we needed the feedback, not next week.

Welcome to Wasp Alpha Testing Program!

The team
I was having a bit too much fun here, but Portal fans will understand.

We quickly put together an admissions page for alpha testers in Notion (you can see it here) and started sharing it around. To counter the hurdles we mentioned above, we time-boxed the program (”this is happening now and you have 48 hours to finish once you start”) and promised a t-shirt to everyone that goes through the tutorial and fills out the feedback form.

Apply to ATP - CTA
CTA from the admissions page

Soon, the first applications started trickling in! For each new applicant, we’d follow up with the instructions on how to successfully go through the Alpha Testing Program:

  • fill out intro form (years of experience, preferred stack, etc)
  • go through our “build a Todo app” tutorial
  • fill out the feedback form - what was good, what was bad etc.

Timeboxing
People were really respectful of this deadline and would politely ask to extend it in case they couldn’t make it.

But, soon after I got the following message on Twitter:

twitter DM - shared atp in swag groups

We got really scared that we would get a ton of folks putting in minimal effort while trying Wasp out just to get the free swag, leaving us empty-handed and having learned nothing! On the other hand, we didn’t have much choice since we didn’t define the “minimum required quality” of feedback in advance.

Luckily, it wasn’t the problem in the end, even the opposite -- we did get a surge of applications, but only a portion of them finished the program and the ones that did left really high-quality feedback!

How it went - test profile & feedback

Tester profile

We received 210 applications and 53 out of those completed the program — 25% completion rate.

We also surveyed applicants about their preferred stack, years of programming experience, etc:

Intro survey - tester profile
Yep, we like puns.

The feedback

The feedback form evaluated testers’ overall experience with Wasp. We asked them what they found to be the best and worst parts of working with Wasp, as well as about the next features they’d like to see.

Feedback survey - experience

The bad parts

What our testers were missing the most was a full-blown IDE and TypeScript support. Both of these are coming in Beta but only JS was supported at the time. Plus, there were some installation problems with Windows (which is not fully supported yet — best to use it through WSL).

Feedback survey - the bad parts

We were already aware that TypeScript support is an important feature, but didn’t have an exact feeling of how much - the feedback was really helpful and helped us prioritise our Beta backlog.

The good parts

Testers’ favourite part was the batteries-included experience, particularly the auth model.

Feedback survey - the good parts

Post-mortem: what didn’t go well

No threshold for feedback quality

Feedback quality

We didn’t put any kind of restrictions on the feedback form, e.g. minimal length of the feedback. That resulted in ~15%-20% of answers being single words, such as depicted above. I’m not sure if there is an efficient way to avoid this or just a stat to live with.

Using free text form for collecting addresses

It never crossed our minds before that validating addresses could be such an important part of shipping swag, but turns out it is. It seems that there are a lot of ways to specify an address, some of which are different from what is expected by our post office, resulting in a number of shipments getting returned.

An ideal solution would be to use a specialized “address” field in a survey that would auto-validate it, but turns out Typeform (which we used) doesn’t have that feature implemented yet, although it’s been highly requested.

Shipment returned

Shipment returned email

The non-obvious benefit of Alpha Testing Program

What went well is that we got a lot of high-quality feedback that steered and fortified our plan for the upcoming Beta release.

The other big benefit is that we finally solved the “looks cool but i’ll try it out later maybe” problem. Overall, our usage went well up during the program, but even after it ended, the baseline increased significantly. This was the second-order effect we didn’t foresee.

Our understanding is that once people finally gave it a try, a portion of them felt the value first-hand and decided to keep using it for other projects as well.

Alpha testing program - usage spike

Summary & going forward: Beta

The overall conclusion from our Alpha Testing Program is it was a worthy effort which got us valuable feedback and positively affected the overall usage. Moving forward we’ll try to focus on ensuring more quality feedback and prioritising 1-to-1 communication to make sure we fully understand what bothers Wasp users and what we can improve. It also might be helpful to do testing in smaller batches so we are not overwhelmed with responses and can focus on the individual testers - that’s something we might try out in Beta.

As mentioned, the next stop is Beta! It comes out on the 27th of November - sign up here to get notified.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Alpha Testing Program: post-mortem

· 7 min read
Matija Sosic

We are working on a new web framework that integrates with React & Node.js, and also happens to be a language. As you can probably imagine, it’s not easy to get people to use a new piece of technology, especially while still in Alpha. On the other hand, without users and their feedback, it’s impossible to know what to build.

That is why we ran Alpha Testing Program for Wasp - here is what we learned and what went both well and wrong along the way.

twitter DM - shared atp in swag groups

“Of course I know about Wasp! I just haven’t come around to trying it out yet.”

Although we hit the front page of HN several times and are about to reach 2,000 stars on GitHub, there is still a big difference between a person starring a repo and actually sitting down and building something with it.

Talking to people, we realised a lot of them had heard of Wasp, thought it was a neat idea, but hadn’t tried it out. These were the main reasons:

  • having to find 30 mins to go through our Build a Todo App tutorial - “I'm busy now, but I’ll do it next week.”
  • building a bare-bones todo app is not that exciting
  • not having an idea what else to build
  • “the product is still in alpha, so I will bookmark it for later”

These are all obvious and understandable reasons. I must admit, I’m much the same — maybe even worse — when it comes to trying out something new/unproven. It just isn’t a priority, and without a push that will help me overcome all these objections, I usually don’t have an incentive to go through with it.

Having realised all that, we understood we needed to give people a reason to try Wasp out now, because that’s when we needed the feedback, not next week.

Welcome to Wasp Alpha Testing Program!

The team
I was having a bit too much fun here, but Portal fans will understand.

We quickly put together an admissions page for alpha testers in Notion (you can see it here) and started sharing it around. To counter the hurdles we mentioned above, we time-boxed the program (”this is happening now and you have 48 hours to finish once you start”) and promised a t-shirt to everyone that goes through the tutorial and fills out the feedback form.

Apply to ATP - CTA
CTA from the admissions page

Soon, the first applications started trickling in! For each new applicant, we’d follow up with the instructions on how to successfully go through the Alpha Testing Program:

  • fill out intro form (years of experience, preferred stack, etc)
  • go through our “build a Todo app” tutorial
  • fill out the feedback form - what was good, what was bad etc.

Timeboxing
People were really respectful of this deadline and would politely ask to extend it in case they couldn’t make it.

But, soon after I got the following message on Twitter:

twitter DM - shared atp in swag groups

We got really scared that we would get a ton of folks putting in minimal effort while trying Wasp out just to get the free swag, leaving us empty-handed and having learned nothing! On the other hand, we didn’t have much choice since we didn’t define the “minimum required quality” of feedback in advance.

Luckily, it wasn’t the problem in the end, even the opposite -- we did get a surge of applications, but only a portion of them finished the program and the ones that did left really high-quality feedback!

How it went - test profile & feedback

Tester profile

We received 210 applications and 53 out of those completed the program — 25% completion rate.

We also surveyed applicants about their preferred stack, years of programming experience, etc:

Intro survey - tester profile
Yep, we like puns.

The feedback

The feedback form evaluated testers’ overall experience with Wasp. We asked them what they found to be the best and worst parts of working with Wasp, as well as about the next features they’d like to see.

Feedback survey - experience

The bad parts

What our testers were missing the most was a full-blown IDE and TypeScript support. Both of these are coming in Beta but only JS was supported at the time. Plus, there were some installation problems with Windows (which is not fully supported yet — best to use it through WSL).

Feedback survey - the bad parts

We were already aware that TypeScript support is an important feature, but didn’t have an exact feeling of how much - the feedback was really helpful and helped us prioritise our Beta backlog.

The good parts

Testers’ favourite part was the batteries-included experience, particularly the auth model.

Feedback survey - the good parts

Post-mortem: what didn’t go well

No threshold for feedback quality

Feedback quality

We didn’t put any kind of restrictions on the feedback form, e.g. minimal length of the feedback. That resulted in ~15%-20% of answers being single words, such as depicted above. I’m not sure if there is an efficient way to avoid this or just a stat to live with.

Using free text form for collecting addresses

It never crossed our minds before that validating addresses could be such an important part of shipping swag, but turns out it is. It seems that there are a lot of ways to specify an address, some of which are different from what is expected by our post office, resulting in a number of shipments getting returned.

An ideal solution would be to use a specialized “address” field in a survey that would auto-validate it, but turns out Typeform (which we used) doesn’t have that feature implemented yet, although it’s been highly requested.

Shipment returned

Shipment returned email

The non-obvious benefit of Alpha Testing Program

What went well is that we got a lot of high-quality feedback that steered and fortified our plan for the upcoming Beta release.

The other big benefit is that we finally solved the “looks cool but i’ll try it out later maybe” problem. Overall, our usage went well up during the program, but even after it ended, the baseline increased significantly. This was the second-order effect we didn’t foresee.

Our understanding is that once people finally gave it a try, a portion of them felt the value first-hand and decided to keep using it for other projects as well.

Alpha testing program - usage spike

Summary & going forward: Beta

The overall conclusion from our Alpha Testing Program is it was a worthy effort which got us valuable feedback and positively affected the overall usage. Moving forward we’ll try to focus on ensuring more quality feedback and prioritising 1-to-1 communication to make sure we fully understand what bothers Wasp users and what we can improve. It also might be helpful to do testing in smaller batches so we are not overwhelmed with responses and can focus on the individual testers - that’s something we might try out in Beta.

As mentioned, the next stop is Beta! It comes out on the 27th of November - sign up here to get notified.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/16/tailwind-feature-announcement.html b/blog/2022/11/16/tailwind-feature-announcement.html index 1af2b7d10e..82631c76c5 100644 --- a/blog/2022/11/16/tailwind-feature-announcement.html +++ b/blog/2022/11/16/tailwind-feature-announcement.html @@ -19,13 +19,13 @@ - - + +
-

Feature Announcement - Tailwind CSS support

· 3 min read
Shayne Czyzewski

Full stack devs

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.

There are backend devs who can do some frontend, and frontend devs who can do some backend. But the mythical full stack dev is exceedingly rare (or more likely, a lie). Even as someone who falls into the meme category above, we all still need to make websites that look noice. This is a place where CSS frameworks can help.

But which one should you use? According to our extensive research, a statistically-questionable-but-you’re-still-significant-to-us 11 people on Twitter wanted us to add better support for Tailwind. Which was lucky for us, since we already added it before asking them. 😅

Twitter voting

Ok, it wasn’t a huge stretch for us to do so preemptively. Tailwind is one of the most heavily used CSS frameworks out there today and seems to keep growing in popularity. So how do you integrate it into your Wasp apps? Like many things in Wasp, it’s really easy- just drop in two config files into the root of your project and you can then start using it! Here are the defaults:

./tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
./postcss.config.cjs
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

When these two files are present, Wasp will make sure all the required NPM dependencies get added, that PostCSS plays nicely with Tailwind directives in CSS files, and that your JavaScript files are properly processed so you can use all the CSS selectors you want (provided you are properly equipped :D).

Best monitor

With that in place, you can add the Tailwind directives to your CSS files like so:

./src/client/Main.css
@tailwind base;
@tailwind components;
@tailwind utilities;

/* rest of content below */

And then start using Tailwind classes in your components:

<h1 className="text-3xl font-bold underline">
Hello world!
</h1>

As usual, Wasp will still automatically reload your code and refresh the browser on any changes. 🥳

Lastly, here is a small example that shows how to add a few Tailwind plugins for the adventurous (wasp file and Tailwind config), and here are the docs for more details. We can’t wait to see what you make!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Feature Announcement - Tailwind CSS support

· 3 min read
Shayne Czyzewski

Full stack devs

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.

There are backend devs who can do some frontend, and frontend devs who can do some backend. But the mythical full stack dev is exceedingly rare (or more likely, a lie). Even as someone who falls into the meme category above, we all still need to make websites that look noice. This is a place where CSS frameworks can help.

But which one should you use? According to our extensive research, a statistically-questionable-but-you’re-still-significant-to-us 11 people on Twitter wanted us to add better support for Tailwind. Which was lucky for us, since we already added it before asking them. 😅

Twitter voting

Ok, it wasn’t a huge stretch for us to do so preemptively. Tailwind is one of the most heavily used CSS frameworks out there today and seems to keep growing in popularity. So how do you integrate it into your Wasp apps? Like many things in Wasp, it’s really easy- just drop in two config files into the root of your project and you can then start using it! Here are the defaults:

./tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
./postcss.config.cjs
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

When these two files are present, Wasp will make sure all the required NPM dependencies get added, that PostCSS plays nicely with Tailwind directives in CSS files, and that your JavaScript files are properly processed so you can use all the CSS selectors you want (provided you are properly equipped :D).

Best monitor

With that in place, you can add the Tailwind directives to your CSS files like so:

./src/client/Main.css
@tailwind base;
@tailwind components;
@tailwind utilities;

/* rest of content below */

And then start using Tailwind classes in your components:

<h1 className="text-3xl font-bold underline">
Hello world!
</h1>

As usual, Wasp will still automatically reload your code and refresh the browser on any changes. 🥳

Lastly, here is a small example that shows how to add a few Tailwind plugins for the adventurous (wasp file and Tailwind config), and here are the docs for more details. We can’t wait to see what you make!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/17/hacktoberfest-wrap-up.html b/blog/2022/11/17/hacktoberfest-wrap-up.html index bb84ac0b5f..76ea946e7d 100644 --- a/blog/2022/11/17/hacktoberfest-wrap-up.html +++ b/blog/2022/11/17/hacktoberfest-wrap-up.html @@ -19,15 +19,15 @@ - - + +
-

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

· 6 min read

2078 lines of code across 24 PRs were changed in Wasp repo during HacktoberFest 2022 - the most prominent online event for promoting and celebrating OSS culture. October has been a blast, to say the least, and the most active month in the repo's history.

This is the story of our journey along with the tips on leveraging Hacktoberfest to get your repo buzzing! 🐝🐝

How it went: the stats

Let's take a quick look at the charts below (data obtained from OSS Insight platform) 👇

PR history
24 contributor PRs in Oct, an all-time high!

Lines of code changes
On the other hand, number of changed LoC isn't that huge

While the number of PRs is at an all-time high, the number of updated lines of code is fewer than usual. If we take a look at the distribution of PR sizes in the first chart, we can see that "xs" and "s" PRs are in the majority (20 out of 24).

This brings us to our first conclusion: first-time contributors start with small steps! The main benefit here is getting potential contributors interested and familiar with the project, rather than expecting them to jump in and +

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

· 6 min read

2078 lines of code across 24 PRs were changed in Wasp repo during HacktoberFest 2022 - the most prominent online event for promoting and celebrating OSS culture. October has been a blast, to say the least, and the most active month in the repo's history.

This is the story of our journey along with the tips on leveraging Hacktoberfest to get your repo buzzing! 🐝🐝

How it went: the stats

Let's take a quick look at the charts below (data obtained from OSS Insight platform) 👇

PR history
24 contributor PRs in Oct, an all-time high!

Lines of code changes
On the other hand, number of changed LoC isn't that huge

While the number of PRs is at an all-time high, the number of updated lines of code is fewer than usual. If we take a look at the distribution of PR sizes in the first chart, we can see that "xs" and "s" PRs are in the majority (20 out of 24).

This brings us to our first conclusion: first-time contributors start with small steps! The main benefit here is getting potential contributors interested and familiar with the project, rather than expecting them to jump in and immediately start implementing the next major feature. Efforts like that require investing time to understand and digest codebase architecture, design decisions and the development process.

On the other hand, being able to implement and merge any feature, no matter the size, from beginning to the end, and to get your name on the list of contributors of your favourite project is an amazing feeling! That will make your contributors feel like superheroes and motivate them to keep taking on larger and larger chunks, and maybe eventually even join the core team!

Thus, the second conclusion would be: don’t underestimate the significance of small PRs! It's not about reducing your backlog, but rather encouraging developers to get engaged with your project in a friendly way.

tip

To make it easier for your new contributors, you can prepare in advance good issues to get started with - e.g. smaller bugs, docs improvements, fun but isolated problems, etc.

We added good-first-issue label to such issues in Wasp repo, and even added extra context such as no-haskell, webdev, example, docs.

With your repo being set, the next question is "How do I get people to pick my project to work on"? Relying solely on putting "Hacktoberfest" topic on your GitHub repo won't do the trick, not with thousands of other repos doing the same.

If you want to get noticed, you need to do marketing. A lot of it. The name of the game here is what you put in is what you get back. Let's talk about this in more detail.

A thin line between genuine interactions and annoying self-promotion

First and foremost, you'll need to create an entry point with all the necessary information for the participants. We opted for a GitHub issue where we categorized Hacktoberfest issues by type, complexity, etc, but it can be anything - a dedicated landing page, Medium/Dev.to article, or whatever works for you. Once you have that, you can start promoting it.

Hacktoberfest entry point - gh issue
Our entry point for Hacktoberfest

Our marketing strategy consisted of the following:

  1. Tweeting regularly - what's new, interesting issues, ...

  2. Writing meaningful Reddit posts about your achievements

  3. Hanging out in HacktoberFest Discord server, chatting with others and answering their questions

  4. Checking posts with appropriate tags on different blogging websites like Medium, Dev.to, Hashnode, etc. and participating in conversations.

There are plenty of other ways to advertise your project, like joining events or writing articles. Even meme contests. The activities mentioned above worked the best for us. Let’s dive a bit deeper.

Tweets are pretty obvious - as mentioned, you can share updates on how stuff is going. Tag contributors, inform your followers about available issues and mention those who might be a good fit for tackling them.

Reddit is a much more complex beast. You need to avoid clickbait post titles, comply with subreddit rules on self-promotion and try to give meaningful info to the community simultaneously. Take less than you give, and you’re good.

posting on reddit
How posting on Reddit feels

The Discord server marketing was pretty straightforward. There’s even a dedicated channel for self-promotion. In case you're not talkative much, dropping a link to your project is OK, and that’s it. On the other hand, the server is an excellent platform for discussing Hacktoberfest-related issues, approaches, and ideas. The more you chat, the higher your chances of drawing attention to your project.

The most engaging but also time consuming activity was commenting on blog posts of other Hacktoberfest participants. Pretending that you’re interested in the topic only to leave a self-promoting comment will not bring you anywhere - it can only result in your comment being removed. Make sure to provide value: add more information on the topic of the article, address specific points the author may have missed, or mention how you’ve dealt with the related issue in your project.

Be consistent and dedicate time to regularly to check new articles and jump into discussions. Share a link to your repo only if it fits into the flow of the conversation.

Content marketing in a nutshell

Was it worth it?

Before joining HacktoberFest as maintainers, we weren’t sure it would be worth the time investment. Our skepticism was reinforced by the following:

  1. Mentions of people submitting trivial PRs just to win the award

  2. The fact that we're making a relatively complex project (DSL for developing React + Node.js full-stack web apps with less code) and it might be hard for people to get into it

  3. The compiler is written is Haskell, with templates in JavaScript - again, not the very common project setup

Fortunately, none of this turned out to be a problem! We've got 24 valid PRs, both Haskell and non-Haskell, a ton of valuable feedback, and several dozen new users and community members.

Wrap up

Don’t expect magic to happen. HacktoberFest is all about smaller changes and getting community introduced to your project. Be ready to promote your repo genuinely and don’t be afraid to take part in the contest. We hope that helps and wish you the best of luck!

Remember, HacktoberFest is all about the celebration of open source. Stick to that principle, and you’ll get the results you could only wish for!

P.S. - Thanks to our contributors!

Massive shout out to our contributors: @ussgarci, @h4r1337, @d0m96, @EmmanuelCoder, @gautier_difolco, @vaishnav_mk1, @NeoLight1010, @abscubix, @JFarayola, @Shahx95 and everyone else for making it possible. You rock! 🤘

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/11/26/erlis-amicus-usecase.html b/blog/2022/11/26/erlis-amicus-usecase.html index fdd0baf193..e79c46a7b1 100644 --- a/blog/2022/11/26/erlis-amicus-usecase.html +++ b/blog/2022/11/26/erlis-amicus-usecase.html @@ -19,13 +19,13 @@ - - + +
-

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

· 5 min read
Matija Sosic

amicus hero shot

Erlis Kllogjri is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how Amicus started out.

Amicus is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.

Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!

Looking for a full-stack “all-in-one” solution, with React & Node.js

Erlis first learned about Wasp on HackerNews and it immediately caught his attention, particularly the configuration language part. One of the companies he worked at in the past had its own internal DSL in the hardware domain, and he understood how helpful it could be for moving fast and avoiding boilerplate.

Erlis also had previous experience in web development, especially on the front-end side in React and Javascript, so that made Wasp a logical choice.

I was looking at other solutions, but none of them were full-stack and sounded like a lot of work just to stitch everything together and get started. I just wanted to get the job done and didn’t care about picking the stack specifics myself. Wasp was really helpful as it set me up with the best practices and I had everything running in just a few minutes!

— Erlis Kllogjri - Amicus

Building Amicus v1.0 and getting first customers!

The idea for Amicus came from his brother, who is employed at a law firm - talking about their process and challenges in executing them, Erlis thought it would be an interesting side project, especially given there is a real problem to solve.

Soon, the first version of Amicus was live! It was made in a true lean startup fashion, starting with the essential features and immediately being tested with users.

Amicus's dashboard
Amicus's dashboard, using Material-UI

Erlis used Material-UI as a UI library since it came with one of the example apps built in Wasp (Beta introduced Tailwind support!). Users could track their clients, active legal matters and there was even integrated billing with Stripe! Amicus also extensively used Wasp’s Async Jobs feature to regularly update invoices, send reminder emails and clear out old data from the database.

After a few iterations with the legal team who were Amicus' test user (e.g. adding support for different types of users via roles), they were ready to get onboarded and become paying customers! More than 20 people from a single company are using Amicus daily for their work, making it an amazing source of continuous feedback for further development.

Erlis enjoyed the most how fast he could progress and ship features with Wasp on a weekly basis. Having both front-end, back-end, and database set and fully configured to work together from the beginning, he could focus on developing features rather than spend time figuring out the intricacies of the specific stack.

If it weren't for Wasp, Amicus would probably have never been finished. I estimate it saved me 100+ hours from the start and I'm still amazed that I did all this work as a team-of-one. Being able to quickly change existing features and add the new ones is the biggest advantage of Wasp for me.

— Erlis Kllogjri - Amicus

Beyond MVP with Wasp

Although Erlis already has a product running in production, with first paying customers, he wants to see how far he can take it and has a lot of ideas (also requests) for the next features. (Actually, Erlis had a big kanban board with post-its on a wall behind him as we were chatting, dedicated just to Amicus - that was impressive to see!).

Some of the most imminent ones are:

  • uploading and sharing files between lawyers and clients
  • usage logging and analytics
  • transactional emails for notifications

Since under the hood Wasp is generating code in today's mainstream, production-tested technologies such as React, Node.js and PostgreSQL (through Prisma), there aren't any technical limitations to scaling Amicus as it grows and attracts more users.

Also, given that the wasp build CLI command generates a ready Docker image for the back-end (and static files for the front-end), deployment options are unlimited. Since Heroku is shutting down its free plan, we added guides on how to deploy your project for free on Fly.io and Railway (freemium).

I was using Wasp while still in Alpha and was impressed how well everything worked, especially given how much stuff I get. I had just a few minor issues and the team responded super quickly on Discord and helped me resolve it.

— Erlis Kllogjri - Amicus

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

· 5 min read
Matija Sosic

amicus hero shot

Erlis Kllogjri is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how Amicus started out.

Amicus is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.

Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!

Looking for a full-stack “all-in-one” solution, with React & Node.js

Erlis first learned about Wasp on HackerNews and it immediately caught his attention, particularly the configuration language part. One of the companies he worked at in the past had its own internal DSL in the hardware domain, and he understood how helpful it could be for moving fast and avoiding boilerplate.

Erlis also had previous experience in web development, especially on the front-end side in React and Javascript, so that made Wasp a logical choice.

I was looking at other solutions, but none of them were full-stack and sounded like a lot of work just to stitch everything together and get started. I just wanted to get the job done and didn’t care about picking the stack specifics myself. Wasp was really helpful as it set me up with the best practices and I had everything running in just a few minutes!

— Erlis Kllogjri - Amicus

Building Amicus v1.0 and getting first customers!

The idea for Amicus came from his brother, who is employed at a law firm - talking about their process and challenges in executing them, Erlis thought it would be an interesting side project, especially given there is a real problem to solve.

Soon, the first version of Amicus was live! It was made in a true lean startup fashion, starting with the essential features and immediately being tested with users.

Amicus's dashboard
Amicus's dashboard, using Material-UI

Erlis used Material-UI as a UI library since it came with one of the example apps built in Wasp (Beta introduced Tailwind support!). Users could track their clients, active legal matters and there was even integrated billing with Stripe! Amicus also extensively used Wasp’s Async Jobs feature to regularly update invoices, send reminder emails and clear out old data from the database.

After a few iterations with the legal team who were Amicus' test user (e.g. adding support for different types of users via roles), they were ready to get onboarded and become paying customers! More than 20 people from a single company are using Amicus daily for their work, making it an amazing source of continuous feedback for further development.

Erlis enjoyed the most how fast he could progress and ship features with Wasp on a weekly basis. Having both front-end, back-end, and database set and fully configured to work together from the beginning, he could focus on developing features rather than spend time figuring out the intricacies of the specific stack.

If it weren't for Wasp, Amicus would probably have never been finished. I estimate it saved me 100+ hours from the start and I'm still amazed that I did all this work as a team-of-one. Being able to quickly change existing features and add the new ones is the biggest advantage of Wasp for me.

— Erlis Kllogjri - Amicus

Beyond MVP with Wasp

Although Erlis already has a product running in production, with first paying customers, he wants to see how far he can take it and has a lot of ideas (also requests) for the next features. (Actually, Erlis had a big kanban board with post-its on a wall behind him as we were chatting, dedicated just to Amicus - that was impressive to see!).

Some of the most imminent ones are:

  • uploading and sharing files between lawyers and clients
  • usage logging and analytics
  • transactional emails for notifications

Since under the hood Wasp is generating code in today's mainstream, production-tested technologies such as React, Node.js and PostgreSQL (through Prisma), there aren't any technical limitations to scaling Amicus as it grows and attracts more users.

Also, given that the wasp build CLI command generates a ready Docker image for the back-end (and static files for the front-end), deployment options are unlimited. Since Heroku is shutting down its free plan, we added guides on how to deploy your project for free on Fly.io and Railway (freemium).

I was using Wasp while still in Alpha and was impressed how well everything worked, especially given how much stuff I get. I had just a few minor issues and the team responded super quickly on Discord and helped me resolve it.

— Erlis Kllogjri - Amicus

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/26/michael-curry-usecase.html b/blog/2022/11/26/michael-curry-usecase.html index 1a191e1f9a..49c18cf44f 100644 --- a/blog/2022/11/26/michael-curry-usecase.html +++ b/blog/2022/11/26/michael-curry-usecase.html @@ -19,13 +19,13 @@ - - + +
-

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

· 5 min read
Matija Sosic

grabbit hero shot

Michael Curry is a senior front-end engineer at Improbable, a metaverse and simulation company based in London. In his free time he enjoys learning about compilers.

In his previous position at StudentBeans, he experienced the problem of multiple engineering teams competing for the same dev environment (e.g. testing, staging, …). Then he discovered Wasp and decided to do something about it!

Read on to learn why Michael chose Wasp to build and deploy an internal tool for managing development environments at StudentBeans.

The problem: the battle for the dev environment

StudentBeans has a microservices-based architecture with multiple environments - test, staging, production, …. The team practices CI/CD and deploys multiple times a day. With such a rapid development speed, it would relatively often happen that multiple engineering teams attempt to claim the same dev environment at the same time.

There wasn't an easy way for teams to synchronize on who is using which environment and it would eventually lead to unexpected changes, confusion, and prolonged development times.

The solution: Grabbit - claim and release dev environments as-you-go

After the incident described above repeated for the n-th time, the team got together for a postmortem. They decided their new development process should look like this:

  • merge your changes
  • claim the environment you want to deploy to (e.g. testing, staging, …)
  • deploy your changes
  • test your changes
  • release the environment once you are done with it so others are able to claim it

The other requirements were to build the solution in-house to save money and also not to spend more than a few hours on it as they still needed to deliver some important features for the ongoing sprint.

The power of rapid prototyping with Wasp

Michael learned about Wasp during its first HackerNews launch and it immediately caught his eye. Being a programming language enthusiast himself, he immediately understood the value of a DSL approach and how it could drastically simplify the development process, while at the same time not preventing him from using his preferred tech stack (React, Node.js) when needed.

Also, although Michael had full-stack experience, his primary strength at the time was on the front-end side. Wasp looked like a great way of not having to deal with the tedious back-end setup and wiring (setting up the database, figuring out API, …) and being able to focus on the UX.

When I first learned about Wasp on HN I was really excited about its DSL approach. It was amazing how fast I could get things running with Wasp - I had the first version within an hour! The language is also fairly simple and straightforward and plays well with React & Node.js + it removes a ton of boilerplate.

— Michael Curry - Grabbit

Out-of-the-box deployment

Once Michael was satisfied with the first version of Grabbit, and confirmed with the team it fits their desired process, the only thing left to do was to deploy it! It is well known this step can get really complicated, especially if you're not yet well-versed in the sea of config options that usually come with it.

Wasp CLI comes with a wasp build command that does all the heavy lifting for you - it creates a directory with static front-end files that you can easily deploy to e.g. Netlify, and on the other hand, a Docker image for the back-end. Since Heroku is ending its free plan, our recommendation is to deploy to Fly.io, for which the detailed guide is provided. You can find the detailed deployment instructions here.

In Michael's case, he deployed Grabbit behind the VPN since it was an internal tool, and this process was made easy by having a ready-to-go Dockerfile.

From MVP to a full-fledged SaaS without a rewrite

The presented functionality of Grabbit above is quite simple (create a resource → claim it → release it), and it could have easily been implemented in some no-code tool or, if we really wanted to go simple, with a Trello board. So why use Wasp at all?

One reason is that developers know and prefer their tools and trust code over the no-code solutions, especially when requirements are still evolving and it is not evident they won't get "stuck" in some closed system. Michael had similar thinking - as he identified this problem at his own company, he realized others must be facing the same issue as well. That is why his plan was to keep improving Grabbit and eventually offer it as a standalone SaaS.

This is where Wasp comes in - he could develop and deploy an initial version of Grabbit in a matter of hours, but still end up with a platform that he can extend indefinitely through the power of code with his stack of choice, React & Node.js, while also using the npm packages he is using everyday at work.

Once he starts adding more advanced features, such as multi-user support with authentication, email notifications, and integration with CI/CD, no-code tools won't cut it any more. This way he saved himself and the company from throwing an MVP away and starting everything from scratch (having to learn the new technology and figure out how to set it all up) as the product evolves.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

· 5 min read
Matija Sosic

grabbit hero shot

Michael Curry is a senior front-end engineer at Improbable, a metaverse and simulation company based in London. In his free time he enjoys learning about compilers.

In his previous position at StudentBeans, he experienced the problem of multiple engineering teams competing for the same dev environment (e.g. testing, staging, …). Then he discovered Wasp and decided to do something about it!

Read on to learn why Michael chose Wasp to build and deploy an internal tool for managing development environments at StudentBeans.

The problem: the battle for the dev environment

StudentBeans has a microservices-based architecture with multiple environments - test, staging, production, …. The team practices CI/CD and deploys multiple times a day. With such a rapid development speed, it would relatively often happen that multiple engineering teams attempt to claim the same dev environment at the same time.

There wasn't an easy way for teams to synchronize on who is using which environment and it would eventually lead to unexpected changes, confusion, and prolonged development times.

The solution: Grabbit - claim and release dev environments as-you-go

After the incident described above repeated for the n-th time, the team got together for a postmortem. They decided their new development process should look like this:

  • merge your changes
  • claim the environment you want to deploy to (e.g. testing, staging, …)
  • deploy your changes
  • test your changes
  • release the environment once you are done with it so others are able to claim it

The other requirements were to build the solution in-house to save money and also not to spend more than a few hours on it as they still needed to deliver some important features for the ongoing sprint.

The power of rapid prototyping with Wasp

Michael learned about Wasp during its first HackerNews launch and it immediately caught his eye. Being a programming language enthusiast himself, he immediately understood the value of a DSL approach and how it could drastically simplify the development process, while at the same time not preventing him from using his preferred tech stack (React, Node.js) when needed.

Also, although Michael had full-stack experience, his primary strength at the time was on the front-end side. Wasp looked like a great way of not having to deal with the tedious back-end setup and wiring (setting up the database, figuring out API, …) and being able to focus on the UX.

When I first learned about Wasp on HN I was really excited about its DSL approach. It was amazing how fast I could get things running with Wasp - I had the first version within an hour! The language is also fairly simple and straightforward and plays well with React & Node.js + it removes a ton of boilerplate.

— Michael Curry - Grabbit

Out-of-the-box deployment

Once Michael was satisfied with the first version of Grabbit, and confirmed with the team it fits their desired process, the only thing left to do was to deploy it! It is well known this step can get really complicated, especially if you're not yet well-versed in the sea of config options that usually come with it.

Wasp CLI comes with a wasp build command that does all the heavy lifting for you - it creates a directory with static front-end files that you can easily deploy to e.g. Netlify, and on the other hand, a Docker image for the back-end. Since Heroku is ending its free plan, our recommendation is to deploy to Fly.io, for which the detailed guide is provided. You can find the detailed deployment instructions here.

In Michael's case, he deployed Grabbit behind the VPN since it was an internal tool, and this process was made easy by having a ready-to-go Dockerfile.

From MVP to a full-fledged SaaS without a rewrite

The presented functionality of Grabbit above is quite simple (create a resource → claim it → release it), and it could have easily been implemented in some no-code tool or, if we really wanted to go simple, with a Trello board. So why use Wasp at all?

One reason is that developers know and prefer their tools and trust code over the no-code solutions, especially when requirements are still evolving and it is not evident they won't get "stuck" in some closed system. Michael had similar thinking - as he identified this problem at his own company, he realized others must be facing the same issue as well. That is why his plan was to keep improving Grabbit and eventually offer it as a standalone SaaS.

This is where Wasp comes in - he could develop and deploy an initial version of Grabbit in a matter of hours, but still end up with a platform that he can extend indefinitely through the power of code with his stack of choice, React & Node.js, while also using the npm packages he is using everyday at work.

Once he starts adding more advanced features, such as multi-user support with authentication, email notifications, and integration with CI/CD, no-code tools won't cut it any more. This way he saved himself and the company from throwing an MVP away and starting everything from scratch (having to learn the new technology and figure out how to set it all up) as the product evolves.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/26/wasp-beta-launch-week.html b/blog/2022/11/26/wasp-beta-launch-week.html index af5870e272..dbce13e71c 100644 --- a/blog/2022/11/26/wasp-beta-launch-week.html +++ b/blog/2022/11/26/wasp-beta-launch-week.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Beta Launch Week announcement

· 5 min read
Matija Sosic

It’s almost here! After almost two years since our Alpha release, countless apps developed, React and Node versions upgraded, and PRs merged we’re only a day away from Beta!

Beta is coming

We’re going to follow a launch week format, which means our Beta launch will last for the whole week! Starting with the Product Hunt launch this Sunday (we’ll let you know once we’re live, so sharpen your upvoting fingers!) we’ll highlight a new feature every day.

I’ll try not to spoil too much in advance but we’re really excited about this - here follows a quick overview of what it’s gonna look like:

Sunday, Nov 27 - Product Hunt launch event 🚀 + let’s get this party started: Auth 🎉

Besides defending our Product Hunt title (we won #1 Product of the Day last time), this time we’ll also have an online party for all of us to celebrate together!

It will be held on our Discord at 9:00 am EST / 15:00 CET - sign up here and make sure to mark yourself as “Interested”!

Join us to meet the team, attend a relaxed AMA session to learn everything about Wasp, from how it started to development challenges (having fun with Haskell, web dev and compilers) and ideas and plans for the future.

Beta launch party instructions

The first feature to announce will be authentication in Wasp! It’s easier and cooler than ever, supports 3rd party providers (hint: starts with “G”), and works smoother than a jar of peanut butter (not the crunchy one of course)!

Monday, Nov 28 - TypeScript support!

TypeScript is here!

When we asked you what was missing in Wasp during our Alpha Testing Program, you were pretty clear:

TypeScript is wanted!

We heard you (honestly we were missing it too) and now it’s here! You can write your code in TypeScript and enjoy all the goodies that types bring. Some things already work really well and there are a few for which we still have ideas on how to make them better, but more on that on Tuesday!

Wednesday, Nov 29 - Tailwind support! 🐈 💨

Tailwind Nic Cage

It’s beautiful! Another highly anticipated featured that also comes with Beta - support for Tailwind CSS framework! Since it has an additional build step it didn’t work out-of-the-box with Alpha, but now it works like a breeze (see what I did here?)!

Honestly, having used it for designing our new Beta landing page I can really see why it gained so much popularity. So long, making up names for classes, “containers”, and “wrappers”!

Thursday, Nov 30 - Optimistic updates!

Without optimistic updates
Stop glitching, dang it!

You know that feeling when you move your Trello card “Try Wasp Beta” from “Todo” column to “Done” column and everything works super smoothly without any glitches? That’s because of optimistic updates! You may not need it often but if you needed and it wasn’t possible you’d feel really sad.

Well, that’s why Alpha is called Alpha and Beta is called Beta 😅. Long story short, now it’s possible to do it in Wasp and it’s also super easy and clean! We're actually very optimistic you’ll feel really good about implementing optimistic updates for your app in Wasp.

Friday, Dec 1 - Improved IDE support, tooling and Wasp LSP!

VS Code support for Wasp LSP

If you like types in TypeScript (and in general), then you will also enjoy Wasp! Our DSL is also a typed language which means it can report errors in compile time, e.g. in case you haven’t configured your route correctly. And now all that happens directly in your editor!

Beta brings LSP, Language Server for Wasp that works with VS Code (support for other editors coming soon! I’m VIM user myself so take a guess :D). That means improved syntax highlighting, code autocompletion and live error reporting - everything you’d expect from a language!

Wasp Language Server in action
Wasp LSP in action!

Saturday, Dec 2 - Grande Finale + #1 Wasp Hackathon!(Waspathon🐝 ?)

First Wasp hackathon

I don’t want to reveal too much in advance, but yep there will be a hackathon, yep there will be cool rewards (at least we think so) and yep it will be awesome! We’ll officially announce it as we end the launch week, and equipped with all the new features Beta brought we’ll switch into the hacking mode!

It’s our first hackathon and we can’t wait to tell you more about it (ok, I admit, we’re still working on it) and see what you beeld with Wasp!

Recap

  • We are launching Beta this Sunday, Nov 27, on Product Hunt at 1am PST / 4am EST / 10am CET - make sure to upvote and comment (anything counts, even “go guys!”) when you can
  • Beta brings a ton of new exciting features - we’ll highlight one each day of the following week
  • On Saturday, Dec 2, we’ll announce a hackathon - our first ever!

That’s it, Waspeteers - keep buzzing as always and see you soon on the other side! 🐝  🅱️

Matija, Martin & the Wasp team

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Beta Launch Week announcement

· 5 min read
Matija Sosic

It’s almost here! After almost two years since our Alpha release, countless apps developed, React and Node versions upgraded, and PRs merged we’re only a day away from Beta!

Beta is coming

We’re going to follow a launch week format, which means our Beta launch will last for the whole week! Starting with the Product Hunt launch this Sunday (we’ll let you know once we’re live, so sharpen your upvoting fingers!) we’ll highlight a new feature every day.

I’ll try not to spoil too much in advance but we’re really excited about this - here follows a quick overview of what it’s gonna look like:

Sunday, Nov 27 - Product Hunt launch event 🚀 + let’s get this party started: Auth 🎉

Besides defending our Product Hunt title (we won #1 Product of the Day last time), this time we’ll also have an online party for all of us to celebrate together!

It will be held on our Discord at 9:00 am EST / 15:00 CET - sign up here and make sure to mark yourself as “Interested”!

Join us to meet the team, attend a relaxed AMA session to learn everything about Wasp, from how it started to development challenges (having fun with Haskell, web dev and compilers) and ideas and plans for the future.

Beta launch party instructions

The first feature to announce will be authentication in Wasp! It’s easier and cooler than ever, supports 3rd party providers (hint: starts with “G”), and works smoother than a jar of peanut butter (not the crunchy one of course)!

Monday, Nov 28 - TypeScript support!

TypeScript is here!

When we asked you what was missing in Wasp during our Alpha Testing Program, you were pretty clear:

TypeScript is wanted!

We heard you (honestly we were missing it too) and now it’s here! You can write your code in TypeScript and enjoy all the goodies that types bring. Some things already work really well and there are a few for which we still have ideas on how to make them better, but more on that on Tuesday!

Wednesday, Nov 29 - Tailwind support! 🐈 💨

Tailwind Nic Cage

It’s beautiful! Another highly anticipated featured that also comes with Beta - support for Tailwind CSS framework! Since it has an additional build step it didn’t work out-of-the-box with Alpha, but now it works like a breeze (see what I did here?)!

Honestly, having used it for designing our new Beta landing page I can really see why it gained so much popularity. So long, making up names for classes, “containers”, and “wrappers”!

Thursday, Nov 30 - Optimistic updates!

Without optimistic updates
Stop glitching, dang it!

You know that feeling when you move your Trello card “Try Wasp Beta” from “Todo” column to “Done” column and everything works super smoothly without any glitches? That’s because of optimistic updates! You may not need it often but if you needed and it wasn’t possible you’d feel really sad.

Well, that’s why Alpha is called Alpha and Beta is called Beta 😅. Long story short, now it’s possible to do it in Wasp and it’s also super easy and clean! We're actually very optimistic you’ll feel really good about implementing optimistic updates for your app in Wasp.

Friday, Dec 1 - Improved IDE support, tooling and Wasp LSP!

VS Code support for Wasp LSP

If you like types in TypeScript (and in general), then you will also enjoy Wasp! Our DSL is also a typed language which means it can report errors in compile time, e.g. in case you haven’t configured your route correctly. And now all that happens directly in your editor!

Beta brings LSP, Language Server for Wasp that works with VS Code (support for other editors coming soon! I’m VIM user myself so take a guess :D). That means improved syntax highlighting, code autocompletion and live error reporting - everything you’d expect from a language!

Wasp Language Server in action
Wasp LSP in action!

Saturday, Dec 2 - Grande Finale + #1 Wasp Hackathon!(Waspathon🐝 ?)

First Wasp hackathon

I don’t want to reveal too much in advance, but yep there will be a hackathon, yep there will be cool rewards (at least we think so) and yep it will be awesome! We’ll officially announce it as we end the launch week, and equipped with all the new features Beta brought we’ll switch into the hacking mode!

It’s our first hackathon and we can’t wait to tell you more about it (ok, I admit, we’re still working on it) and see what you beeld with Wasp!

Recap

  • We are launching Beta this Sunday, Nov 27, on Product Hunt at 1am PST / 4am EST / 10am CET - make sure to upvote and comment (anything counts, even “go guys!”) when you can
  • Beta brings a ton of new exciting features - we’ll highlight one each day of the following week
  • On Saturday, Dec 2, we’ll announce a hackathon - our first ever!

That’s it, Waspeteers - keep buzzing as always and see you soon on the other side! 🐝  🅱️

Matija, Martin & the Wasp team

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/28/why-we-chose-prisma.html b/blog/2022/11/28/why-we-chose-prisma.html index eda6a4eb54..3327fdc206 100644 --- a/blog/2022/11/28/why-we-chose-prisma.html +++ b/blog/2022/11/28/why-we-chose-prisma.html @@ -19,13 +19,13 @@ - - + +
-

Why we chose Prisma as a database layer for Wasp

· 7 min read
Martin Sosic

Beta is coming

Wasp is a full-stack JS web dev framework, covering frontend, backend, and database. When choosing the solution to build our database layer on top, we chose Prisma, even though it was still somehwat new tech at that point, and we believe today we made a great choice -> read on to learn why!

At Wasp, we aim to simplify full-stack web development via a specialized high-level language. This language allows you to describe the main parts of your web app succinctly, avoiding a lot of usual boilerplate and configuration while giving you lots of features and ensuring best practices. Wasp is essentially a full-stack web framework implemented as a specialized language that works with React & Node.js!

When we started working on Wasp, we wanted to keep it easy to learn and to the point, so we decided:

  • the Wasp language should only be used at a high level, so you would still use React, NodeJS, HTML, CSS, etc. to implement your custom logic. If a full-stack web app is an orchestra, Wasp is the conductor.
  • the Wasp language should be declarative and simple, very similar to JSON, but “smarter” in the sense it understands web app concepts and makes sure your app follows them.

With that in mind, we focused on identifying high-level web app concepts that are worth capturing in the Wasp language. We identified the following parts of a web app:

  • General app info (title, head, favicon, …)
  • Pages and Routes
  • Data Models (aka Entities), e.g. User, Task, Organization, Article, … .
  • Operations (communication between client and server; CRUD on data models, 3rd party APIs, …)
  • Deployment

Entities

Of all of those, Entities are in the middle of everything, present through the whole codebase, and are central to all the other parts of the web app: client, server, and database. They were, however, also the most daunting part to implement!

When we started, we imagined an Entity would look something like this in Wasp:

entity User {
id: Id,
username: String @unique,
email: String @unique
groups: [Group]
}

While adding this initial syntax to our language was feasible, there were also much bigger tasks to tackle in order to make this a proper solution:

  • expand syntax to be flexible enough for real-life use cases
  • support migrations (data and schema)
  • generate code that users can call from JS/TS to query and update entities in the DB
  • and probably a lot of other things that we hadn’t even thought of yet!

Mongoose, Sequelize, … or Prisma?

We already decided that we would pick an ORM(ish) solution for JS/TS which we would build the rest of the features on top of. We started evaluating different ones: Mongoose, Sequelize, TypeORM, … .

But then we looked at Prisma, and the winner was clear! Not only was Prisma taking care of everything that we cared about, but it had one additional feature that made it a perfect fit:

model User {
id Int @id @default(autoincrement())
username String @unique
password String
}

No, this is not another idea of how the syntax for Entities could look like in Wasp language → this is the Prisma Schema Language (PSL)!!!

Prisma Schema Language (PSL)

Indeed, Prisma is unique in having a special, declarative language for describing data models (schema), and it was exactly what we needed for Wasp.

So instead of implementing our own syntax for describing Entities, we decided to use Prisma and their PSL to describe Entities (data models) inside the Wasp language.

Today, Entities are described like this in Wasp language:

// ... some Wasp code ...

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}

// ... some Wasp code ...

So in the middle of Wasp, you just switch to writing PSL (Prisma Schema Language) to describe an entity!

Another great thing is that the PSL is at its core a pretty simple language, so we implemented our own parser for it → that means that Wasp actually understands what you wrote, even though it is PSL, and can fully work with it. So we lost nothing by using PSL instead of our own syntax and instead gained all the features that Prisma brings.

Other Benefits

Besides PSL, there were plenty of other reasons why we felt Prisma is a great fit for us:

  • It is targeting Javascript / Typescript.
  • It takes care of migrations and has a nice workflow for doing it.
  • It supports different databases: Mongo, PostgreSQL, CockroachDB, …, which is very important for Wasp since our vision is to support different stacks in the future.
  • It has Prisma Studio - UI for inspecting your database, which we also make available to you via Wasp CLI.
  • It keeps improving quickly and is very focused on a nice developer experience, which is also our focus here at Wasp.
  • Community is extremely welcoming and the core team is super helpful - all of our questions and issues were answered super quickly!

Challenges

While integrating Prisma into Wasp went really smoothly, there were a few hiccups:

  • Getting Prisma CLI to provide interactive output while being called programmatically by Wasp was tricky, and in the end, we had to use a bit of a dirty approach to trick the Prisma CLI into thinking it is called interactively. We opened an issue for this with Prisma, so hopefully, we will be able to remove this once it is resolved: https://github.com/prisma/prisma/issues/7113.
  • In the early days, there were some bugs, however, they were always quickly solved, so updating to the newest Prisma version was often the solution.
  • It took us a bit of fiddling to get Prisma to work with its schema outside of the server’s root directory, but we did get it working in the end!

Most of these were due to us stretching the boundaries of how Prisma was imagined to be used, but in total Prisma proved to be fairly flexible!

Summary

With its declarative language for describing schema, focus on ergonomics, and JS/TS as the target language, Prisma was really a stroke of luck for us - if not for it, it would have taken much more effort to get the Entities working in Wasp.

When we started using it, Prisma was still somewhat early, and it was certainly the least-mature technology in our stack - but we decided to bet on it because it was just a perfect fit, and it made so much sense. Today, with Prisma being a mature and popular solution, we are more than happy we made that choice!

Future

Already, Prisma is playing a big role at Wasp, but there is still more that we plan and want to do:

  • support Prisma’s Enum and Type declarations
  • expose more of Prisma’s CLI commands, especially database seeding
  • add support in Wasp for multiple databases (which Prisma already supports)
  • improve IDE support for PSL within the Wasp language

If you are interested in helping with any of these, reach out to us on this issue https://github.com/wasp-lang/wasp/issues/641, or in any case, join us on our Discord server!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Why we chose Prisma as a database layer for Wasp

· 7 min read
Martin Sosic

Beta is coming

Wasp is a full-stack JS web dev framework, covering frontend, backend, and database. When choosing the solution to build our database layer on top, we chose Prisma, even though it was still somehwat new tech at that point, and we believe today we made a great choice -> read on to learn why!

At Wasp, we aim to simplify full-stack web development via a specialized high-level language. This language allows you to describe the main parts of your web app succinctly, avoiding a lot of usual boilerplate and configuration while giving you lots of features and ensuring best practices. Wasp is essentially a full-stack web framework implemented as a specialized language that works with React & Node.js!

When we started working on Wasp, we wanted to keep it easy to learn and to the point, so we decided:

  • the Wasp language should only be used at a high level, so you would still use React, NodeJS, HTML, CSS, etc. to implement your custom logic. If a full-stack web app is an orchestra, Wasp is the conductor.
  • the Wasp language should be declarative and simple, very similar to JSON, but “smarter” in the sense it understands web app concepts and makes sure your app follows them.

With that in mind, we focused on identifying high-level web app concepts that are worth capturing in the Wasp language. We identified the following parts of a web app:

  • General app info (title, head, favicon, …)
  • Pages and Routes
  • Data Models (aka Entities), e.g. User, Task, Organization, Article, … .
  • Operations (communication between client and server; CRUD on data models, 3rd party APIs, …)
  • Deployment

Entities

Of all of those, Entities are in the middle of everything, present through the whole codebase, and are central to all the other parts of the web app: client, server, and database. They were, however, also the most daunting part to implement!

When we started, we imagined an Entity would look something like this in Wasp:

entity User {
id: Id,
username: String @unique,
email: String @unique
groups: [Group]
}

While adding this initial syntax to our language was feasible, there were also much bigger tasks to tackle in order to make this a proper solution:

  • expand syntax to be flexible enough for real-life use cases
  • support migrations (data and schema)
  • generate code that users can call from JS/TS to query and update entities in the DB
  • and probably a lot of other things that we hadn’t even thought of yet!

Mongoose, Sequelize, … or Prisma?

We already decided that we would pick an ORM(ish) solution for JS/TS which we would build the rest of the features on top of. We started evaluating different ones: Mongoose, Sequelize, TypeORM, … .

But then we looked at Prisma, and the winner was clear! Not only was Prisma taking care of everything that we cared about, but it had one additional feature that made it a perfect fit:

model User {
id Int @id @default(autoincrement())
username String @unique
password String
}

No, this is not another idea of how the syntax for Entities could look like in Wasp language → this is the Prisma Schema Language (PSL)!!!

Prisma Schema Language (PSL)

Indeed, Prisma is unique in having a special, declarative language for describing data models (schema), and it was exactly what we needed for Wasp.

So instead of implementing our own syntax for describing Entities, we decided to use Prisma and their PSL to describe Entities (data models) inside the Wasp language.

Today, Entities are described like this in Wasp language:

// ... some Wasp code ...

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}

// ... some Wasp code ...

So in the middle of Wasp, you just switch to writing PSL (Prisma Schema Language) to describe an entity!

Another great thing is that the PSL is at its core a pretty simple language, so we implemented our own parser for it → that means that Wasp actually understands what you wrote, even though it is PSL, and can fully work with it. So we lost nothing by using PSL instead of our own syntax and instead gained all the features that Prisma brings.

Other Benefits

Besides PSL, there were plenty of other reasons why we felt Prisma is a great fit for us:

  • It is targeting Javascript / Typescript.
  • It takes care of migrations and has a nice workflow for doing it.
  • It supports different databases: Mongo, PostgreSQL, CockroachDB, …, which is very important for Wasp since our vision is to support different stacks in the future.
  • It has Prisma Studio - UI for inspecting your database, which we also make available to you via Wasp CLI.
  • It keeps improving quickly and is very focused on a nice developer experience, which is also our focus here at Wasp.
  • Community is extremely welcoming and the core team is super helpful - all of our questions and issues were answered super quickly!

Challenges

While integrating Prisma into Wasp went really smoothly, there were a few hiccups:

  • Getting Prisma CLI to provide interactive output while being called programmatically by Wasp was tricky, and in the end, we had to use a bit of a dirty approach to trick the Prisma CLI into thinking it is called interactively. We opened an issue for this with Prisma, so hopefully, we will be able to remove this once it is resolved: https://github.com/prisma/prisma/issues/7113.
  • In the early days, there were some bugs, however, they were always quickly solved, so updating to the newest Prisma version was often the solution.
  • It took us a bit of fiddling to get Prisma to work with its schema outside of the server’s root directory, but we did get it working in the end!

Most of these were due to us stretching the boundaries of how Prisma was imagined to be used, but in total Prisma proved to be fairly flexible!

Summary

With its declarative language for describing schema, focus on ergonomics, and JS/TS as the target language, Prisma was really a stroke of luck for us - if not for it, it would have taken much more effort to get the Entities working in Wasp.

When we started using it, Prisma was still somewhat early, and it was certainly the least-mature technology in our stack - but we decided to bet on it because it was just a perfect fit, and it made so much sense. Today, with Prisma being a mature and popular solution, we are more than happy we made that choice!

Future

Already, Prisma is playing a big role at Wasp, but there is still more that we plan and want to do:

  • support Prisma’s Enum and Type declarations
  • expose more of Prisma’s CLI commands, especially database seeding
  • add support in Wasp for multiple databases (which Prisma already supports)
  • improve IDE support for PSL within the Wasp language

If you are interested in helping with any of these, reach out to us on this issue https://github.com/wasp-lang/wasp/issues/641, or in any case, join us on our Discord server!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/11/29/permissions-in-web-apps.html b/blog/2022/11/29/permissions-in-web-apps.html index 913d5a86af..897a52f6a4 100644 --- a/blog/2022/11/29/permissions-in-web-apps.html +++ b/blog/2022/11/29/permissions-in-web-apps.html @@ -19,12 +19,12 @@ - - + +
-

Permissions (access control) in web apps

· 19 min read
Martin Sosic

At Wasp, we are working on a config language / DSL for building web apps that integrates with React & Node.js.
+

Permissions (access control) in web apps

· 19 min read
Martin Sosic

At Wasp, we are working on a config language / DSL for building web apps that integrates with React & Node.js.
This requires us to deeply understand different parts of what constitutes a web app, in order to be able to model them in our DSL.

Recently our focus was on access control, and I decided to capture the learnings in this blog post, to help others quickly get up to speed on how to do access control in web apps.
So, if you are new to access control in web apps, or have been doing it for some time but want to get a better idea of standard practices, read along!

Quick overview of what this blog post covers:

  1. Permissions, yay! Wait, what are they though? (quick overview of basic terms)
  2. Where do we check permissions in a web app: frontend vs backend vs db
  3. Common approaches (RBAC, ABAC, …)
  4. OWASP recommendations
  5. Implementing access control in practice
  6. Summary (TLDR)

1. Permissions, yay! Wait, what are they though?

Unless your web app is mostly about static content or is a form of art, it will likely have a notion of users and user accounts.

Artistic dolphin painting with brush
This dolphin doesn't need users

In such a case, you will need to know which user has permissions to do what -> who can access which resources, and who can execute which operations.

Some common examples of permissions in action:

  1. User can access only their own user account.
  2. If the user is an admin, they can ban other users’ accounts.
  3. User can read other users’ articles, but can't modify them.
  4. The title and description of the article behind the paywall are publicly accessible, but the content is not.
  5. User can send an email invitation to up to 10 future users per day.

Aha, you mean access control! Sorry, authorization! Hmm, authentication?

There are different terms out there (authentication, authorization, access control, permissions) that are often confused for each other, so let's quickly clarify what each one of them stands for.

Spidermen representing authN, authZ, AC and permissions pointing at each other
They all look the same!

1) Authentication (or as cool kids would say: authN)

Act of verifying the user's identity.
Answers the question "Who are they?"

A: Knock Knock
@@ -45,7 +45,7 @@ An interesting finding is that even though the sample is pretty small, it is clear that devs prefer RBAC over OWASP-recommended ABAC.
I believe this is due to 2 main reasons: RBAC is simpler + there are more libraries/frameworks out there supporting RBAC than ABAC (again, due to it being simpler).
It does seem that ABAC is picking up recently though, so it would be interesting to repeat this poll in the future and see what changes.

Organic development

Organic growth of my code (meme)

Often, we add permission checks to our web app one by one, as needed. For example, if we are using NodeJS with ExpressJS for our server and writing middleware that handles HTTP API requests, we will add a bit of logic into that middleware that does some checks to ensure a user can actually perform that action. Or maybe we will embed “checks” into our database queries so that we query only what the user is allowed to access. Often a combination.

What can be dangerous with such an organic approach is the complexity that arises as the codebase grows - if we don’t put enough effort into centralizing and structuring our access control logic, it can become very hard to reason about it and to do consistent updates to it, leading to mistakes and vulnerabilities.

Imagine having to modify the web app so that user can now only read their own articles and articles of their friends, while before they were allowed to read any article. If there is only one place where we can make this update, we will have a nice time, but if there are a bunch of places and we need to hunt those down first and then make sure they are all updated in the same way, we are in for a lot of trouble and lot of space to make mistakes.

Using an existing solution

Instead of figuring out on our own how to structure the access control code, often it is a better choice to use an existing access control solution! Besides not having to figure and implement everything on your own, another big advantage is that these solutions are battle-tested, which is very important for the code dealing with the security of your web app.

We can roughly divide these solutions into frameworks and (external) providers, where frameworks are embedded into your web app and shipped together with it, while providers are externally hosted and usually paid services.

A couple of popular solutions:

  1. https://casbin.org/ (multiple approaches, multiple languages, provider)
    1. Open source authZ library that has support for many access control models (ACL, RBAC, ABAC, …) and many languages (Go, Java, Node.js, JS, Rust, …). While somewhat complex, it is also powerful and flexible. They also have their Casdoor platform, which is authN and authZ provider.
  2. https://casl.js.org/v5/en/ (ABAC, Javascript)
    1. Open source JS/TS library for ABAC. CASL gives you a nice way to define the ABAC rules in your web / NodeJS code, and then also check them and call them. It has a bunch of integrations with popular solutions like React, Angular, Prisma, Mongoose, … .
  3. https://github.com/CanCanCommunity/cancancan (Ruby on Rails ABAC)
    1. Same like casl.js, but for Ruby on Rails! Casl.js was actually inspired and modeled by cancancan.
  4. https://github.com/varvet/pundit
    1. Popular open-source Ruby library focused around the notion of policies, giving you the freedom to implement your own approach based on that.
  5. https://spring.io/projects/spring-security
    1. Open source authN and authZ framework for Spring (Java).
  6. https://github.com/dfunckt/django-rules
    1. A generic, approachable open source framework for building rule-based systems in Django (Python).
  7. Auth0 (provider)
    1. Auth0 has been around for some time and is probably the most popular authN provider out there. While authN is their main offering (they give you SDKs for authentication + they store user profiles and let you manage them through their SaaS), they also allow you to define authZ to some degree, via RBAC and policies.
  8. https://www.osohq.com/ (provider, DSL)
    1. OSO is an authZ provider, unique in a way that they have a specialized language for authorization (DSL, called Polar) in which you define your authorization rules. They come with support for common approaches (e.g. RBAC, ABAC, ReBAC) but also support custom ones. Then, you can use their open source library embedded in your application, or use their managed cloud offering.
  9. https://warrant.dev/ (Provider)
    1. Relatively new authZ provider, they have a dashboard where you can manage your rules in a central location and then use them from multiple languages via their SDKs, even on the client to perform UI checks. Rules can also be managed programmatically via SDK.
  10. https://authzed.com/ (Provider)
    1. AuthZed brings a specialized SpiceDB permissions database which they use as a centralized place for storing and managing rules. Then, you can use their SDKs to query, store, and validate application permissions.

Summary (TLDR)

  • Authentication (authN) answers “who are they”, authorization (authZ) answers “are they allowed to”, while access control is the overarching term for the whole process of performing authN and authZ.
  • Doing access control on the frontend is just for show (for improving UX) and you can’t rely on it. Any and all real access control needs to be done on the server (possibly a bit in the db, but normally not needed).
  • While it is ok to start with a simple access control approach at the beginning, you should be ready to switch to a more advanced approach once the complexity grows. The most popular approaches for doing access control are RBAC (role-based) and ABAC (attribute-based). RBAC is easier to get going with, but ABAC is more powerful.
  • You should make sure your access control has as little duplication as possible and is centralized, in order to reduce the chance of introducing bugs.
  • It is usually smart to use existing solutions, like access control frameworks or external providers.

Access control in Wasp

In Wasp, we don’t yet have special support for access control, although we are planning to add it in the future. As it seems at the moment, we will probably go for ABAC, and we would love to provide a way to define access rules both at the Operations level and at Entity (data model) level. Due to Wasp’s mission to provide a highly integrated full-stack experience, we are excited about the possibilities this offers to provide an access control solution that is integrated tightly with the whole web app, through the whole stack!

You can check out our discussion about this in our “Support for Permissions” RFC.

Thanks to the reviewers

Karan Kajla (pro advice on RBAC!), Graham Neray (great general advice + pointed out ReBAC), Dennis Walsh (awesome suggestions how to have article read better), Shayne Czyzewski, Matija Sosic, thank you for taking the time to review this article and make it better! Your suggestions, corrections, and ideas were invaluable.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/11/29/typescript-feature-announcement.html b/blog/2022/11/29/typescript-feature-announcement.html index 4a41f499dc..629c3d5587 100644 --- a/blog/2022/11/29/typescript-feature-announcement.html +++ b/blog/2022/11/29/typescript-feature-announcement.html @@ -19,16 +19,16 @@ - - + +
-

Feature Announcement - TypeScript Support

· 8 min read
Filip Sodić

Wasp TS support

Prologue

TypeScript doesn't need much introduction at this point, so we'll keep it short! +

Feature Announcement - TypeScript Support

· 8 min read
Filip Sodić

Wasp TS support

Prologue

TypeScript doesn't need much introduction at this point, so we'll keep it short! Wasp finally allows you to write your code in TypeScript (i.e., the most popular web technology after JavaScript) on both the front-end and the back-end.

You can now define and use types in any part of your code, enjoying all benefits of the static type checker. At the time of writing, not all parts of Wasp are typed as well as they could be, but we're working on it! Exposing all Wasp functionalities through informative typed interfaces is one of our top priorities.

Without further ado, let's see how we can use TypeScript with Wasp.

Setting up a TypeScript project in Wasp

Let's start by creating a fresh Wasp project:

wasp new myApp

This will generate a project skeleton in the folder myApp. The project structure is different than before, and there are now several additional generated files that help with IDE and TypeScript support. So let's explain it:

.
├── .gitignore
├── main.wasp # Your wasp code goes here.
├── src
│   ├── client # Your client code (JS/CSS/HTML) goes here.
│   │   ├── Main.css
│   │   ├── MainPage.jsx
│   │   ├── react-app-env.d.ts
│   │   ├── tsconfig.json
│   │   └── waspLogo.png
│   ├── server # Your server code (Node JS) goes here.
│   │   └── tsconfig.json
│   ├── shared # Your shared (runtime independent) code goes here.
│   │   └── tsconfig.json
│   └── .waspignore
└── .wasproot

At this point, we can choose one of three options:

  1. We write our code exclusively in JavaScript.
  2. We write our code exclusively in TypeScript.
  3. We write some parts of our code in JavaScript, and other parts in TypeScript.

Since the third option is a superset of the first two, that's what Wasp currently supports. In other words, regardless of whether you want your entire codebase in one of these languages or you want to mix it up, there's no extra configuration necessary! Simply use the appropriate extension (.ts and .tsx for TypeScript; .js and .jsx for JavaScript), and your IDE and Wasp will know what to do.

To demonstrate this, let's start Wasp and change MainPage.jsx to MainPage.tsx:

wasp start
mv src/client/MainPage.jsx src/client/MainPage.tsx

That's it! Wasp will notice the change and recompile, and your app will continue to work. The only difference is that you can now write TypeScript in MainPage.tsx and get helpful information from your IDE and the static type checker. Try removing an import and see what happens.

The same applies to any file you may want to include in your project. Specify the language you wish to use via the extension, and Wasp will do the rest!

caution

Even if you use TypeScript and have a server file called someFile.ts, you must still import it as if it had the .js extension (i.e., import foo from 'someFile.js'). Wasp internally uses esnext module resolution, which always requires specifying the extension as .js (i.e., the extension used in the emitted JS file). This applies to all @server imports (and files on the server in general).

Read more about ES modules in TypeScript here. If you're interested in the discussion and the reasoning behind this, read about it in this GitHub issue.

This does not apply to front-end files. Thanks to Webpack, you don't need to write extensions when working with client-side imports.

Moving existing projects to the new structure (and optionally TypeScript)

If you wish to move an existing project to the new structure, the easiest approach comes down to creating a new project and moving all the files from your old project into appropriate locations. After doing this, you can choose which files you'd like to implement in TypeScript, change the extension and go for it.

To avoid digging too deep, this is all we'll say about migrating. For a more detailed migration guide, check our changelog. It explains everything step-by-step.

TypeScript in action

Finally, let's demonstrate how TypeScript helps us by using it in a small Todo app. The part of our code in charge of rendering tasks looks something like this:


function MainPage() {
const { data: tasks } = useQuery(getTasks)

return (
<div>
<h1>Todos</h1>
<TaskList tasks={tasks} />
</div>
)
}

function TaskList({ tasks }) {
if (!tasks.len) {
return <div>No tasks</div>
}

return (
<div>
{tasks.map((task, idx) => <Task {...task} key={idx}/>)}
</div>
)
}



function Task({ id, isdone, description }) {
return (
<div>
<label>
<input
type='checkbox'
id={id}
checked={isdone}
onChange={
(event) => updateTask({ id, isDone: event.target.checked })
}
/>
<span>{description}</span>
</label>
</div>
)
}

Try to see if you can find any bugs. When you're confident you've got all of them, continue reading.

Let's see what happens when we bring TypeScript into the picture. Remember, we only need to change the extension to tsx. After we do this, The IDE will warn us about missing type definitions, so let's fill these in. While we're at it, we can also tell useQuery what types it's working with by specifying its type arguments.

Here's how our code looks after these changes:

type Task = {
id: string
description: string
isDone: boolean
}

function MainPage() {
const { data: tasks } = useQuery<Task, Task[]>(getTasks)

return (
<div>
<h1>Todos</h1>
<TaskList tasks={tasks} />
</div>
)
}

function TaskList({ tasks }: { tasks: Task[] }) {
if (!tasks.len) {
return <div>No tasks</div>
}

return (
<div>
{tasks.map((task, idx) => <Task {...task} key={idx}/>)}
</div>
)
}



function Task({ id, isdone, description }: Task) {
return (
<div>
<label>
<input
type='checkbox'
id={id}
checked={isdone}
onChange={
(event) => updateTask({ id, isDone: event.target.checked })
}
/>
<span>{description}</span>
</label>
</div>
)
}

As soon as we change our code, TypeScript detects three errors:

TypeScript erros
The errors are pretty simple (almost as if we've made them up for this example :)

  1. The first error warns us that tasks might be undefined (e.g., on the first render), which TaskList does not expect
  2. The second error tells us that the property len does not exist on the array tasks. In other words, we misspelled length.
  3. Finally, the third error tells us that the type Task does not contain the field isdone. This is also a typo. The field's name should be isDone.

Thanks to TypeScript, we can quickly fix all three errors, saving us a lot of time we'd probably lose by hunting them down manually or, even worse, during runtime.


type Task = {
id: string
description: string
isDone: boolean
}
function MainPage() {
const { data: tasks } = useQuery<Task, Task[]>(getTasks)

return (
<div>
<h1>Todos</h1>
{tasks && <TaskList tasks={tasks} />}
</div>
)
}

function TaskList({ tasks }: { tasks: Task[] }) {
if (!tasks.length) {
return <div>No tasks</div>
}

return (
<div>
{tasks.map((task, idx) => <Task {...task} key={idx} />)}
</div>
)
}



function Task({ id, isDone, description }: Task) {
return (
<div>
<label>
<input
type='checkbox'
id={id}
checked={isDone}
onChange={
(event) => updateTask({ id, isDone: event.target.checked })
}
/>
<span>{description}</span>
</label>
</div>
)
}

And that's it! This is the joy of TypeScript. We've easily fixed all reported errors, and our code should now work correctly (well, at least less incorrectly).

Future work

You might have noticed that, if we want to use the Task type, we have to write most of its type definition twice - once when defining the Task entity in the .wasp file and then again in our code. While we can define the type in src/shared to avoid writing (almost) the same code on both the server and the client, we'll still have duplication between the code in src/shared and our .wasp file.

The good news is that we know about this, also find it annoying, and are working to fix it as soon as possible! In the near future, Wasp will generate types from entities and allow you to access them using @wasp imports. Other improvements exist, too. For example, Wasp could read your query declarations and provide you with the correct type for the context object in their definitions. Another possible improvement is automatically typing queries on the front-end, and then relying on type inference to correctly type useQuery (instead of users specifying its type arguments explicitly).

In short, there's a long and exciting path ahead of us, full of interesting possibilities. So stick with Wasp and see how far we can make it!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/11/29/wasp-beta.html b/blog/2022/11/29/wasp-beta.html index c41c566f32..4be84b53be 100644 --- a/blog/2022/11/29/wasp-beta.html +++ b/blog/2022/11/29/wasp-beta.html @@ -19,14 +19,14 @@ - - + +
-

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

· 3 min read
Matija Sosic

Wasp is a simple configuration language for building full-stack web apps with less code and ensured best practices. It integrates with React, Node.js and Prisma and provides a lot of common features (auth, CRUD, async jobs, ...) out of the box.

Today, we’re moving to Beta.

Since the launch of Wasp Alpha in February 2021, we’ve been fortunate to work with hundreds of early adopters who helped us shape the product and prioritise the features to build. Number of applications have been deployed to production and even the first revenue generating product was built on top of Wasp.

Alpha in numbers

  • 1,011 projects created
  • 2,012 GitHub stars
  • 45 GitHub contributors
  • 243 issues closed
  • 42,170 lines of code

Here are the the new features that ship with Beta:

🟦 TypeScript support

Developers can now write all their code in TypeScript both on client and server. We’re also in the process of migrating our codebase and adding new types to Wasp imports every day.

Learn more here →

🔑 Full-stack authentication

Besides username & password, Wasp now also supports authentication with Google. We offer both UI helpers (forms you can just import) and functions you can call from client or server if you need more control.

Learn more here →

💨 Tailwind support

Tailwind CSS framework is now supported in Wasp. Just add two files to the project and you’re ready to go!

Learn more here →

⏳ Async jobs/workers

Developers can run one-time or schedule repeating functions that run out of the regular request-response band. This is useful for e.g. sending emails, crunching data, generating reports and other resources intensive tasks. Powered by pg-boss, zero setup required.

Learn more here →

🥛 Optimistic updates support

Wasp will by default propagate your data model changes across the stack. Still, in some cases +

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

· 3 min read
Matija Sosic

Wasp is a simple configuration language for building full-stack web apps with less code and ensured best practices. It integrates with React, Node.js and Prisma and provides a lot of common features (auth, CRUD, async jobs, ...) out of the box.

Today, we’re moving to Beta.

Since the launch of Wasp Alpha in February 2021, we’ve been fortunate to work with hundreds of early adopters who helped us shape the product and prioritise the features to build. Number of applications have been deployed to production and even the first revenue generating product was built on top of Wasp.

Alpha in numbers

  • 1,011 projects created
  • 2,012 GitHub stars
  • 45 GitHub contributors
  • 243 issues closed
  • 42,170 lines of code

Here are the the new features that ship with Beta:

🟦 TypeScript support

Developers can now write all their code in TypeScript both on client and server. We’re also in the process of migrating our codebase and adding new types to Wasp imports every day.

Learn more here →

🔑 Full-stack authentication

Besides username & password, Wasp now also supports authentication with Google. We offer both UI helpers (forms you can just import) and functions you can call from client or server if you need more control.

Learn more here →

💨 Tailwind support

Tailwind CSS framework is now supported in Wasp. Just add two files to the project and you’re ready to go!

Learn more here →

⏳ Async jobs/workers

Developers can run one-time or schedule repeating functions that run out of the regular request-response band. This is useful for e.g. sending emails, crunching data, generating reports and other resources intensive tasks. Powered by pg-boss, zero setup required.

Learn more here →

🥛 Optimistic updates support

Wasp will by default propagate your data model changes across the stack. Still, in some cases you might want more control over that flow for the sake of smoother UX - that is now easy to achieve with Wasp.

Learn more here →

📟 Wasp Language Server

Wasp now has its own LSP for VS Code (other editors coming soon) - that means improved syntax highlighting, code snippets, autocompletion, and error reporting.

Learn more here →

What’s next?

The next features are going to be about making Wasp easier to use - more examples, starter templates and UI helpers. Longer term, we’ll look into deeper integration of data models throughout the stack and supporting more functionalities through the DSL.

It’s Beta Launch Week and we’re highlighting a new feature every week. Also, at the end of the week we’ll kick-off first Wasp hackathon! Signup here to stay in the loop.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/11/30/optimistic-update-feature-announcement.html b/blog/2022/11/30/optimistic-update-feature-announcement.html index e5cfa76c22..a7b46c98fb 100644 --- a/blog/2022/11/30/optimistic-update-feature-announcement.html +++ b/blog/2022/11/30/optimistic-update-feature-announcement.html @@ -19,16 +19,16 @@ - - + +
-

Feature Release Announcement - Wasp Optimistic Updates

· 7 min read
Filip Sodić

We’re excited to announce that Wasp actions now feature native support for optimistic updates! +

Feature Release Announcement - Wasp Optimistic Updates

· 7 min read
Filip Sodić

We’re excited to announce that Wasp actions now feature native support for optimistic updates! Continue reading to to find out what optimistic updates are and how Wasp implements them.

Wasp TS support

What are Optimistic Updates Anyway?

Think about an interactive web app you use daily. It could be almost anything (e.g., Reddit, Youtube, Facebook). It almost certainly features UI elements you can interact with without refreshing the page, such as upvotes on Reddit or likes on Youtube.

All these small actions play out in the same manner. Let's look at Reddit upvotes as an example:

  1. You click on the upvote button
  2. Your browser sends a request to the server to save the upvote
  3. The server saves your upvote to the database and sends a successful response to your browser
  4. Your browser receives the successful response and reflects the change in the UI (i.e., you see your upvote)

The client waits for the server's confirmation before updating the UI because actions can sometimes fail. Well, at least that was the original idea.

These days, many popular websites update their UIs without waiting for servers' responses. Most of the time, everything goes as expected: you click on an upvote, and the server returns a successful response a couple of seconds later (depending on how fast your connection is). Since programmers want their users to have a snappier experience, instead of waiting for a confirmation, they update the UI immediately (as if the action were successful) and then roll back if the server doesn't return a successful response (which rarely happens). This pattern of optimistically updating the UI before receiving the confirmation of success is called, you guessed it, an Optimistic Update.

Most popular modern websites use optimistic updates to some degree. As mentioned, Reddit uses them for upvotes and downvotes, Youtube uses them for likes, and Trello uses them when moving cards between lists.

Optimistic updates are a significant UX improvement, but since they introduce additional state (which can get out of sync with the server), they can be tricky to get right. Then there's also the issue of writing additional code for managing the cache and rolling back the changes if the request ends up failing. Luckily, we're here to help!

Wasp recently added native support for optimistic updates, and the rest of this post demonstrates how to quickly set it up in your Wasp application.

A Wasp Todo App Without Optimistic Updates

To honor the tradition of demonstrating UIs using Todo apps, We'll show you how to improve the UX of toggling an item's status when working with a slow connection. Before looking at our todo app in action, let's see how we've implemented it in Wasp.

These are the relevant declarations in our .wasp file:

main.wasp
entity Task {=psl
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
psl=}

// A query for fetching all tasks.
query getTasks {
fn: import { getTasks } from "@server/queries.js",
entities: [Task]
}


// An action for updating the task's status.
action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}

This is the query we use to fetch the tasks (together with their statuses):

queries.js
export const getTasks = async (args, context) => {
return context.entities.Task.findMany()
}

Here's the action we use to update a task’s status:

actions.js
export const updateTask = async ({ id, isDone }, context) => {
return context.entities.Task.updateMany({
where: { id },
data: { isDone }
})
}

Finally, this is how our client uses this action to update a task:

MainPage.js
import updateTask from '@wasp/queries'

// ...

function Task({ id, isDone, description }) {
return (
<div className="task">
<label className="description">
<input
type='checkbox' id={id}
checked={isDone}
onChange={
(e) => updateTask({ id, isDone: e.target.checked })
}
/><span>{description}</span></label>
</div>
)
}

Let's first see how updating a task looks when everything works as expected (i.e., we're on a fast connection):

Normal todo list

So far, so good! But what happens when our connection is not as fast?

Todo list with lag

Hmm, this isn't quite as smooth as we'd like it to be. The user has to wait for several seconds before seeing their their changes reflected by the UI.

How can we improve it? Well, of course, we can optimistically update the checkbox!

Performing a Wasp Action Optimistically

To perform the updateTask action optimistically, all we need to do is decorate the calling code on the client:

MainPage.js
import updateTask from '@wasp/queries'

// ...

function Task({ id, isDone, description }) {
const updateTaskOptimistically = useAction(updateTask, {
optimisticUpdates: [{
// Addressing the query we want to update.
getQuerySpecifier: () => [getTasks],
// Telling Wasp how to update the addressed query using the new payload
// and the previously cached data.
updateQuery: ({ id, isDone }, oldTasks) => oldTasks.map(
task => task.id === id ? { ...task, isDone } : task
)
}]
})

return (
<div className="task">
<label className="description">
<input
type='checkbox' id={id}
checked={isDone}
onChange={
(e) => updateTaskOptimistically({ id, isDone: e.target.checked })
}
/><span>{description}</span></label>
</div>
)
}

Those are all the changes we need, the rest of the code (i.e., main.wasp, queries.js and actions.js) remains the same. We won't describe the API in detail, but if you're curious, everything is covered by our official docs.

Finally, let's see how this version of the app looks in action:

Optimistically updated todo list

Our app no longer waits for the server before rendering the changes. Instead, it updates the cache optimistically, continues waiting for the response, and rolls back the changes if the action fails (Wasp internally handles all of this). As previously mentioned, simple changes such as this one rarely fail. Therefore, most of the time, the user enjoys their snappier experience without ever knowing anything special is happening in the background.

What Makes Optimistic Updates Difficult

There's an old software engineering joke you're probably familiar with:

There are only two hard things in Computer Science: cache invalidation and naming things.

Optimistically updating a query involves plenty of meddling with the client-side cache, which is bound to come with a few gotchas. Examples include the answers to questions such as:

  • What happens when an optimistically updated action fails?
  • What happens when the user uses the optimistically updated data in a new action?
  • What happens when the user performs a different action that affects the same cached data as the optimistically updated one?
  • etc.

Notice how Wasp users don't need to know about any of these issues when using our optimistic updates API. They only need to tell Wasp which query they wish to update and how, and Wasp takes care of the rest.

Wasp internally uses React Query, an excellent asynchronous state management library we'll gladly recommend to anyone. While React Query does solve some of these problems and helps with some of the rest, we still had to implement quite a complex mechanism to fully cover all edge cases.

Describing this mechanism, although technically interesting, is beyond the scope of a feature announcement. But stay tuned because in a future blog post, we'll be taking a deep dive into the infrastructure Wasp uses to ensure optimistic updates are performed correctly and consistently.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2022/12/01/beta-ide-improvements.html b/blog/2022/12/01/beta-ide-improvements.html index 2d36a65f48..5e169fe00a 100644 --- a/blog/2022/12/01/beta-ide-improvements.html +++ b/blog/2022/12/01/beta-ide-improvements.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Beta brings major IDE improvements

· 6 min read
Martin Sosic

With the Beta release (0.7), Wasp brings its IDE game to a whole new level!

So far Wasp didn’t have much beyond basic syntax highlighting in VSCode, but now it has:

  1. Wasp language server, that brings the following to your .wasp files:
    1. live error reporting in your editor
    2. autocompletion (basic for now)
  2. VSCode Wasp language extension:
    1. snippets (for page, query, action, entity)
    2. improved syntax highlighting for .wasp files
    3. integration with the above-mentioned language server
  3. Support for popular IDEs to fully support Javascript and Typescript files in the Wasp project.

Wasp IDE support in action in VSCode: syntax highlighting, snippets, live error reporting.

Wasp IDE support in action in VSCode: syntax highlighting, snippets, live error reporting.

Wasp Language Server

Wasp Language Server (WLS) is the “brain” behind smart IDE features like live error reporting and autocompletion - so if it seems like IDE actually understands your code to some degree, well that is the language server!

tip

For curious, check out the source code of WLS on Github: https://github.com/wasp-lang/wasp/tree/main/waspc/waspls/src/Wasp/LSP .

Features

Live error/warning reporting

WLS compiles wasp code for you as you work on it and shows you any errors directly in the editor, via red squiggly lines.

Autocompletion

WLS understands at which part of code you are right now and offers appropriate completions for it.

note

Right now WLS is pretty naive here, and mostly focuses on offering available expressions when it realizes you need an expression. This is helpful but just a start, and it will get much smarter in future versions!

Bit of history: why are Language Servers cool

Years ago, there was no standardized way to write something like Language Server for your language, instead, each language was doing something of its own, and then each editor/IDE would also implement its own layer of logic for using it, and that was a loooot of work that needed to be done for each editor!

Luckily, Microsoft then came up with Language Server Protocol - a standardized way of communicating between the “smart” part, implemented by language creators, and the editor/IDE part (language extension) that is using it. This enabled each editor to implement this logic for interacting with language servers only once, and then it can be used for any language server!

This is great for us, language creators, because it means that once we implement a language server for our language, most of the work is done, and the work we need to do per each editor is manageable.

Right now WLS is used only by the VSCode Wasp language extension, but thanks to the nature of the Language Server Protocol, it should be relatively easy to add support for other editors too! Check this GH issue if you are interested in helping.

Setup

The best thing: there is nothing you, as a Wasp user, have to do to set up WLS! It already comes bundled with your installation of wasp → so if you can run wasp projects on your machine, you already have WLS, and it is always of the correct version needed for your current wasp installation. The only thing you need to ensure is you have wasp version ≥ 0.6, and a relatively fresh VSCode Wasp language extension.

An easy way to check that your version of wasp has WLS packaged into it is to run it and look at its usage instructions: it should mention waspls as one of the commands.

Wasp VSCode extension

If we would call Wasp Language Server (WLS) the “backend”, then VSCode Wasp language extension would be “frontend” → it takes care of everything to ensure you have a nice experience working with Wasp in VSCode, while delegating the hardest work to the WLS.

tip

For curious, you can check out its source code here, core of it is just one file: https://github.com/wasp-lang/vscode-wasp/blob/main/src/extension.ts

Features

Syntax highlighting

Nothing unexpected here: it recognizes different parts of Wasp syntax, like type, value, identifier, comment, string, … and colors them appropriately.

If you are curious how is this implemented, check https://github.com/wasp-lang/vscode-wasp/blob/main/syntaxes/wasp.tmLanguage.yaml → the whole syntax of Wasp is described via this “mysterious” old TextMate format, since that is the way to do it in VSCode.

Snippets

Wasp allows you to quickly generate a snippet of code for a new page, query, action, or entity!

Check out our snippet definitions here: https://github.com/wasp-lang/vscode-wasp/blob/main/snippets/wasp.json . It is actually really easy, in VSCode, to define them and add new ones.

Live error reporting + autocompletion

This is done by delegating the work to WLS, as described above!

IDE support for Javascript / Typescript in Wasp project

Due to how unique Wasp is in its approach, getting an IDE to provide all the usual features for Javascript / Typescript wasn’t completely working, and instead, the IDE would get somewhat confused with the context in which files are and would for example not be able to offer “go to definition” for some values, or would not know how to follow the import path.

With Wasp Beta this is now resolved! We resolved this by somewhat changing the structure of the Wasp project and also adding tsconfig.json files that provide IDE with the information needed to correctly analyze the JS/TS source files.

To learn more about Typescript support in Wasp Beta, check this blog post!

What does the future hold?

While Wasp Beta greatly improved IDE support for Wasp, there are still quite a few things we want to improve on:

  1. Smarter autocompletion via WLS.
    1. Right now it suggests any expression when you need an expression. In the future, we want it to know exactly what is the type of needed expression, and suggest only expressions of that type! So if I am in route ... { to: <my_cursor_here> }, then I want to see only pages among the suggested completions, not queries or actions or something else.
    2. Further, we would also like it to autocomplete on dictionary fields → so if I am in route ... { <my_cursor_here> }, it should offer me path and to as completions, as those are only valid fields in the route dictionary.
  2. Extensions for other editors besides VSCode. Now that we have Wasp Language Server, these shouldn’t be too hard to implement! This is also a great task for potential contributors: check this GH issue if you are interested.
  3. Implement Wasp code formatter. We could make it a part of WLS, and then have the editor extension call it on save.
  4. Improve support for PSL (Prisma Schema Language) in .wasp files.

If any of these sound interesting, feel free to join us on our Github, or join the discussion on Discord!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Beta brings major IDE improvements

· 6 min read
Martin Sosic

With the Beta release (0.7), Wasp brings its IDE game to a whole new level!

So far Wasp didn’t have much beyond basic syntax highlighting in VSCode, but now it has:

  1. Wasp language server, that brings the following to your .wasp files:
    1. live error reporting in your editor
    2. autocompletion (basic for now)
  2. VSCode Wasp language extension:
    1. snippets (for page, query, action, entity)
    2. improved syntax highlighting for .wasp files
    3. integration with the above-mentioned language server
  3. Support for popular IDEs to fully support Javascript and Typescript files in the Wasp project.

Wasp IDE support in action in VSCode: syntax highlighting, snippets, live error reporting.

Wasp IDE support in action in VSCode: syntax highlighting, snippets, live error reporting.

Wasp Language Server

Wasp Language Server (WLS) is the “brain” behind smart IDE features like live error reporting and autocompletion - so if it seems like IDE actually understands your code to some degree, well that is the language server!

tip

For curious, check out the source code of WLS on Github: https://github.com/wasp-lang/wasp/tree/main/waspc/waspls/src/Wasp/LSP .

Features

Live error/warning reporting

WLS compiles wasp code for you as you work on it and shows you any errors directly in the editor, via red squiggly lines.

Autocompletion

WLS understands at which part of code you are right now and offers appropriate completions for it.

note

Right now WLS is pretty naive here, and mostly focuses on offering available expressions when it realizes you need an expression. This is helpful but just a start, and it will get much smarter in future versions!

Bit of history: why are Language Servers cool

Years ago, there was no standardized way to write something like Language Server for your language, instead, each language was doing something of its own, and then each editor/IDE would also implement its own layer of logic for using it, and that was a loooot of work that needed to be done for each editor!

Luckily, Microsoft then came up with Language Server Protocol - a standardized way of communicating between the “smart” part, implemented by language creators, and the editor/IDE part (language extension) that is using it. This enabled each editor to implement this logic for interacting with language servers only once, and then it can be used for any language server!

This is great for us, language creators, because it means that once we implement a language server for our language, most of the work is done, and the work we need to do per each editor is manageable.

Right now WLS is used only by the VSCode Wasp language extension, but thanks to the nature of the Language Server Protocol, it should be relatively easy to add support for other editors too! Check this GH issue if you are interested in helping.

Setup

The best thing: there is nothing you, as a Wasp user, have to do to set up WLS! It already comes bundled with your installation of wasp → so if you can run wasp projects on your machine, you already have WLS, and it is always of the correct version needed for your current wasp installation. The only thing you need to ensure is you have wasp version ≥ 0.6, and a relatively fresh VSCode Wasp language extension.

An easy way to check that your version of wasp has WLS packaged into it is to run it and look at its usage instructions: it should mention waspls as one of the commands.

Wasp VSCode extension

If we would call Wasp Language Server (WLS) the “backend”, then VSCode Wasp language extension would be “frontend” → it takes care of everything to ensure you have a nice experience working with Wasp in VSCode, while delegating the hardest work to the WLS.

tip

For curious, you can check out its source code here, core of it is just one file: https://github.com/wasp-lang/vscode-wasp/blob/main/src/extension.ts

Features

Syntax highlighting

Nothing unexpected here: it recognizes different parts of Wasp syntax, like type, value, identifier, comment, string, … and colors them appropriately.

If you are curious how is this implemented, check https://github.com/wasp-lang/vscode-wasp/blob/main/syntaxes/wasp.tmLanguage.yaml → the whole syntax of Wasp is described via this “mysterious” old TextMate format, since that is the way to do it in VSCode.

Snippets

Wasp allows you to quickly generate a snippet of code for a new page, query, action, or entity!

Check out our snippet definitions here: https://github.com/wasp-lang/vscode-wasp/blob/main/snippets/wasp.json . It is actually really easy, in VSCode, to define them and add new ones.

Live error reporting + autocompletion

This is done by delegating the work to WLS, as described above!

IDE support for Javascript / Typescript in Wasp project

Due to how unique Wasp is in its approach, getting an IDE to provide all the usual features for Javascript / Typescript wasn’t completely working, and instead, the IDE would get somewhat confused with the context in which files are and would for example not be able to offer “go to definition” for some values, or would not know how to follow the import path.

With Wasp Beta this is now resolved! We resolved this by somewhat changing the structure of the Wasp project and also adding tsconfig.json files that provide IDE with the information needed to correctly analyze the JS/TS source files.

To learn more about Typescript support in Wasp Beta, check this blog post!

What does the future hold?

While Wasp Beta greatly improved IDE support for Wasp, there are still quite a few things we want to improve on:

  1. Smarter autocompletion via WLS.
    1. Right now it suggests any expression when you need an expression. In the future, we want it to know exactly what is the type of needed expression, and suggest only expressions of that type! So if I am in route ... { to: <my_cursor_here> }, then I want to see only pages among the suggested completions, not queries or actions or something else.
    2. Further, we would also like it to autocomplete on dictionary fields → so if I am in route ... { <my_cursor_here> }, it should offer me path and to as completions, as those are only valid fields in the route dictionary.
  2. Extensions for other editors besides VSCode. Now that we have Wasp Language Server, these shouldn’t be too hard to implement! This is also a great task for potential contributors: check this GH issue if you are interested.
  3. Implement Wasp code formatter. We could make it a part of WLS, and then have the editor extension call it on save.
  4. Improve support for PSL (Prisma Schema Language) in .wasp files.

If any of these sound interesting, feel free to join us on our Github, or join the discussion on Discord!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2022/12/08/fast-fullstack-chatgpt.html b/blog/2022/12/08/fast-fullstack-chatgpt.html index 6e9c7e0fb7..8f24acc139 100644 --- a/blog/2022/12/08/fast-fullstack-chatgpt.html +++ b/blog/2022/12/08/fast-fullstack-chatgpt.html @@ -19,13 +19,13 @@ - - + +
-

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

· 3 min read
Vinny

There’s a lot of hype around ChatGPT at the moment, and for good reason. It’s amazing. But there’s also some very valid criticism: that it’s simply taking the grunt work out of programming by writing boilerplate for us, which we as developers have to maintain!

I expected technology to make programming less laborious, as it does to most things. But I have to admit I expected it to happen by programmers switching to more powerful languages, rather than continuing to write programs full of boilerplate, but having AIs generate most of it.

PG is totally right in his remark above, but what he doesn’t realize is that there are languages out there that attempt to overcome this very problem, and Wasp is one of them.

What makes Wasp unique is that it’s a framework that uses a super simple language to help you build your web app: front-end, server, and deployment. But it’s not a complicated language like Java or Python, it’s more similar to SQL or JSON, so the learning curve is really quick (technically, it’s a Domain Specific Langauge or DSL).

Check it out for yourself:

main.wasp
app todoApp {
title: "ToDo App",/* visible in tab */

auth: {/* full-stack auth out-of-the-box */
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
}
}
}

route RootRoute { path: "/", to: MainPage }
page MainPage {
/* import your React code */
component: import Main from "@client/Main.js"
}

With this simple file above, Wasp will continually compile a truly full-stack web app for you, with a React front-end, and an ExpressJS server. You’re free to then build out the important features yourself with React, NodeJS, Prisma, and react-query.

The great part is, you can probably understand the Wasp syntax without even referencing the docs. Which means AI can probably work with it easily as well. So rather than having AI create a ton of boilerplate for us, we thought “can ChatGPT write Wasp?” If it can, all we need is to have it create that one file, and then the power of Wasp will take care of the rest. No more endless boilerplate!

So that’s exactly what we set to find out in the video above. The results? Well let’s just say they speak for themselves.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

· 3 min read
Vinny

There’s a lot of hype around ChatGPT at the moment, and for good reason. It’s amazing. But there’s also some very valid criticism: that it’s simply taking the grunt work out of programming by writing boilerplate for us, which we as developers have to maintain!

I expected technology to make programming less laborious, as it does to most things. But I have to admit I expected it to happen by programmers switching to more powerful languages, rather than continuing to write programs full of boilerplate, but having AIs generate most of it.

PG is totally right in his remark above, but what he doesn’t realize is that there are languages out there that attempt to overcome this very problem, and Wasp is one of them.

What makes Wasp unique is that it’s a framework that uses a super simple language to help you build your web app: front-end, server, and deployment. But it’s not a complicated language like Java or Python, it’s more similar to SQL or JSON, so the learning curve is really quick (technically, it’s a Domain Specific Langauge or DSL).

Check it out for yourself:

main.wasp
app todoApp {
title: "ToDo App",/* visible in tab */

auth: {/* full-stack auth out-of-the-box */
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {},
google: {}
}
}
}

route RootRoute { path: "/", to: MainPage }
page MainPage {
/* import your React code */
component: import Main from "@client/Main.js"
}

With this simple file above, Wasp will continually compile a truly full-stack web app for you, with a React front-end, and an ExpressJS server. You’re free to then build out the important features yourself with React, NodeJS, Prisma, and react-query.

The great part is, you can probably understand the Wasp syntax without even referencing the docs. Which means AI can probably work with it easily as well. So rather than having AI create a ton of boilerplate for us, we thought “can ChatGPT write Wasp?” If it can, all we need is to have it create that one file, and then the power of Wasp will take care of the rest. No more endless boilerplate!

So that’s exactly what we set to find out in the video above. The results? Well let’s just say they speak for themselves.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/01/11/betathon-review.html b/blog/2023/01/11/betathon-review.html index 46e56d1103..b5e0ca768d 100644 --- a/blog/2023/01/11/betathon-review.html +++ b/blog/2023/01/11/betathon-review.html @@ -19,13 +19,13 @@ - - + +
-

Hosting Our First Hackathon: Results & Review

· 6 min read
Vinny

To finalize the Wasp Beta launch week, we held a Beta Hackathon, which we dubbed the “Betathon”. The idea was to hold a simple, open, and fun hackathon to encourage users to build with Wasp, and that’s exactly what they did!

As Wasp is still in its early days, we weren’t sure what the response would be, or if there’d be any response at all. Considering that we didn’t do much promotion of the Hackathon outside of our own channels, we were surprised by the results.

In this post, I’ll give you a quick run-down of:

  • the hackathon results 🏆
  • how the hackathon was organized
  • how we promoted it
  • the community response

…and the Winners Are:

What’s a hackathon without the participants!? Let’s get this post off to a proper start by congratulating our winners and showcasing their work. 🔍

🥇 Tim’s Job Board

Tim's Job Board

Tim really went for it and created a feature-rich Job Board:

Wasp is very awesome! Easy setup and start-up especially if you're familiar with the Prisma ORM and Tailwind CSS. The stack is small but powerful... I'm going to use Wasp on a few MVP projects this year.” - Tim

🥈Chris’s “Cook Wherever” Recipes App

Chris's Cook Wherever Recipes App

Chris created an extensive database of recipes in a slick app:

This was the best app dev experience I ever had! …Walking through the docs, I immediately figured out how to use Wasp and was able to make a prototype in a couple of days.” - Chris

🥉 Richard’s Roadmap & Feature Voting App

Richard’s Roadmap & Feature Voting App

I liked how Wasp simplified writing query/actions that are used to interact with the backend and frontend. How everything is defined and configured in wasp file and just works. Also […] login/signup was really easy to do since Wasp provides these two methods for use.” -

🥉 Emmanuel’s Notes App

Emmanuel’s Notes App

I joined the hackathon less than 48 hours before the submission deadline. Wasp made it look easy because it handled the hard parts for me. For example, username/password authentication took less than 7 lines of code to implement. - excerpt from Emmanuel’s Betathon Blog Post

Hackathon How-to

Personally, I’ve never organized a hackathon before, and this was Wasp’s first hackathon as well, so when you’re a complete newbie at something, you often look towards others for inspiration. Being admirers of the work and style of Supabase, we drew a lot of inspiration from their “launch week” approach when preparing for our own Beta launch and hacakthon.

Wasp Betathon Homepage
Our dedicated hackathon landing page w/ intro video & submission form

With some good inspiration in hand, we set off to create a simple, easy-going Hackathon experience. We weren’t certain we’d get many participants, so we decided to make the process as open as possible: two weeks to work on any project using Wasp, alone or in a team of up to 4 people, submitted on our Betathon Homepage before the deadline. That was it.

When you’re an early-stage startup, you can’t offer big cash prizes, so we asked Railway if they’d be interested in sponsoring some prizes, as we’re big fans of their deployment and hosting platform. Luckily, they agreed (thanks, Railway 🙏🚂). It was also a great match, since we already had the documentation for deploying Wasp apps to Railway on our website, making it an obvious choice for the participants to deploy their Hackathon apps with.

Keyboard
Disclaimer: actual prize keyboard will be cooler and waspier 😎🐝

On top of that, we decided that a cool grand prize could be a Wasp-colored mechanical keyboard. Nothing fancy, but keyboards are an item a lot of programmers love. We also threw in some Wasp beanies and shirts, and stated that we’d spotlight the winner’s on our platforms and social media accounts.

Promotion

For the Wasp Beta Launch Week, we were active and publicising Wasp on many platforms. We didn’t outright promote the hackathon on those platforms, but we were getting a lot of incoming interest to our Website and Discord, so we made noise about it there. We posted banners on the homepage, and made announcements on Discord and Twitter that directed people to a Beta Hacakthon homepage we created.

The homepage was nice to have as a central spot for all the rules and relevant info. We also added a fun intro video to give the hackathon a more personal touch. I also think the effort put into making an intro video gives participants the feeling that they’re entering into a serious contest and committing to something of substance.

Hackathon Wasp app repo
Wanna host your own Hackathon? Use our template app!

As an extra bonus, we wrote the Betathon Homepage with Wasp, and put the source code up on our GitHub. We thought it might inspire people to build with Wasp, using it as a guide while creating their own projects for the hackathon, plus it could be used by others in the future if they want to host their own hackathon. 💻

The Response

The response overall was small but significant, considering Wasp’s age. We were also extremely happy with the quality of the engagement. We had thirteen participants register overall, a nice number considering we only started promoting the hackathon on the day that we announced it (this is probably something we’d do differently next time)!

We also asked participants for their feedback on participating in the Hackathon, and they were all pleased with the open, straight-forward approach we took, so we’ll most likely be repeating this for future versions. Other good signs were the many comments that participants were eager to take part in our next hackathon, as well as some dedicated new community members, which makes it all the more motivating for us. 💪


A big THANK YOU again to all the participants for their hard work and feedback. Here’s to the next one! 🍻

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Hosting Our First Hackathon: Results & Review

· 6 min read
Vinny

To finalize the Wasp Beta launch week, we held a Beta Hackathon, which we dubbed the “Betathon”. The idea was to hold a simple, open, and fun hackathon to encourage users to build with Wasp, and that’s exactly what they did!

As Wasp is still in its early days, we weren’t sure what the response would be, or if there’d be any response at all. Considering that we didn’t do much promotion of the Hackathon outside of our own channels, we were surprised by the results.

In this post, I’ll give you a quick run-down of:

  • the hackathon results 🏆
  • how the hackathon was organized
  • how we promoted it
  • the community response

…and the Winners Are:

What’s a hackathon without the participants!? Let’s get this post off to a proper start by congratulating our winners and showcasing their work. 🔍

🥇 Tim’s Job Board

Tim's Job Board

Tim really went for it and created a feature-rich Job Board:

Wasp is very awesome! Easy setup and start-up especially if you're familiar with the Prisma ORM and Tailwind CSS. The stack is small but powerful... I'm going to use Wasp on a few MVP projects this year.” - Tim

🥈Chris’s “Cook Wherever” Recipes App

Chris's Cook Wherever Recipes App

Chris created an extensive database of recipes in a slick app:

This was the best app dev experience I ever had! …Walking through the docs, I immediately figured out how to use Wasp and was able to make a prototype in a couple of days.” - Chris

🥉 Richard’s Roadmap & Feature Voting App

Richard’s Roadmap & Feature Voting App

I liked how Wasp simplified writing query/actions that are used to interact with the backend and frontend. How everything is defined and configured in wasp file and just works. Also […] login/signup was really easy to do since Wasp provides these two methods for use.” -

🥉 Emmanuel’s Notes App

Emmanuel’s Notes App

I joined the hackathon less than 48 hours before the submission deadline. Wasp made it look easy because it handled the hard parts for me. For example, username/password authentication took less than 7 lines of code to implement. - excerpt from Emmanuel’s Betathon Blog Post

Hackathon How-to

Personally, I’ve never organized a hackathon before, and this was Wasp’s first hackathon as well, so when you’re a complete newbie at something, you often look towards others for inspiration. Being admirers of the work and style of Supabase, we drew a lot of inspiration from their “launch week” approach when preparing for our own Beta launch and hacakthon.

Wasp Betathon Homepage
Our dedicated hackathon landing page w/ intro video & submission form

With some good inspiration in hand, we set off to create a simple, easy-going Hackathon experience. We weren’t certain we’d get many participants, so we decided to make the process as open as possible: two weeks to work on any project using Wasp, alone or in a team of up to 4 people, submitted on our Betathon Homepage before the deadline. That was it.

When you’re an early-stage startup, you can’t offer big cash prizes, so we asked Railway if they’d be interested in sponsoring some prizes, as we’re big fans of their deployment and hosting platform. Luckily, they agreed (thanks, Railway 🙏🚂). It was also a great match, since we already had the documentation for deploying Wasp apps to Railway on our website, making it an obvious choice for the participants to deploy their Hackathon apps with.

Keyboard
Disclaimer: actual prize keyboard will be cooler and waspier 😎🐝

On top of that, we decided that a cool grand prize could be a Wasp-colored mechanical keyboard. Nothing fancy, but keyboards are an item a lot of programmers love. We also threw in some Wasp beanies and shirts, and stated that we’d spotlight the winner’s on our platforms and social media accounts.

Promotion

For the Wasp Beta Launch Week, we were active and publicising Wasp on many platforms. We didn’t outright promote the hackathon on those platforms, but we were getting a lot of incoming interest to our Website and Discord, so we made noise about it there. We posted banners on the homepage, and made announcements on Discord and Twitter that directed people to a Beta Hacakthon homepage we created.

The homepage was nice to have as a central spot for all the rules and relevant info. We also added a fun intro video to give the hackathon a more personal touch. I also think the effort put into making an intro video gives participants the feeling that they’re entering into a serious contest and committing to something of substance.

Hackathon Wasp app repo
Wanna host your own Hackathon? Use our template app!

As an extra bonus, we wrote the Betathon Homepage with Wasp, and put the source code up on our GitHub. We thought it might inspire people to build with Wasp, using it as a guide while creating their own projects for the hackathon, plus it could be used by others in the future if they want to host their own hackathon. 💻

The Response

The response overall was small but significant, considering Wasp’s age. We were also extremely happy with the quality of the engagement. We had thirteen participants register overall, a nice number considering we only started promoting the hackathon on the day that we announced it (this is probably something we’d do differently next time)!

We also asked participants for their feedback on participating in the Hackathon, and they were all pleased with the open, straight-forward approach we took, so we’ll most likely be repeating this for future versions. Other good signs were the many comments that participants were eager to take part in our next hackathon, as well as some dedicated new community members, which makes it all the more motivating for us. 💪


A big THANK YOU again to all the participants for their hard work and feedback. Here’s to the next one! 🍻

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/01/18/wasp-beta-update-dec.html b/blog/2023/01/18/wasp-beta-update-dec.html index 1a53890699..4f516b1b7a 100644 --- a/blog/2023/01/18/wasp-beta-update-dec.html +++ b/blog/2023/01/18/wasp-beta-update-dec.html @@ -19,14 +19,14 @@ - - + +
-

Wasp Beta December 2022

· 6 min read
Matija Sosic

Wasp Update Dec 22

Want to stay in the loop? → Join our newsletter!

Hey Wasp tribe 🐝 ,

Happy New Year! I know you're probably already sick of hearing it, but hopefully we're the last ones to congratulate you 🔫 👈 (that's pistol fingers emoji in case you were wondering).

Pistol fingers
This is how I imagine myself telling the joke above.

Now that the Beta Launch craze is over (thanks for your support, it was amazing - we saw more devs hacking with Wasp than ever!), we're back to our usual programming. Let's dive in and see what's new and what's in the plans for this year:

🎮 🐝 We hosted our first hackathon - it was a blast! 🎉 🎉

Tweet about Wasp

We launched our first Wasp hackathon ever on the last day of Beta Launch (thus we named it Betathon) and got some really cool submissions! Winners received hosting credits kindly offered by our partners at Railway and a special 1st place award was a wasp-themed mechanical keyboard (we're still assembling it but we'll post photos on our twitter :))!

This was the best app dev experience I ever had! …Walking through the docs, I immediately figured out how to use Wasp and was able to make a prototype in a couple of days.” - Chris

To check out the winning projects and see where devs found Wasp most helpful, take a look here: Wasp Betathon review post

🔑 New auth method - GitHub! 🐙

Next to username/password and Google, Wasp now also supports GitHub as an authentication method!

Support for GitHub auth in Wasp

Putting the code above in your main.wasp file and specifying your GitHub env variables is all you need to do! Wasp will provide you with a full-stack GitHub authentication along with UI helpers (GitHub sign-up button) you can immediately use in your React component.

For more details, check the docs here.

💬 Let's discuss - on GitHub Discussions!

Wasp is now on GitHub Discussions

So far we've been capturing your feedback across GitHub issues and Wasp Discord server, but with the current volume it has become a bit unwieldy and hard to keep track of.

That's why we introduced Wasp GitHub Discussions! It's a relatively new service by GitHub that allows distinguishing between specific, well-defined issues (bug reports, TODOs, ...) and discussion items (ideating about new features, figuring out best practices, etc) and allows for upvotes from the community.

If there is a feature you'd like to see in Wasp (e.g. support for Vue) you can create a new post for it or upvote it if it is already there!

🚀 Next launch is coming - a super early sneak peek 👀

Next launch sneak peek

We know we just wrapped up Beta release, but we are busy wasps and our heads are already in the next one! We made a preliminary draft of the features that are going to be included - the "theme" of this release is going to be about making Wasp super easy and friendly for you to use.

We'll further polish our auth & deployment experience, along with ensuring TypeScript experience is fully typed and as helpful as possible. Stay tuned for the official roadmap and date of the next launch!

Want to make sure your fav feature makes it into the next release? Let us know on Discussions!

🎥 Wasp is now on YouTube!

Wasp is on YouTube

Thanks to Vince, who recently joined as Devrel (intro blog post coming soon!), Wasp now finally has its YouTube channel!

We're just starting out but already made some splashes - our "Build a full-stack app in 9 mins with Wasp and ChatGPT" got over 2k views (not bad for a channel with 50 subscribers, right?).

We also made our first YT short, featuring how to add auth to your app in 60 seconds with Wasp.

If you want to stay in the loop (and I guess you do since you're reading this :D), please subscribe to our channel and help us reach the first 100 subscribers on YouTube!

Subscribe to Wasp on YouTube
You know you want it!

🕹 Community highlights

Wasp Github Star Growth - over 2,000 ⭐️, woohoo!

Beta was great and it brought us to 2,234 stars! We never imagined Wasp could become so popular when we were just getting started. Huge thanks to all our contributors and stargazers - you are amazing!

Wasp has over 2,000 GitHub stars

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

And before you leave, here's a photo of a squishy wasp (ok, it's a bumblebee, but you get it) proudly rocking Wasp swag 🤘 🐝 (yep, we got a bunch of these for the office, you can also see Martin the background :D)!

Wasp's new mascot
This lil' boy actually became pretty popular in our community - we're now looking for a name for him!

Thanks for reading and see you in a month!

Buzzity buzz, you got that pizzazz 🐝 🐝,
+

Wasp Beta December 2022

· 6 min read
Matija Sosic

Wasp Update Dec 22

Want to stay in the loop? → Join our newsletter!

Hey Wasp tribe 🐝 ,

Happy New Year! I know you're probably already sick of hearing it, but hopefully we're the last ones to congratulate you 🔫 👈 (that's pistol fingers emoji in case you were wondering).

Pistol fingers
This is how I imagine myself telling the joke above.

Now that the Beta Launch craze is over (thanks for your support, it was amazing - we saw more devs hacking with Wasp than ever!), we're back to our usual programming. Let's dive in and see what's new and what's in the plans for this year:

🎮 🐝 We hosted our first hackathon - it was a blast! 🎉 🎉

Tweet about Wasp

We launched our first Wasp hackathon ever on the last day of Beta Launch (thus we named it Betathon) and got some really cool submissions! Winners received hosting credits kindly offered by our partners at Railway and a special 1st place award was a wasp-themed mechanical keyboard (we're still assembling it but we'll post photos on our twitter :))!

This was the best app dev experience I ever had! …Walking through the docs, I immediately figured out how to use Wasp and was able to make a prototype in a couple of days.” - Chris

To check out the winning projects and see where devs found Wasp most helpful, take a look here: Wasp Betathon review post

🔑 New auth method - GitHub! 🐙

Next to username/password and Google, Wasp now also supports GitHub as an authentication method!

Support for GitHub auth in Wasp

Putting the code above in your main.wasp file and specifying your GitHub env variables is all you need to do! Wasp will provide you with a full-stack GitHub authentication along with UI helpers (GitHub sign-up button) you can immediately use in your React component.

For more details, check the docs here.

💬 Let's discuss - on GitHub Discussions!

Wasp is now on GitHub Discussions

So far we've been capturing your feedback across GitHub issues and Wasp Discord server, but with the current volume it has become a bit unwieldy and hard to keep track of.

That's why we introduced Wasp GitHub Discussions! It's a relatively new service by GitHub that allows distinguishing between specific, well-defined issues (bug reports, TODOs, ...) and discussion items (ideating about new features, figuring out best practices, etc) and allows for upvotes from the community.

If there is a feature you'd like to see in Wasp (e.g. support for Vue) you can create a new post for it or upvote it if it is already there!

🚀 Next launch is coming - a super early sneak peek 👀

Next launch sneak peek

We know we just wrapped up Beta release, but we are busy wasps and our heads are already in the next one! We made a preliminary draft of the features that are going to be included - the "theme" of this release is going to be about making Wasp super easy and friendly for you to use.

We'll further polish our auth & deployment experience, along with ensuring TypeScript experience is fully typed and as helpful as possible. Stay tuned for the official roadmap and date of the next launch!

Want to make sure your fav feature makes it into the next release? Let us know on Discussions!

🎥 Wasp is now on YouTube!

Wasp is on YouTube

Thanks to Vince, who recently joined as Devrel (intro blog post coming soon!), Wasp now finally has its YouTube channel!

We're just starting out but already made some splashes - our "Build a full-stack app in 9 mins with Wasp and ChatGPT" got over 2k views (not bad for a channel with 50 subscribers, right?).

We also made our first YT short, featuring how to add auth to your app in 60 seconds with Wasp.

If you want to stay in the loop (and I guess you do since you're reading this :D), please subscribe to our channel and help us reach the first 100 subscribers on YouTube!

Subscribe to Wasp on YouTube
You know you want it!

🕹 Community highlights

Wasp Github Star Growth - over 2,000 ⭐️, woohoo!

Beta was great and it brought us to 2,234 stars! We never imagined Wasp could become so popular when we were just getting started. Huge thanks to all our contributors and stargazers - you are amazing!

Wasp has over 2,000 GitHub stars

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

And before you leave, here's a photo of a squishy wasp (ok, it's a bumblebee, but you get it) proudly rocking Wasp swag 🤘 🐝 (yep, we got a bunch of these for the office, you can also see Martin the background :D)!

Wasp's new mascot
This lil' boy actually became pretty popular in our community - we're now looking for a name for him!

Thanks for reading and see you in a month!

Buzzity buzz, you got that pizzazz 🐝 🐝,
Matija, Martin and the Wasp team

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/01/31/wasp-beta-launch-review.html b/blog/2023/01/31/wasp-beta-launch-review.html index 8aac16d6b6..9cec779482 100644 --- a/blog/2023/01/31/wasp-beta-launch-review.html +++ b/blog/2023/01/31/wasp-beta-launch-review.html @@ -19,13 +19,13 @@ - - + +
-

Convincing developers to try a new web framework - the effects of launching beta

· 7 min read
Matija Sosic

Alpha feedback

We are developing an OSS web framework in a form of a config language (DSL) that works with React & Node.js. Getting developers to use a new tool (especially a web framework) is a pretty hard thing to do. We wished there were more stories of how today's mainstream tools got adopted that we could learn from, so that motivated us to document our own.

Want to stay in the loop? → Join our newsletter!

TL;DR

  • HackerNews launch post brought the most traffic, by far
  • Product Hunt launch went worse than expected, bots took over
  • Our goal was to reach GitHub Trending but we failed
  • Less overall traffic than for the Alpha launch, but much higher quality of feedback + a shift in public perception
  • Having a public launch date made us 3x more productive

📊 The results: stats

We launched Beta on Nov 27, 2022 in a launch week format, recently popularized by Supabase. During the first week we launched on Product Hunt, and after the weekend we posted on HackerNews. Here's what the numbers were on the last day of the launch:

  • 190 GitHub stars added to the repo
  • 108 new projects started
  • 83 new users (installed Wasp locally and ran it)

Web visitors during beta launch week

HN launch caused almost 2x spike in traffic and usage. Also, although our launch week already ended by the start of December, we actually had the most users ever throughout December:

WAU displayed monthly

Looking back, this wasn't at all our biggest event in terms of traffic, but it was in terms of usage:

All time stats

One of the main effects of the launch (together with a few recent successful HN posts, and the Alpha Testing Program we ran in Jul '22) is that we managed to move the baseline WAU from ~10 to ~20. Another effect, felt more subjectively, is the change in the community perception.

Community perception shift

As mentioned above, although our Alpha launch had higher absolute numbers (website traffic, HN upvotes etc), it felt that Beta launch caused the biggest perception shift in the community so far.

Before were mostly getting superficial comments like “this looks cool, I’ll give it a try once”, or “why DSL approach and not the other one”, and this time we could notice that portion of people already knew Wasp from before (some even used it), and had more specific questions, even proposing next features that we planned but haven’t published yet.

Beta feedback

Although the core message (DSL for developing full-stack web apps with React & Node.js) hasn’t changed, there was significantly less pushback to the concept than before. I guess it comes down to the time elapsed and the product being more polished and validated from the outside - Beta, published use-cases, testimonials, …

Before the launch

This was our initial plan:

Launch timeline

For 20 days before the launch we were posting daily countdown banners on Twitter + a few polls (e.g. what's your favourite CSS framework) to engage the audience.

Examples of pre-launch tweets

Our Twitter game is still super young (~500 followers) so it didn't have a big effect but it helped to get the team excited and a few people also noticed it and commented/voted.

Due to the lack of time we ended up doing user testing in-house. That's still something I'd like to improve and make a habit of in the future.

A few other things we did prior to the launch:

  • Redesigned our project page - gave it a new, sleeker look
  • Published use cases with our most successful users and featured them on the project page
  • Activated our Discord and email list
  • Organized a launch event (call on Discord) to celebrate the launch - it went better than expected, a decent amount of people showed up and we had some good discussions!

The launch

As mentioned, we went with a launch week format - we liked the idea of having a whole week filled with content rather than cramming everything in a single day. We highlighted a new feature every day + launched a hackathon on the last day of the week, to keep the momentum. You can see the full schedule here.

Launch week schedule

We also shared our launch news at different places, most successful being Product Hunt, HackerNews and Reddit.

Product Hunt - failed, but ok

The mistake we did was launching on the Thanksgiving weekend - there was little (real) traffic + the mods were away so the bots took over!

We ended up as #5 product of the day with ~250 upvotes, which wasn’t so bad because in the end we got featured in their daily newsletter with 1M+ subscribers.

The bad part was that mods were away and pretty much all other products in front of us were fake or obviously bot powered! It felt like there was no real interaction on any of these products, just endless “congrats on the launch” comments from the newly created accounts with obviously fake names. Two products were also clearly violating PH rules (one was the same product that launched a week or two ago, but just changed the name).

The most disappointing part for us (and especially for the team) was that it felt like there aren’t any real people on PH, just bots.

🕹 Post-launch: Wasp Hackathon #1 - Betathon!

Since we introduced all the new features during the launch week, we thought a good way to keep the community engaged and give them a reason to try Wasp Beta out would be to throw a hackathon! It was the first time we did so we weren't sure how it'd go, but it went better than expected!

Tweet about Betathon - our #1 hackathon!

In the end, it was definitely worth it (see review and submissions here). It was quite lightweight to organize (we even made a custom web app with Wasp for the hackathon which you can also use for your hackathon) and we got some really nice submissions and community shout-outs.

Announcing a launch date publicly is great for productivity

Another big benefit we noticed from this type of launching is how much more productive it made the whole team. Although the launch date was totally self-imposed (and we did move it a couple of times internally), it was still an amazing forcing function once we announced it publicly. It focused the efforts of the whole team and it also felt great.

We decided to keep going with the quarterly release schedule in this format - 3 months is just enough time to make a dent on the product side, but not long enough to get stuck or caught up with endless refactoring. It also forces us to plan for the features that will have most impact on the developers using Wasp and make their lives easier, because we all want to have something cool and useful to present during the launch week.

Conclusion

I hope you found this post helpful or at least interesting! Creating a new web framework might be one of the most notorious things to do as a developer, but that shouldn't be a reason not to do it - where are the new frameworks going to come from otherwise?

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Convincing developers to try a new web framework - the effects of launching beta

· 7 min read
Matija Sosic

Alpha feedback

We are developing an OSS web framework in a form of a config language (DSL) that works with React & Node.js. Getting developers to use a new tool (especially a web framework) is a pretty hard thing to do. We wished there were more stories of how today's mainstream tools got adopted that we could learn from, so that motivated us to document our own.

Want to stay in the loop? → Join our newsletter!

TL;DR

  • HackerNews launch post brought the most traffic, by far
  • Product Hunt launch went worse than expected, bots took over
  • Our goal was to reach GitHub Trending but we failed
  • Less overall traffic than for the Alpha launch, but much higher quality of feedback + a shift in public perception
  • Having a public launch date made us 3x more productive

📊 The results: stats

We launched Beta on Nov 27, 2022 in a launch week format, recently popularized by Supabase. During the first week we launched on Product Hunt, and after the weekend we posted on HackerNews. Here's what the numbers were on the last day of the launch:

  • 190 GitHub stars added to the repo
  • 108 new projects started
  • 83 new users (installed Wasp locally and ran it)

Web visitors during beta launch week

HN launch caused almost 2x spike in traffic and usage. Also, although our launch week already ended by the start of December, we actually had the most users ever throughout December:

WAU displayed monthly

Looking back, this wasn't at all our biggest event in terms of traffic, but it was in terms of usage:

All time stats

One of the main effects of the launch (together with a few recent successful HN posts, and the Alpha Testing Program we ran in Jul '22) is that we managed to move the baseline WAU from ~10 to ~20. Another effect, felt more subjectively, is the change in the community perception.

Community perception shift

As mentioned above, although our Alpha launch had higher absolute numbers (website traffic, HN upvotes etc), it felt that Beta launch caused the biggest perception shift in the community so far.

Before were mostly getting superficial comments like “this looks cool, I’ll give it a try once”, or “why DSL approach and not the other one”, and this time we could notice that portion of people already knew Wasp from before (some even used it), and had more specific questions, even proposing next features that we planned but haven’t published yet.

Beta feedback

Although the core message (DSL for developing full-stack web apps with React & Node.js) hasn’t changed, there was significantly less pushback to the concept than before. I guess it comes down to the time elapsed and the product being more polished and validated from the outside - Beta, published use-cases, testimonials, …

Before the launch

This was our initial plan:

Launch timeline

For 20 days before the launch we were posting daily countdown banners on Twitter + a few polls (e.g. what's your favourite CSS framework) to engage the audience.

Examples of pre-launch tweets

Our Twitter game is still super young (~500 followers) so it didn't have a big effect but it helped to get the team excited and a few people also noticed it and commented/voted.

Due to the lack of time we ended up doing user testing in-house. That's still something I'd like to improve and make a habit of in the future.

A few other things we did prior to the launch:

  • Redesigned our project page - gave it a new, sleeker look
  • Published use cases with our most successful users and featured them on the project page
  • Activated our Discord and email list
  • Organized a launch event (call on Discord) to celebrate the launch - it went better than expected, a decent amount of people showed up and we had some good discussions!

The launch

As mentioned, we went with a launch week format - we liked the idea of having a whole week filled with content rather than cramming everything in a single day. We highlighted a new feature every day + launched a hackathon on the last day of the week, to keep the momentum. You can see the full schedule here.

Launch week schedule

We also shared our launch news at different places, most successful being Product Hunt, HackerNews and Reddit.

Product Hunt - failed, but ok

The mistake we did was launching on the Thanksgiving weekend - there was little (real) traffic + the mods were away so the bots took over!

We ended up as #5 product of the day with ~250 upvotes, which wasn’t so bad because in the end we got featured in their daily newsletter with 1M+ subscribers.

The bad part was that mods were away and pretty much all other products in front of us were fake or obviously bot powered! It felt like there was no real interaction on any of these products, just endless “congrats on the launch” comments from the newly created accounts with obviously fake names. Two products were also clearly violating PH rules (one was the same product that launched a week or two ago, but just changed the name).

The most disappointing part for us (and especially for the team) was that it felt like there aren’t any real people on PH, just bots.

🕹 Post-launch: Wasp Hackathon #1 - Betathon!

Since we introduced all the new features during the launch week, we thought a good way to keep the community engaged and give them a reason to try Wasp Beta out would be to throw a hackathon! It was the first time we did so we weren't sure how it'd go, but it went better than expected!

Tweet about Betathon - our #1 hackathon!

In the end, it was definitely worth it (see review and submissions here). It was quite lightweight to organize (we even made a custom web app with Wasp for the hackathon which you can also use for your hackathon) and we got some really nice submissions and community shout-outs.

Announcing a launch date publicly is great for productivity

Another big benefit we noticed from this type of launching is how much more productive it made the whole team. Although the launch date was totally self-imposed (and we did move it a couple of times internally), it was still an amazing forcing function once we announced it publicly. It focused the efforts of the whole team and it also felt great.

We decided to keep going with the quarterly release schedule in this format - 3 months is just enough time to make a dent on the product side, but not long enough to get stuck or caught up with endless refactoring. It also forces us to plan for the features that will have most impact on the developers using Wasp and make their lives easier, because we all want to have something cool and useful to present during the launch week.

Conclusion

I hope you found this post helpful or at least interesting! Creating a new web framework might be one of the most notorious things to do as a developer, but that shouldn't be a reason not to do it - where are the new frameworks going to come from otherwise?

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/02/02/no-best-framework.html b/blog/2023/02/02/no-best-framework.html index 6b96f1470d..f9d0f5de8c 100644 --- a/blog/2023/02/02/no-best-framework.html +++ b/blog/2023/02/02/no-best-framework.html @@ -19,13 +19,13 @@ - - + +
-

The Best Web App Framework Doesn't Exist

· 3 min read
Vinny

The web app framework you choose doesn’t really matter. Well, it matters, just not as much as others would like you to believe.

The fact that so many libraries and frameworks exist in 2023, and that the best one is still hotly debated, proves my point. It’s the web developers biggest “first-world problem” — a problem that’s not really a problem. On Maslow’s Hierarchy of Developer Needs, it’s definitely near the top (ok, I made that up 😅)


hierarchy of developer needs


For example, according the the StateOfJS survey, there were 5 Front-end Frameworks with good retention in 2018, now there are 11 in 2022. That’s a 120% increase in a matter of 4 years, and that’s not even taking into account the hot meta-frameworks like NextJS, SvelteKit, or Astro!


State of JS 2022
A growing family of frameworks...


These are great developments for the space, overall. They improve things like developer speed, bundle size, performance, and developer experience. But they also make it damn hard for developers and teams to make a decision when trying to decide which to use for their next project. It’s even worse for beginners, which is probably why they just go for React — which, of course, is perfectly fine.

And I think all of this is OK, because in the end it doesn’t really matter which one you choose. When it really comes down to it, all that matters is that the framework you chose:

  • Is stable
  • Allows you to move quickly
  • Allows you to reach your end goal

Why? Because most of them are built around the same concepts, have proven themselves capable of performing at scale, and have communities you can engage with and learn from.

React might be the most prominent in job descriptions, but if you’re looking for a new role and only have experience in Vue or Angular, I can’t imagine it would take you more than a week to build a side-project with React to display your ability to prospective employers.

On the flip side, if you’re a beginner or Junior dev, once you have the basics of HTML, CSS, and JS under your belt, it doesn’t really matter what framework you learn. I personally started learning backend development with Node/ExpressJS, but landed my first role as a Frontend developer with Angular. In my second role I used NextJS, and now I work with Wasp (a full-stack framework built on top of React and ExpressJS). Developers never stop learning, so it’s kind of a non-argument to deride any specific framework — unless it really sucks, but then no one will continue to use it anyway.


Use what works


So, in the end, use what works. Because in 99.99% of cases, your choice of web framework will not decide the fate of your project.

If you’ve done a bit of research and found a framework that suits your needs and you enjoy using it — use it. There’s really no good reason not to.



Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

The Best Web App Framework Doesn't Exist

· 3 min read
Vinny

The web app framework you choose doesn’t really matter. Well, it matters, just not as much as others would like you to believe.

The fact that so many libraries and frameworks exist in 2023, and that the best one is still hotly debated, proves my point. It’s the web developers biggest “first-world problem” — a problem that’s not really a problem. On Maslow’s Hierarchy of Developer Needs, it’s definitely near the top (ok, I made that up 😅)


hierarchy of developer needs


For example, according the the StateOfJS survey, there were 5 Front-end Frameworks with good retention in 2018, now there are 11 in 2022. That’s a 120% increase in a matter of 4 years, and that’s not even taking into account the hot meta-frameworks like NextJS, SvelteKit, or Astro!


State of JS 2022
A growing family of frameworks...


These are great developments for the space, overall. They improve things like developer speed, bundle size, performance, and developer experience. But they also make it damn hard for developers and teams to make a decision when trying to decide which to use for their next project. It’s even worse for beginners, which is probably why they just go for React — which, of course, is perfectly fine.

And I think all of this is OK, because in the end it doesn’t really matter which one you choose. When it really comes down to it, all that matters is that the framework you chose:

  • Is stable
  • Allows you to move quickly
  • Allows you to reach your end goal

Why? Because most of them are built around the same concepts, have proven themselves capable of performing at scale, and have communities you can engage with and learn from.

React might be the most prominent in job descriptions, but if you’re looking for a new role and only have experience in Vue or Angular, I can’t imagine it would take you more than a week to build a side-project with React to display your ability to prospective employers.

On the flip side, if you’re a beginner or Junior dev, once you have the basics of HTML, CSS, and JS under your belt, it doesn’t really matter what framework you learn. I personally started learning backend development with Node/ExpressJS, but landed my first role as a Frontend developer with Angular. In my second role I used NextJS, and now I work with Wasp (a full-stack framework built on top of React and ExpressJS). Developers never stop learning, so it’s kind of a non-argument to deride any specific framework — unless it really sucks, but then no one will continue to use it anyway.


Use what works


So, in the end, use what works. Because in 99.99% of cases, your choice of web framework will not decide the fate of your project.

If you’ve done a bit of research and found a framework that suits your needs and you enjoy using it — use it. There’s really no good reason not to.



Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/02/14/amicus-indiehacker-interview.html b/blog/2023/02/14/amicus-indiehacker-interview.html index 84cae5c59e..219f086b8c 100644 --- a/blog/2023/02/14/amicus-indiehacker-interview.html +++ b/blog/2023/02/14/amicus-indiehacker-interview.html @@ -19,14 +19,14 @@ - - + +
-

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

· 8 min read
Vinny

I guess it was less me having an idea and validating it, and more a valid idea coming to me and biting me in the ass, and me thinking ‘oh hey…’ +

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

· 8 min read
Vinny

I guess it was less me having an idea and validating it, and more a valid idea coming to me and biting me in the ass, and me thinking ‘oh hey…’  Erlis Kllogjri


Erlis Kllogjri, a computer engineer and the creator of Amicus.work, went from idea to paying customers in just one week 🤯! In this interview, he tells how sometimes the best ideas come looking for you, and how moving quickly can help you stay inspired, motivated, and pull in your first satisfied customers.


Amicus Homepage


Before we begin with the unlikely origin story of Amicus.work, can you tell us a bit about what it is?

Amicus is a SaaS tool for legal teams that helps keep you organized and on top of your legal needs. Think of it like "Asana for lawyers", but with features and workflows tailored to the domain of law.

It allows attorneys and their clients to easily track the progress of the legal case they are dealing with, and collaborate with others involved in the case, all in one central location. For example, deadline reminders help with not missing key dates and workflow visualization allows lawyer and client to see where the process is stuck, and get it unstuck.

Your time from initial idea to working MVP seemed fast. How long was it and how did you achieve it so quickly?

From the initial discussions to the launch of the initial prototype was probably a week or so. This is even quicker than it sounds because I was working a full time job at the time. The speed [of execution] was fully enabled by Wasp, a full-stack web app framework.

I was looking at other solutions, but none of them were full-stack and sounded like a lot of work just to stitch everything together and get started. I just wanted to get the job done and didn’t care about picking the stack specifics myself. Wasp was really helpful as it set me up with the best practices and I had everything running in just a few minutes!

How were you able to get these first customers so quickly?

The first user is a little bit of a cheat because I know them — my brother, who is a lawyer. But having read about other entrepreneurs, this is not that uncommon. Sometimes the first users we know are ourselves, sometimes they’re family or friends, and sometimes it’s someone you sought out. But I think it was important to have the client before the idea, because that way you have the problem before the solution.

What advice would you give to other Solopreneurs regarding the validation process?

With regard to process, I spent a lot of time having discussions with my first user - my brother. The better you know the first user, the more careful you need to be I think. They’re going to give you slack and support your ideas. You don’t really want that, so you have to dive deeper into each problem/solution - like asking 5 why’s, so you can be more objective.

Once more users came on, I began sending out surveys about the key things I wanted to know. I also started setting up SQL queries and adding logs to answer questions about what kind of user was using what features the most etc. Being a solopreneur means you have to be even more careful about what you spend your time building.

MRR is low at the moment, around ~$90, and the first goal is to get to an MRR around ~$2,000. At that point I would be able to throw more time and resources at the application, increase the utility, and kick off a virtuous cycle of more revenue and utility.

That’s great. So rather than trying to find a clever idea, the idea found you.

It’s funny because I have all of these harebrained ideas that I’m always kicking around, thinking about how to validate them: MVPs, setting up a landing page that gets emails or deposits, etc.

Meanwhile my brother was telling me about this pain of managing matters that no tool really helped with. Clients want to know where the process is, how many steps are left, how they need to be reminded of important dates like contract deadlines, etc. So I agreed to build something to see if it would help. Wasp was instrumental here because if these steps had taken too long I would have probably lost interest and gotten distracted by something else. It allowed me to abstract all the details of a full stack app and focus on the product itself.

I built the prototype and it was TERRIBLE, it hurts to think back on that first version. But it was being used, and terrible though it was, it was still providing utility. And that was the point where it clicked the idea would work - if my first crude attempt was useful, and it would only get better with each iteration, there is a space here to provide so much value that some of it can be captured.

I guess it was less me having an idea and validating it, and more a valid idea coming to me and biting me in the ass, and me thinking ‘oh hey…’.

What’s been the biggest lessons learned as a result from building Amicus? If you could do it over, what would you do the same and what would you do differently?

I think one of the things I would do differently is spend a little more time at the beginning getting a full grasp on the use cases. I tried doing this with interviews with the first client. However once what was intended was built, I come across all of these questions that weren’t initially obvious. I have seen PMs in the past create paper mockups (or using Figma if there is time) and walking a person through what they would do - then all of a sudden these assumptions you both had bubble up. [I] would probably do something like that if possible.

What were your biggest concerns before getting started building Amicus? What problems did you know you wanted to avoid and how did you successfully achieve those goals?

[My] biggest concern when getting started building Amicus was honestly that it would go to the unfinished project graveyard. Once again, Wasp was key to resolving this. Being able to remove most of the redundancy involved in making a full stack app really helped me. It allowed me to focus on the interesting problems.

One of the things I have been trying to be careful to avoid is building things that aren’t needed or solving problems that don’t exist. It is very easy to get into the trap of thinking ‘oh this would be cool’ or ‘oh this extra thing might need to be build incase…’. I have been trying to be rigorous about validating features before building them (by talking to users or through the surveys), and unless theres a good reason to believe something is a problem I don’t spend my time fixing it. This is very hard, but it has allowed me to focus.


Wasp Logo

Have you done any form of advertising? press releases? How are you spreading the word about Amicus at the moment?

No advertising yet and no press releases either. Right now spreading of the word is mostly through word of mouth. Advertising can be a money pit, especially when you don’t know what you’re doing (and I probably don’t know what I am doing) so I want to first make sure I am at the point where users feel passionate enough about Amicus to where they tell others about it. Once I get there, advertising can have a bigger return even with my fumbling.

What made you decide to go it alone as a “Solopreneur”? Were you confident that you’d be able to tackle the challenge alone, and if so why?

This wasn’t so much a decision as something that came about one decision at a time. What initially started as just a handy app for my brother to use, naturally grew in scope and utility, and all of a sudden there was a business and I effectively became a solopreneur. Although I’ve always wanted to be an entrepreneur, I didn’t realize I had become a solopreneur until after the fact.


Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/02/21/junior-developer-misconceptions.html b/blog/2023/02/21/junior-developer-misconceptions.html index f4d0901959..4a89479a9c 100644 --- a/blog/2023/02/21/junior-developer-misconceptions.html +++ b/blog/2023/02/21/junior-developer-misconceptions.html @@ -19,15 +19,15 @@ - - + +
-

The Most Common Misconceptions Amongst Junior Developers

· 6 min read
Vinny

High code quality only indirectly affects users. The main purpose is to keep development velocity high which benefits all stakeholders +

The Most Common Misconceptions Amongst Junior Developers

· 6 min read
Vinny

High code quality only indirectly affects users. The main purpose is to keep development velocity high which benefits all stakeholders  zoechi


We recently asked the web dev community on Reddit.com what the most common misconceptions are amongst junior developers, and we got a ton of great responses -- more than 270 to be exact.

Because there was so much to discuss, Matija and I decided to summarize the replies and give our own opinions in a longer-form YouTube video, which you can watch below.

You can also continue reading further for a summary of the main concepts.

The Most Common Themes

Among the responses were lots of great, specific examples, but we noticed a lot of common themes within them:

  • Code Quality
  • Managing Time & Expectations
  • Effective Communication & Teamwork

These seemed to be the topics senior devs had the most to say about. And it makes sense -- these are the things that, when you get to the core of the issues, can make or break almost any career.

It was also interesting to see that the top replies were issues that encompassed all of these themes. For example, take the top-voted reply:

Clean it up later
The most common misconception is that you're going to come back and clean that up later.

First Quality & Then Velocity

The top reply above touches on all three of the common themes we outlined, because within it is a message about quality -- about doing things correctly. And whenever you speak about quality, there is an inherent assumption that it takes longer, so we're also talking about time management. And, if you're a part of a team, you can't work effectively without good communication and teamwork.

Nevertheless, in the "quality" debate there were effectively two camps, with those who thought quality code was about:

  1. writing clean, readable code, that's easy to maintain
  2. writing code that gets shipped on time and works.

The balance between meeting deadlines, shipping features, and writing the best possible code is obviously a tricky one to get right. Some people had the opinion that business realities trump clean code patterns in the dash to meet deadlines and keep clients happy, while others thought that clean, quality code should be the priority, and that by making it a priority you can actually increase long-term velocity, even if short-term deadlines aren't met.

You don't have to touch all the code you see

This discussion can distract from Junior developers priorities though, which are to grow and improve as a developer, not lead the team to success. Therefore, it's probably best for Junior devs to focus on quality first, and then improve their speed of delivery second.

Stay Humble & Manage Expectations

As a Junior developer, it's not expected that you're going to get everything right the first time. There is an assumption that you will learn the best practices over time, and along the way you might produce inconsistent work, make mistakes, or even possibly break some things along the way.

But that's okay.

It's part of the process. It's expected. And it's important to remember that this is not a reflection of your value or worth as an engineer or individual.

In the replies, there were also many developers who recognized another developer's desire "to fix things later" as a way to brush off criticism towards their work. They generally viewed this as a bad habit to get into, as it is often one that plagues developers even as they gain more experience. "Code reviews are not personal", and being able to take criticism graciously is an important skill to develop. After all, seniors are there to guide you towards making better decisions based on their own experiences. And juniors are there to learn.

The senior dev doesn't know everything

But how often should you seek a Senior's advice? Should you do what they said, or what some dude told you is the only way to do x on YouTube or in some blogpost ;) ?

Should you ask for help every time you get stuck, or should you compromise your sanity and struggle alone for days?

Well, it depends on who you ask. But most of the replies made it clear that:

  1. You should try it out yourself first.
  2. Use the resources available to you (Google, Stack Overflow, GPT) to try and figure it out.
  3. Ask for help once you considerably slow down on making any progress.
  4. If you have a possible solution and it differs from the senior dev's suggestion, that doesn't mean it's wrong -- there can sometimes be many possible ways to achieve the same goal!

Bothering seniors with questions

Be Flexibile & Open to Change

Nothing changes faster than the world of technology. As a developer, you need to constantly be learning and adapting to new technologies and trends. If you don't like change, well then being a software developer probably isn't the right career for you.

Everything takes longer than you think

On top of things changing constantly, it's the kind of job that challenges your assumptions. What you think might be the best solution turns out to be incompatible with your team's desired goals or end product, and you're forced to use a "sub-optimal" solution instead. Why? Because it's the best way to get the job done given your team's constraints. "Sorry, pal, but we can't use your favorite framework on this one."

The developers who stay flexible and open-minded are often at an advantage here. They're the ones that are less dogmatic about a particular technology or approach, and are more willing to adapt to the situation at hand. They're typically the ones that progress faster than their peers, and they're the ones that get the job done well.


Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/03/02/wasp-beta-update-feb.html b/blog/2023/03/02/wasp-beta-update-feb.html index eb15146b9e..35431b77dc 100644 --- a/blog/2023/03/02/wasp-beta-update-feb.html +++ b/blog/2023/03/02/wasp-beta-update-feb.html @@ -19,14 +19,14 @@ - - + +
-

Wasp Beta - February 2023

· 6 min read
Matija Sosic

Wasp Update Feb 23

Want to stay in the loop? → Join our newsletter!

Hey Wasp acolytes (Waspolytes?) 🐝,

What's kickin'? We at Wasp spent the whole month thinking of the coolest features to add to our next release and we can't wait to share it with you!

Tell me now
Ok ok, we're getting there, chill!

Let me cut to the chase and show you what's been cooking in Wasp pot for the past month:

Deploy to Fly.io with a single command for free 🚀☁️

Deploy to fly.io with single command

This is the only command you need to run to deploy your full app (client, server, and database) to Fly.io! They also offer a generous free tier so you can deploy your v1 without any second thoughts.

Check out our docs for more details: Deploying your Wasp app to Fly.io

✅ Full stack TypeScript support

Types everywhere

This is one of the features we are most excited about! Now, when you define an entity in your Wasp file, it immediately becomes accessible as a type both on a client and a server.

Full stack TypeScript support

This feature beautifully showcases the power of the Wasp language approach and how much it can cut down on the boilerplate. And we're just getting started!

For more details, check out our docs on reusing entity types on both a client and a server.

🗓 We set a date for the next launch - April 11th! 🚀

Launch party

Mark your calendars, it's official! We will release the next version of Wasp on April 11th - in exactly 40 days! As the last time, we will follow a launch week format with a lot of memes, swag and fun prizes (Including Da Boi, of course).

Here's a quick list of the planned features:

  • Using Vite instead of CRA under the hood - you'll be able to create new Wasp apps in a blink of an eye! 🚀
  • Custom API routes
  • Code scaffolding for the quicker start
  • Support for sending emails
  • Password reset via email
  • Improved Auth UI
  • Testing support

And more! This is quite an ambitious plan but we are fully committed to getting it done. Any comments or ideas, ping us on our Discord.

☎️ We had our Community Call #2 - meet Da Boi

We had a community call

We had so much fun on our last community call that we decided we have to do it again! As you can notice, our community-approved mascot Da Boi stole the show. The rest was pretty much just a filler and an excuse to have more fun with Da Boi :D.

On a serious note, it was great to catch up with the community prior to the next release - we discussed features and the roadmap and everybody shared what they're building and what they'd like to see next in Wasp.

🎥 Wasp is now on YouTube!

Wasp is on YouTube

We are still going strong with our YouTube! The latest video started as a question on Reddit and it escalated quite quickly, with 200+ comments - we cover the responses we received + our expert commentary :D.

If you want to stay in the loop (and I guess you do since you're reading this :D), please subscribe to our channel and help us reach the first 100 subscribers on YouTube!

Subscribe to Wasp on YouTube
You know you want it!

⌨️ From the blog

🕹 Community highlights

  • PhraseTutor: Learn Italian in a week! There is a new app built from scratch with Wasp, by Mihovil - one of our early community members who recently joined the team as an engineer! It's smooth both on the front end and back end and will teach you Italian before you can say (or eat) "quattro formaggi"!

    Phrase Tutor

Developer life 💻⌨️💽

Here is the cool stuff we came across this month

Wasp Github Star Growth - 2,317 ⭐️, woohoo!

Huge thanks to all our contributors and stargazers - you are amazing!

Wasp has over 2,000 GitHub stars

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

That's a wrap! Thanks for reading and we can't wait for our next launch to get out and see how you like it. As always, we're on Discord and appreciate any comments, feedback, and ideas - that's how Wasp came to be!

As a parting gift, here are a few curated Da Boi memes created by our valued community members:

Wasp's new mascot

Buzzy buzz, you got that snazz 🐝 🐝,
+

Wasp Beta - February 2023

· 6 min read
Matija Sosic

Wasp Update Feb 23

Want to stay in the loop? → Join our newsletter!

Hey Wasp acolytes (Waspolytes?) 🐝,

What's kickin'? We at Wasp spent the whole month thinking of the coolest features to add to our next release and we can't wait to share it with you!

Tell me now
Ok ok, we're getting there, chill!

Let me cut to the chase and show you what's been cooking in Wasp pot for the past month:

Deploy to Fly.io with a single command for free 🚀☁️

Deploy to fly.io with single command

This is the only command you need to run to deploy your full app (client, server, and database) to Fly.io! They also offer a generous free tier so you can deploy your v1 without any second thoughts.

Check out our docs for more details: Deploying your Wasp app to Fly.io

✅ Full stack TypeScript support

Types everywhere

This is one of the features we are most excited about! Now, when you define an entity in your Wasp file, it immediately becomes accessible as a type both on a client and a server.

Full stack TypeScript support

This feature beautifully showcases the power of the Wasp language approach and how much it can cut down on the boilerplate. And we're just getting started!

For more details, check out our docs on reusing entity types on both a client and a server.

🗓 We set a date for the next launch - April 11th! 🚀

Launch party

Mark your calendars, it's official! We will release the next version of Wasp on April 11th - in exactly 40 days! As the last time, we will follow a launch week format with a lot of memes, swag and fun prizes (Including Da Boi, of course).

Here's a quick list of the planned features:

  • Using Vite instead of CRA under the hood - you'll be able to create new Wasp apps in a blink of an eye! 🚀
  • Custom API routes
  • Code scaffolding for the quicker start
  • Support for sending emails
  • Password reset via email
  • Improved Auth UI
  • Testing support

And more! This is quite an ambitious plan but we are fully committed to getting it done. Any comments or ideas, ping us on our Discord.

☎️ We had our Community Call #2 - meet Da Boi

We had a community call

We had so much fun on our last community call that we decided we have to do it again! As you can notice, our community-approved mascot Da Boi stole the show. The rest was pretty much just a filler and an excuse to have more fun with Da Boi :D.

On a serious note, it was great to catch up with the community prior to the next release - we discussed features and the roadmap and everybody shared what they're building and what they'd like to see next in Wasp.

🎥 Wasp is now on YouTube!

Wasp is on YouTube

We are still going strong with our YouTube! The latest video started as a question on Reddit and it escalated quite quickly, with 200+ comments - we cover the responses we received + our expert commentary :D.

If you want to stay in the loop (and I guess you do since you're reading this :D), please subscribe to our channel and help us reach the first 100 subscribers on YouTube!

Subscribe to Wasp on YouTube
You know you want it!

⌨️ From the blog

🕹 Community highlights

  • PhraseTutor: Learn Italian in a week! There is a new app built from scratch with Wasp, by Mihovil - one of our early community members who recently joined the team as an engineer! It's smooth both on the front end and back end and will teach you Italian before you can say (or eat) "quattro formaggi"!

    Phrase Tutor

Developer life 💻⌨️💽

Here is the cool stuff we came across this month

Wasp Github Star Growth - 2,317 ⭐️, woohoo!

Huge thanks to all our contributors and stargazers - you are amazing!

Wasp has over 2,000 GitHub stars

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

That's a wrap! Thanks for reading and we can't wait for our next launch to get out and see how you like it. As always, we're on Discord and appreciate any comments, feedback, and ideas - that's how Wasp came to be!

As a parting gift, here are a few curated Da Boi memes created by our valued community members:

Wasp's new mascot

Buzzy buzz, you got that snazz 🐝 🐝,
Matija, Martin and the Wasp team

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hear.html b/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hear.html index c6c2ae9c86..657caf29a3 100644 --- a/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hear.html +++ b/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hear.html @@ -19,13 +19,13 @@ - - + +
-

10 "Hard Truths" All Junior Developers Need to Hear

· 4 min read
Vinny

hard truths for junior devs

Ok, I have to admit, these aren’t really Truths, but rather some opinions I’ve formed over my journey switching careers from Educator to Developer.

It’s well known at this point that software — especially web — development is a viable option for someone looking for a new career without going the traditional education route. Due to this, and the fact that salaries tend to be very good, I think a portion of people making the switch might be doing it for the wrong reasons.

And once you get into that career, as a Junior it can often be difficult to know what you should be doing to advance your career. There are a ton of opinions out there (including mine) and juniors tend to develop a lot of misconceptions, as my colleague and I discussed in our recent Reddit post and follow-up video.

So, I put together this list of things you should consider when starting out a career in tech:

  1. 👎 If you’re doing it solely for the money, you’re not gonna make it. True, you don’t need a degree or anyone’s permission to advance in this career, but you need ambition and mental stamina. A genuine interest is needed to maintain them.

  2. 😎 You don’t have to follow the trends. Follow what interests you. Like I said before, you need mental stamina in this field of work. Following your interests will keep you engaged and help avoid burnout.

  3. 👩‍💻 You don’t need to know a piece of tech inside and out, contrary to what some devs might want you to believe. The truth is, you are always learning, and there will always be gaps in your knowledge. Your confidence in being able to fill those gaps is what matters.

  4. 🧱 Start building, ASAP. Find a problem that interests you and build the solution yourself. Contribute to Open-Source projects that you use. A portfolio of unique work speaks volumes about your abilities. Plus, there’s no better teacher than experience.

  5. 😱 Be fearless and seek feedback. Put your work out there and be ready to have it criticized. If you can stomach it, you’ll come out the other side a much better developer.

  6. 🧐 You should have a firm understanding of what you’re doing. Don’t copy-paste someone else’s answer (or GPT’s) to your problem and call it a day. Question why things work, and figure it out for yourself.

  7. 🏋️‍♀️ You have to do the grunt work, unfortunately. Don’t expect high salaries from the beginning. And you’ll probably want to improve your portfolio by working on side projects in your free time, or you might stay a junior dev for longer than you wish.

  8. 🧗‍♂️ Challenge yourself. Don’t let yourself get too comfortable. If you do, you won’t improve. Offer to take new, difficult, and daunting tasks at work or with your personal projects. You’ll be surprised what you can achieve.

  9. 💰 You don’t have to pay for boot camps or courses. In fact, you’re better off tackling problems on your own and only asking for help if you’re truly stuck. There’s a wealth of free resources out there, and when you’re on the job, these might be the only things to assist you.

  10. 🗣 Programming is definitely not the only skill you’ll need. Being respectful, communicative, conscientious, ambitious, and humble will put you in a different league and make you a valuable asset in any tech team.

TIP: Looking for some inspiration? Feedback? Motivation? Join us over at the Wasp Discord server, where we've got an active, friendly community of web developers of all skill levels that build side-projects, share their experiences, make memes, and chat about life



Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

10 "Hard Truths" All Junior Developers Need to Hear

· 4 min read
Vinny

hard truths for junior devs

Ok, I have to admit, these aren’t really Truths, but rather some opinions I’ve formed over my journey switching careers from Educator to Developer.

It’s well known at this point that software — especially web — development is a viable option for someone looking for a new career without going the traditional education route. Due to this, and the fact that salaries tend to be very good, I think a portion of people making the switch might be doing it for the wrong reasons.

And once you get into that career, as a Junior it can often be difficult to know what you should be doing to advance your career. There are a ton of opinions out there (including mine) and juniors tend to develop a lot of misconceptions, as my colleague and I discussed in our recent Reddit post and follow-up video.

So, I put together this list of things you should consider when starting out a career in tech:

  1. 👎 If you’re doing it solely for the money, you’re not gonna make it. True, you don’t need a degree or anyone’s permission to advance in this career, but you need ambition and mental stamina. A genuine interest is needed to maintain them.

  2. 😎 You don’t have to follow the trends. Follow what interests you. Like I said before, you need mental stamina in this field of work. Following your interests will keep you engaged and help avoid burnout.

  3. 👩‍💻 You don’t need to know a piece of tech inside and out, contrary to what some devs might want you to believe. The truth is, you are always learning, and there will always be gaps in your knowledge. Your confidence in being able to fill those gaps is what matters.

  4. 🧱 Start building, ASAP. Find a problem that interests you and build the solution yourself. Contribute to Open-Source projects that you use. A portfolio of unique work speaks volumes about your abilities. Plus, there’s no better teacher than experience.

  5. 😱 Be fearless and seek feedback. Put your work out there and be ready to have it criticized. If you can stomach it, you’ll come out the other side a much better developer.

  6. 🧐 You should have a firm understanding of what you’re doing. Don’t copy-paste someone else’s answer (or GPT’s) to your problem and call it a day. Question why things work, and figure it out for yourself.

  7. 🏋️‍♀️ You have to do the grunt work, unfortunately. Don’t expect high salaries from the beginning. And you’ll probably want to improve your portfolio by working on side projects in your free time, or you might stay a junior dev for longer than you wish.

  8. 🧗‍♂️ Challenge yourself. Don’t let yourself get too comfortable. If you do, you won’t improve. Offer to take new, difficult, and daunting tasks at work or with your personal projects. You’ll be surprised what you can achieve.

  9. 💰 You don’t have to pay for boot camps or courses. In fact, you’re better off tackling problems on your own and only asking for help if you’re truly stuck. There’s a wealth of free resources out there, and when you’re on the job, these might be the only things to assist you.

  10. 🗣 Programming is definitely not the only skill you’ll need. Being respectful, communicative, conscientious, ambitious, and humble will put you in a different league and make you a valuable asset in any tech team.

TIP: Looking for some inspiration? Feedback? Motivation? Join us over at the Wasp Discord server, where we've got an active, friendly community of web developers of all skill levels that build side-projects, share their experiences, make memes, and chat about life



Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/03/08/building-a-full-stack-app-supabase-vs-wasp.html b/blog/2023/03/08/building-a-full-stack-app-supabase-vs-wasp.html index 26f89d9fb9..e71f7d6639 100644 --- a/blog/2023/03/08/building-a-full-stack-app-supabase-vs-wasp.html +++ b/blog/2023/03/08/building-a-full-stack-app-supabase-vs-wasp.html @@ -19,13 +19,13 @@ - - + +
-

Building a full-stack app for learning Italian: Supabase vs. Wasp

· 14 min read
Mihovil Ilakovac

wasp vs. supabase

Intro

What to expect

In this blog post, I will explain how I created the Phrase Tutor app for learning Italian phrases using two different technologies. I will share some code snippets to show what was required to build the app with both Wasp and Supabase.

Phrase Tutor’s front-end
Phrase Tutor’s front-end

As a senior full-stack developer with experience in building many side-projects, I prefer a quick development cycle. I enjoy turning ideas into POCs in just a few days or even hours.

We will examine how each technology can help when building a full-stack app and where Wasp and Supabase excel.

I wanted to learn Italian fast

Whenever I travel abroad, I enjoy imagining what it would be like to live in that place. For instance, I usually don't like taking crowded public transportation, but for some reason, it brings me joy when I do it in a foreign country. It's all about the feeling that I'm living there. One of the most important things for me to fully experience the culture is to learn the language or, at the very least, be able to not speak English all the time.

Pretending to be Italian
Pretending to be Italian

My girlfriend and I were planning a trip to Italy, and I wanted to learn some Italian. I thought about what would be the easiest way to learn as much as possible with the least amount of effort. I decided that learning the top 100 Italian phrases would be a good start. I had a week to do it, and learning 100 phrases seemed doable if I practiced every day.

The learning method

In high school, I had a system for learning historical facts and dates quickly called "focusing on things you don’t know".

Here's how it works:

  1. Gather a pool of facts you want to learn (e.g. "When did WWI start?" - "1914").
  2. Ask yourself each question in the pool.
  3. If you know the answer, remove the fact from the pool.
  4. If you don't know the answer, keep it in the pool.
  5. Repeat with the smaller pool until there are no more facts left.

I made a small app for this and shared it with my classmates, but it didn't go further than that.

Now, I want to use the same method to learn Italian phrases for my trip. So, as a better developer now, I'll make a proper app and host it somewhere 🙂

Building the Phrase Tutor app

We will create an app that follows the method described above. The app will show you a phrase and you can tell it if you know the translation or not by selecting "I knew it" or "I didn't know it".

How the learning in the app should work
How the learning in the app should work

The app will keep track of your answers and suggest which phrases you should learn next 🕵️

I’ve built the app twice: first with Supabase and then with Wasp. Supabase is a well-rounded open-source Backend as a Service (BaaS) product that adds superpowers to your front-end apps. On the other hand, Wasp is an open-source framework for building full-stack apps that helps to keep the boilerplate low. Let’s see how they compare.

Initial Supabase version

When I made the initial version, I worked heavily with Vue.js, which I used to create the first version of the Phrase Tutor app. I started by collecting some phrases. I searched on Google for "best Italian phrases to learn" and came across an article titled "100 Italian phrases to learn." (After extracting the phrases from the HTML, I found out that there were only 96 phrases, but that was still good enough for me.)

The initial app contained the phrases in a JSON file that the frontend loaded. It was completely static, but it worked.

{
"id": 1,
"group": "general",
"translations": {
"en": "Yes",
"it": "Si"
}
}

I put it on Cloudflare Pages and it went live.

I showed it to my girlfriend, but she didn't like some of the phrases I used. If only I had a backend with a database to edit the phrases. Then I had an idea: let's add a database with Supabase.

Supabase is a managed backend solution that provides a lot of free stuff: a PostgreSQL database and social authentication among other things.

Phrase Tutor built with Supabase
Phrase Tutor built with Supabase

I set up the database tables using the Supabase UI which was pretty straightforward.

The table I needed only had a few fields:

CREATE TABLE phrases (
id bigint NOT NULL,
group character varying NULL,
translations_en text NOT NULL,
translations_it text NOT NULL
);

Then I had to seed the database with some SQL. Executing SQL statements is easy with the use of Supabase’s UI. You just log in, open the SQL editor and paste in the code:

INSERT INTO phrases(id,"group",translations_en,translations_it) VALUES (1,'general','Yes','Si');
INSERT INTO phrases(id,"group",translations_en,translations_it) VALUES (2,'general','No','No');
...

Integrating Supabase into my existing front-end app was simple using their Javascript SDK. If you're familiar with Firebase, it should feel similar. Essentially, you build your SQL queries on the frontend and use the resulting data in your app.

Using the SDK felt pretty straightforward and I could get what I wanted out of the database without much hassle.

const { data, error } = await supabase.from("phrases").select("*");

And just like that, my static Vue.js app had a database to rely on 🎉

Adding the login with Google was a matter of enabling it in Supabase UI and setting up the Client ID and Client Secret variables. In order to trigger the login process with Google, I once again relied on their Javascript SDK.

supabase.auth.signInWithOAuth({ provider: "google" });

Awesome! I'm glad that I can now edit the phrases and that there is a login feature that I plan to use later.

In the future, I have plans to add more languages to the app and also allow registered users to contribute new phrases and translations. I believe this will make the app more useful and engaging for language learners.

And just like that, my app went from a pure static app to an app with a database and Google login 🤯

info

Check out the deployed app written with Vue.js and Supabase: https://phrase-tutor.pages.dev

info

View the source here

Joining Wasp and dogfooding it

Some background before the second part: I started working at Wasp earlier this year. I'm really happy to work on a technology that solves a problem I care about: when I do side-projects, I dislike writing the same dull parts every time from scratch. I copy and paste from my previous side projects, but eventually, the code snippets become old and outdated.

Naturally, I wanted to test out Wasp by rewriting one of my side projects. I decided to see how Wasp could work with the Phrase Tutor project.

Wasp works by having an easy-to-understand config file called main.wasp which coordinates your pieces of client and server functionalities. Its main purpose is to keep you productive and focused on writing interesting bits. It feels pretty much like using a web framework that covers your whole app.

Phrase Tutor built with Wasp
Phrase Tutor built with Wasp

Let's begin by creating the data models. Wasp uses Prisma under the hood to communicate with your database, which makes it easy to manage your database without worrying about the details. This is just one of the many choices the framework made for me, and I appreciate the feeling of using a setup that works.

I had to first declare all of the entities I needed with Prisma PSL in the Wasp config file.

entity Phrase {=psl
id Int @id @default(autoincrement())
group String
phrase String
translations Translation[]
psl=}

entity Language {=psl
id Int @id @default(autoincrement())
name String @unique
emoji String
translations Translation[]
psl=}

entity Translation {=psl
id Int @id @default(autoincrement())
phraseId Int
languageId Int
translation String
phrase Phrase @relation(fields: [phraseId], references: [id], onDelete: Cascade)
language Language @relation(fields: [languageId], references: [id], onDelete: Cascade)
psl=}

I'm using a PostgreSQL database again, and you can see that the field definitions are similar.

I improved the data schema a bit by defining three tables instead of one. I separated the concept of a Phrase from the concepts of Language and Translation. This will make it easier to add new languages in the future.

I added some phrases to the database using Prisma and a Wasp action:

export async function seedItalianPhrases(args, context) {
const data = [
{
id: 1,
group: "general",
translations_en: "Yes",
translations_it: "Si"
},
...
]
for (const phrase of seedPhrases) {
await context.entities.Phrase.create({
...
});
}
}

Let’s now look at what I needed to do to get the data flowing from the backend to my React app.

First, I declared a query in my Wasp config file:

app phraseTutor {
...
}
...

query fetchAllPhrases {
fn: import { getAllPhrases } from "@server/queries.js",
entities: [Phrase]
}

Then I wrote the code for my backend to fetch the phrases. You’ll notice it’s quite similar to the code I wrote for fetching phrases with the Supabase SDK, but I had to include the translations relation since we now have multiple tables.

// My query got the Prisma entity through the context parameter
// which I just used to fetch all the phrases
export async function getAllPhrases(args, context) {
return context.entities.Phrase.findMany({
include: {
translations: true
}
});
}

And lastly, I could just import the query into my React app. It’s set up in a way that it handles cache invalidation automatically, one less thing to worry about, which is awesome 😎

// Wasp relies on React Query in the background
const { data: phrases, isLoading } = useQuery(fetchAllPhrases);

Let’s also add support for Google auth for our app. It involves declaring you want it in the Wasp file, adding some env variables and using it in the React application.

We declare it to the Wasp file by adding the google key under auth:

app phraseTutor {
...
auth: {
userEntity: User,
externalAuthEntity: SocialUser,
methods: {
// Define we want the Google auth
google: {
// Optionally, we can adjust what is saved from the user's data
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
}
},
onAuthFailedRedirectTo: "/"
},
...
}

// Some of the entities needed for auth
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
profilePicture String
externalAuthAssociations SocialUser[]
createdAt DateTime @default(now())
psl=}

entity SocialUser {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

And … that’s it. We can now use the Google auth in our frontend 🎉

import { signInUrl as googleSignInUrl } from "@wasp/auth/helpers/Google";
...
const { data: user } = useAuth();

Writing a full-stack React and Express.js with Wasp felt like a guided experience; I didn't have to focus too hard on the dev tooling, building, or deploying.

Instead, I could focus on the logic needed for Phrase Tutor to work and just run wasp start most of the time. I did need to write some extra code to get everything running, but I'm free to customize this code however I want.

info

Check out the deployed project built with Wasp: https://phrasetutor.com

info

View the source here

Let's compare some of the features

I want to compare the features of Supabase and Wasp. It's good to think about different ways to do things and their pros and cons.

FeatureSupabaseWasp
Getting data from the APIUse the Supabase JS SDK to query database tablesDeclare query in Wasp config and use Prisma JS SDK to implement it
Custom business logicWriting custom PostgreSQL procedures or by writing edge functionsDeclare actions in the Wasp file and write server-side JS
Defining the database schemaVisual editor or by CREATE TABLE queryBy code - edit Prisma schema and commit changes
AuthEnable in UIEnable it in the Wasp file
DeploymentSupabase managed instance or self-host itDeploy anywhere, support for https://fly.io one line deployment

With Supabase, I liked how familiar the SDK felt and their UI made it easy to configure parts of my backend. I didn’t need to think about deploying Supabase since I used their hosted version, but it did get paused after 1 week of inactivity on the free tier.

On the other hand, Wasp felt like the glue for my React + Express.js + Prisma app and I needed to write more code to get things done. It felt more explicit because I wrote code closer to what I would normally write. I deployed it to fly.io with the Wasp command wasp deploy fly launch and it’s now live on https://phrasetutor.com

Conclusion

It's all about the use case

Choosing the right solution for your needs can be difficult. That's why it's important to try out different options and see how they work for you. In this case, I compared two options: Supabase and Wasp.

Supabase is a great choice if you want a well-rounded open-source BaaS product that adds superpowers to your front-end apps. It provides a lot of free stuff, such as a PostgreSQL database and social authentication, which can make development easier and faster. It also has a nice SDK and UI that the end user can use to easily define their app's configuration.

Wasp is an open-source framework for building full-stack apps that helps out with keeping the boilerplate low. It is a bit more explicit about some things, such as defining your auth entities, but that can be a plus when you have more advanced use cases. By using Wasp as the glue for your full-stack application, you can have the best of both worlds: a development and production setup that works out of the box while still allowing you to develop your app any way you like.

In the case of Phrase Tutor, I liked working with both Supabase and Wasp. I did, however, get a different feeling from working with the two technologies. With Supabase I felt like my front-end app got instant superpowers and it now has a database and login, which was nice considering the effort I had to put in. But now I had a black-box dependency that I needed to build around.

When I used Wasp to rebuild Phrase Tutor, it felt different because it was a full-stack app. I had more control over the application code, so I could change it and evolve it as I wanted. I felt like I had built an app that could grow in any direction. Although I had to write more code, it felt like a good trade-off for future needs.

To decide which option is best for you, I would suggest trying both and seeing how you feel. It is easy to set up both tools and see if they make sense for you.

Grazie for reading 🙃
Grazie for reading 🙃

If you try out the Phrase Tutor app, please let me know what you think. You can reach me on Twitter. I'm always looking for ways to make it better.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Building a full-stack app for learning Italian: Supabase vs. Wasp

· 14 min read
Mihovil Ilakovac

wasp vs. supabase

Intro

What to expect

In this blog post, I will explain how I created the Phrase Tutor app for learning Italian phrases using two different technologies. I will share some code snippets to show what was required to build the app with both Wasp and Supabase.

Phrase Tutor’s front-end
Phrase Tutor’s front-end

As a senior full-stack developer with experience in building many side-projects, I prefer a quick development cycle. I enjoy turning ideas into POCs in just a few days or even hours.

We will examine how each technology can help when building a full-stack app and where Wasp and Supabase excel.

I wanted to learn Italian fast

Whenever I travel abroad, I enjoy imagining what it would be like to live in that place. For instance, I usually don't like taking crowded public transportation, but for some reason, it brings me joy when I do it in a foreign country. It's all about the feeling that I'm living there. One of the most important things for me to fully experience the culture is to learn the language or, at the very least, be able to not speak English all the time.

Pretending to be Italian
Pretending to be Italian

My girlfriend and I were planning a trip to Italy, and I wanted to learn some Italian. I thought about what would be the easiest way to learn as much as possible with the least amount of effort. I decided that learning the top 100 Italian phrases would be a good start. I had a week to do it, and learning 100 phrases seemed doable if I practiced every day.

The learning method

In high school, I had a system for learning historical facts and dates quickly called "focusing on things you don’t know".

Here's how it works:

  1. Gather a pool of facts you want to learn (e.g. "When did WWI start?" - "1914").
  2. Ask yourself each question in the pool.
  3. If you know the answer, remove the fact from the pool.
  4. If you don't know the answer, keep it in the pool.
  5. Repeat with the smaller pool until there are no more facts left.

I made a small app for this and shared it with my classmates, but it didn't go further than that.

Now, I want to use the same method to learn Italian phrases for my trip. So, as a better developer now, I'll make a proper app and host it somewhere 🙂

Building the Phrase Tutor app

We will create an app that follows the method described above. The app will show you a phrase and you can tell it if you know the translation or not by selecting "I knew it" or "I didn't know it".

How the learning in the app should work
How the learning in the app should work

The app will keep track of your answers and suggest which phrases you should learn next 🕵️

I’ve built the app twice: first with Supabase and then with Wasp. Supabase is a well-rounded open-source Backend as a Service (BaaS) product that adds superpowers to your front-end apps. On the other hand, Wasp is an open-source framework for building full-stack apps that helps to keep the boilerplate low. Let’s see how they compare.

Initial Supabase version

When I made the initial version, I worked heavily with Vue.js, which I used to create the first version of the Phrase Tutor app. I started by collecting some phrases. I searched on Google for "best Italian phrases to learn" and came across an article titled "100 Italian phrases to learn." (After extracting the phrases from the HTML, I found out that there were only 96 phrases, but that was still good enough for me.)

The initial app contained the phrases in a JSON file that the frontend loaded. It was completely static, but it worked.

{
"id": 1,
"group": "general",
"translations": {
"en": "Yes",
"it": "Si"
}
}

I put it on Cloudflare Pages and it went live.

I showed it to my girlfriend, but she didn't like some of the phrases I used. If only I had a backend with a database to edit the phrases. Then I had an idea: let's add a database with Supabase.

Supabase is a managed backend solution that provides a lot of free stuff: a PostgreSQL database and social authentication among other things.

Phrase Tutor built with Supabase
Phrase Tutor built with Supabase

I set up the database tables using the Supabase UI which was pretty straightforward.

The table I needed only had a few fields:

CREATE TABLE phrases (
id bigint NOT NULL,
group character varying NULL,
translations_en text NOT NULL,
translations_it text NOT NULL
);

Then I had to seed the database with some SQL. Executing SQL statements is easy with the use of Supabase’s UI. You just log in, open the SQL editor and paste in the code:

INSERT INTO phrases(id,"group",translations_en,translations_it) VALUES (1,'general','Yes','Si');
INSERT INTO phrases(id,"group",translations_en,translations_it) VALUES (2,'general','No','No');
...

Integrating Supabase into my existing front-end app was simple using their Javascript SDK. If you're familiar with Firebase, it should feel similar. Essentially, you build your SQL queries on the frontend and use the resulting data in your app.

Using the SDK felt pretty straightforward and I could get what I wanted out of the database without much hassle.

const { data, error } = await supabase.from("phrases").select("*");

And just like that, my static Vue.js app had a database to rely on 🎉

Adding the login with Google was a matter of enabling it in Supabase UI and setting up the Client ID and Client Secret variables. In order to trigger the login process with Google, I once again relied on their Javascript SDK.

supabase.auth.signInWithOAuth({ provider: "google" });

Awesome! I'm glad that I can now edit the phrases and that there is a login feature that I plan to use later.

In the future, I have plans to add more languages to the app and also allow registered users to contribute new phrases and translations. I believe this will make the app more useful and engaging for language learners.

And just like that, my app went from a pure static app to an app with a database and Google login 🤯

info

Check out the deployed app written with Vue.js and Supabase: https://phrase-tutor.pages.dev

info

View the source here

Joining Wasp and dogfooding it

Some background before the second part: I started working at Wasp earlier this year. I'm really happy to work on a technology that solves a problem I care about: when I do side-projects, I dislike writing the same dull parts every time from scratch. I copy and paste from my previous side projects, but eventually, the code snippets become old and outdated.

Naturally, I wanted to test out Wasp by rewriting one of my side projects. I decided to see how Wasp could work with the Phrase Tutor project.

Wasp works by having an easy-to-understand config file called main.wasp which coordinates your pieces of client and server functionalities. Its main purpose is to keep you productive and focused on writing interesting bits. It feels pretty much like using a web framework that covers your whole app.

Phrase Tutor built with Wasp
Phrase Tutor built with Wasp

Let's begin by creating the data models. Wasp uses Prisma under the hood to communicate with your database, which makes it easy to manage your database without worrying about the details. This is just one of the many choices the framework made for me, and I appreciate the feeling of using a setup that works.

I had to first declare all of the entities I needed with Prisma PSL in the Wasp config file.

entity Phrase {=psl
id Int @id @default(autoincrement())
group String
phrase String
translations Translation[]
psl=}

entity Language {=psl
id Int @id @default(autoincrement())
name String @unique
emoji String
translations Translation[]
psl=}

entity Translation {=psl
id Int @id @default(autoincrement())
phraseId Int
languageId Int
translation String
phrase Phrase @relation(fields: [phraseId], references: [id], onDelete: Cascade)
language Language @relation(fields: [languageId], references: [id], onDelete: Cascade)
psl=}

I'm using a PostgreSQL database again, and you can see that the field definitions are similar.

I improved the data schema a bit by defining three tables instead of one. I separated the concept of a Phrase from the concepts of Language and Translation. This will make it easier to add new languages in the future.

I added some phrases to the database using Prisma and a Wasp action:

export async function seedItalianPhrases(args, context) {
const data = [
{
id: 1,
group: "general",
translations_en: "Yes",
translations_it: "Si"
},
...
]
for (const phrase of seedPhrases) {
await context.entities.Phrase.create({
...
});
}
}

Let’s now look at what I needed to do to get the data flowing from the backend to my React app.

First, I declared a query in my Wasp config file:

app phraseTutor {
...
}
...

query fetchAllPhrases {
fn: import { getAllPhrases } from "@server/queries.js",
entities: [Phrase]
}

Then I wrote the code for my backend to fetch the phrases. You’ll notice it’s quite similar to the code I wrote for fetching phrases with the Supabase SDK, but I had to include the translations relation since we now have multiple tables.

// My query got the Prisma entity through the context parameter
// which I just used to fetch all the phrases
export async function getAllPhrases(args, context) {
return context.entities.Phrase.findMany({
include: {
translations: true
}
});
}

And lastly, I could just import the query into my React app. It’s set up in a way that it handles cache invalidation automatically, one less thing to worry about, which is awesome 😎

// Wasp relies on React Query in the background
const { data: phrases, isLoading } = useQuery(fetchAllPhrases);

Let’s also add support for Google auth for our app. It involves declaring you want it in the Wasp file, adding some env variables and using it in the React application.

We declare it to the Wasp file by adding the google key under auth:

app phraseTutor {
...
auth: {
userEntity: User,
externalAuthEntity: SocialUser,
methods: {
// Define we want the Google auth
google: {
// Optionally, we can adjust what is saved from the user's data
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
}
},
onAuthFailedRedirectTo: "/"
},
...
}

// Some of the entities needed for auth
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
profilePicture String
externalAuthAssociations SocialUser[]
createdAt DateTime @default(now())
psl=}

entity SocialUser {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

And … that’s it. We can now use the Google auth in our frontend 🎉

import { signInUrl as googleSignInUrl } from "@wasp/auth/helpers/Google";
...
const { data: user } = useAuth();

Writing a full-stack React and Express.js with Wasp felt like a guided experience; I didn't have to focus too hard on the dev tooling, building, or deploying.

Instead, I could focus on the logic needed for Phrase Tutor to work and just run wasp start most of the time. I did need to write some extra code to get everything running, but I'm free to customize this code however I want.

info

Check out the deployed project built with Wasp: https://phrasetutor.com

info

View the source here

Let's compare some of the features

I want to compare the features of Supabase and Wasp. It's good to think about different ways to do things and their pros and cons.

FeatureSupabaseWasp
Getting data from the APIUse the Supabase JS SDK to query database tablesDeclare query in Wasp config and use Prisma JS SDK to implement it
Custom business logicWriting custom PostgreSQL procedures or by writing edge functionsDeclare actions in the Wasp file and write server-side JS
Defining the database schemaVisual editor or by CREATE TABLE queryBy code - edit Prisma schema and commit changes
AuthEnable in UIEnable it in the Wasp file
DeploymentSupabase managed instance or self-host itDeploy anywhere, support for https://fly.io one line deployment

With Supabase, I liked how familiar the SDK felt and their UI made it easy to configure parts of my backend. I didn’t need to think about deploying Supabase since I used their hosted version, but it did get paused after 1 week of inactivity on the free tier.

On the other hand, Wasp felt like the glue for my React + Express.js + Prisma app and I needed to write more code to get things done. It felt more explicit because I wrote code closer to what I would normally write. I deployed it to fly.io with the Wasp command wasp deploy fly launch and it’s now live on https://phrasetutor.com

Conclusion

It's all about the use case

Choosing the right solution for your needs can be difficult. That's why it's important to try out different options and see how they work for you. In this case, I compared two options: Supabase and Wasp.

Supabase is a great choice if you want a well-rounded open-source BaaS product that adds superpowers to your front-end apps. It provides a lot of free stuff, such as a PostgreSQL database and social authentication, which can make development easier and faster. It also has a nice SDK and UI that the end user can use to easily define their app's configuration.

Wasp is an open-source framework for building full-stack apps that helps out with keeping the boilerplate low. It is a bit more explicit about some things, such as defining your auth entities, but that can be a plus when you have more advanced use cases. By using Wasp as the glue for your full-stack application, you can have the best of both worlds: a development and production setup that works out of the box while still allowing you to develop your app any way you like.

In the case of Phrase Tutor, I liked working with both Supabase and Wasp. I did, however, get a different feeling from working with the two technologies. With Supabase I felt like my front-end app got instant superpowers and it now has a database and login, which was nice considering the effort I had to put in. But now I had a black-box dependency that I needed to build around.

When I used Wasp to rebuild Phrase Tutor, it felt different because it was a full-stack app. I had more control over the application code, so I could change it and evolve it as I wanted. I felt like I had built an app that could grow in any direction. Although I had to write more code, it felt like a good trade-off for future needs.

To decide which option is best for you, I would suggest trying both and seeing how you feel. It is easy to set up both tools and see if they make sense for you.

Grazie for reading 🙃
Grazie for reading 🙃

If you try out the Phrase Tutor app, please let me know what you think. You can reach me on Twitter. I'm always looking for ways to make it better.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/03/17/new-react-docs-pretend-spas-dont-exist.html b/blog/2023/03/17/new-react-docs-pretend-spas-dont-exist.html index d473400d7c..8587cbd5fe 100644 --- a/blog/2023/03/17/new-react-docs-pretend-spas-dont-exist.html +++ b/blog/2023/03/17/new-react-docs-pretend-spas-dont-exist.html @@ -19,13 +19,13 @@ - - + +
-

New React docs pretend SPAs don't exist anymore

· 5 min read
Matija Sosic

Where is Vite

React just released their new docs at https://react.dev/. While it looks great and packs a lot of improvements, one section that caught the community’s attention is “Start a New React Project”. The strongly recommended way to start a new React project is to use a framework such as Next.js, while the traditional route of using bundlers like Vite or CRA is fairly strongly discouraged.

Next.js is a great framework, and its rise in popularity is due in a large part to the return of SEO optimization via Server-Side-Rendering (SSR) within the collective developer conscience. And it definitely does make sense to use a framework that provides SSR for static sites and pages that rely on SEO.

But what about typical Single Page Apps (SPAs)? Dashboard-like tools that live behind the auth (and don’t need SEO at all), and for which React was originally designed, still very much exist.

The new React docs - use a framework unless your app has “unusual” constraints

react new project docs

The new docs make a pretty strong claim for using a framework when starting a new React project. Even if you read through the “Can I use React without a framework” section (hidden behind a collapsed toggle by default), you have to go through a wall of text convincing you why not using a framework is a bad idea, mainly due to the lack of SSR. Only then, in the end, comes the piece mentioning other options, such as Vite and Parcel:

use framework unless you app has unusual constraints

Even then, first you’ll have to admit your app has unusual constraints (and no examples were given of what that could be) before you’re actually “allowed” not to use a framework. It feels very much like you’re doing it in spite of all the warnings and that there actually isn’t a case where you should do it.

Why SPAs (still) matter

SPAs still have their place

SSR/SSG has been getting a lot of attention lately and has been a flagship feature of most new frameworks built on top of React. And rightly so - it has solved a major issue of using React for static & SEO-facing sites where time to first content (FCP) is crucial.

On the other hand, the use case where React, Angular, and other UI frameworks initially shined were dashboard apps (e.g., project management systems, CRMs, …) - it allowed for a radically better UX, which resembled that of desktop apps.

Although interactive content-rich apps (blogging platforms, marketplaces, social platforms) are today a typical poster child demo app for frameworks, dashboard-like apps still very much exist, and there are more of them than ever. Thousands of companies are building their internal tools daily, just like new SaaS-es pop up every day.

SEO is largely irrelevant for them since everything is happening behind the auth layer, where everything is centered around workflows, not content. SSR might even be counter-productive since it puts more pressure on your servers instead of distributing the rendering load across the clients.

How then would you develop SPAs?

Traditionally, React was only a UI library in your stack of choice. You would use CRA (or Vite nowadays) as a bundler/starter for your React project. Then you’d probably add a routing library (e.g., react-router) and maybe a state management library (e.g., Redux, or react-query), and you’d already be set pretty well. You would develop your backend in whatever you choose - Node.js/Express, Rails, or anything else.

There are also new frameworks emerging that focus on this particular use case (e.g., RedwoodJS and Wasp (disclaimer: this is us!)) whose flagship feature is not SSR, but rather the abstraction of API and CRUD on data models, and getting full-stack functionality from UI to the database, with extra features such as easy authentication and deployment out of the box.

With a “go for Next or you are unusual” and “you need SSR” message, React is making a strong signal against other solutions that don’t emphasize SSR as their main feature.

So what’s the big deal? Nobody forces you to use SSR in Next/Remix

That’s correct, but also it’s true that a buy-in into a whole framework is a much bigger step than just opting for a UI library. Frameworks are (more) opinionated and come with many decisions (code structure, architecture, deployment) made upfront for you. Which is great and that’s why they are valuable and why we’ll keep using them.

But, both sides of the story should be presented, and the final call should be left to the developer. React is too useful, valuable, and popular a tool and community to allow itself to skip this step.

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

New React docs pretend SPAs don't exist anymore

· 5 min read
Matija Sosic

Where is Vite

React just released their new docs at https://react.dev/. While it looks great and packs a lot of improvements, one section that caught the community’s attention is “Start a New React Project”. The strongly recommended way to start a new React project is to use a framework such as Next.js, while the traditional route of using bundlers like Vite or CRA is fairly strongly discouraged.

Next.js is a great framework, and its rise in popularity is due in a large part to the return of SEO optimization via Server-Side-Rendering (SSR) within the collective developer conscience. And it definitely does make sense to use a framework that provides SSR for static sites and pages that rely on SEO.

But what about typical Single Page Apps (SPAs)? Dashboard-like tools that live behind the auth (and don’t need SEO at all), and for which React was originally designed, still very much exist.

The new React docs - use a framework unless your app has “unusual” constraints

react new project docs

The new docs make a pretty strong claim for using a framework when starting a new React project. Even if you read through the “Can I use React without a framework” section (hidden behind a collapsed toggle by default), you have to go through a wall of text convincing you why not using a framework is a bad idea, mainly due to the lack of SSR. Only then, in the end, comes the piece mentioning other options, such as Vite and Parcel:

use framework unless you app has unusual constraints

Even then, first you’ll have to admit your app has unusual constraints (and no examples were given of what that could be) before you’re actually “allowed” not to use a framework. It feels very much like you’re doing it in spite of all the warnings and that there actually isn’t a case where you should do it.

Why SPAs (still) matter

SPAs still have their place

SSR/SSG has been getting a lot of attention lately and has been a flagship feature of most new frameworks built on top of React. And rightly so - it has solved a major issue of using React for static & SEO-facing sites where time to first content (FCP) is crucial.

On the other hand, the use case where React, Angular, and other UI frameworks initially shined were dashboard apps (e.g., project management systems, CRMs, …) - it allowed for a radically better UX, which resembled that of desktop apps.

Although interactive content-rich apps (blogging platforms, marketplaces, social platforms) are today a typical poster child demo app for frameworks, dashboard-like apps still very much exist, and there are more of them than ever. Thousands of companies are building their internal tools daily, just like new SaaS-es pop up every day.

SEO is largely irrelevant for them since everything is happening behind the auth layer, where everything is centered around workflows, not content. SSR might even be counter-productive since it puts more pressure on your servers instead of distributing the rendering load across the clients.

How then would you develop SPAs?

Traditionally, React was only a UI library in your stack of choice. You would use CRA (or Vite nowadays) as a bundler/starter for your React project. Then you’d probably add a routing library (e.g., react-router) and maybe a state management library (e.g., Redux, or react-query), and you’d already be set pretty well. You would develop your backend in whatever you choose - Node.js/Express, Rails, or anything else.

There are also new frameworks emerging that focus on this particular use case (e.g., RedwoodJS and Wasp (disclaimer: this is us!)) whose flagship feature is not SSR, but rather the abstraction of API and CRUD on data models, and getting full-stack functionality from UI to the database, with extra features such as easy authentication and deployment out of the box.

With a “go for Next or you are unusual” and “you need SSR” message, React is making a strong signal against other solutions that don’t emphasize SSR as their main feature.

So what’s the big deal? Nobody forces you to use SSR in Next/Remix

That’s correct, but also it’s true that a buy-in into a whole framework is a much bigger step than just opting for a UI library. Frameworks are (more) opinionated and come with many decisions (code structure, architecture, deployment) made upfront for you. Which is great and that’s why they are valuable and why we’ll keep using them.

But, both sides of the story should be presented, and the final call should be left to the developer. React is too useful, valuable, and popular a tool and community to allow itself to skip this step.

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/04/11/wasp-launch-week-two.html b/blog/2023/04/11/wasp-launch-week-two.html index 5db226af5e..08edb3e0f8 100644 --- a/blog/2023/04/11/wasp-launch-week-two.html +++ b/blog/2023/04/11/wasp-launch-week-two.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Launch Week #2

· 7 min read
Matija Sosic

Here we go again! After three months of building and talking to our community about what features they'd like to see next, we're proud to kick off our second Launch Week. It stars tomorrow, and you can sign up for the launch event here!

Launch Week 2 is coming

Wasp Beta introduced a lot of core features that enabled developers to a build full-fledged SaaS-es. Since then, our community grew rapidly and we watched you deploy numerous apps and some of you even making their startups and earning their first revenue on top of Wasp!

Seeing that all the essential building blocks are now in place, our next goal became to make Wasp really easy (and fun) to use. We've had a bunch of ideas on everything we'd like to improve with DX for a while, and now finally came the right time to do it.

Nonetheless, the theme and sentiment of this launch week is best captured by an ancient term that poets used to describe some of the most beautiful and marvelous wonders of the world (e.g. pyramids, or the hanging gardens of Babylon): pizzazz 🍕.

Wednesday, Apr 12 - Launch event 🚀 + Pizzazz opener: Auth UI 💅

Wasp's easy auth has been by a long shot one of the most popular features in the community. We decided to take it one step further - Wasp now offers beautifully designed, pre-made auth components that you can simply plug into your app and immediately get that razzle dazzle on!

Auth UI Demo
On your localhost, tomorrow

We'll present this and much more at our Kick-off event, starting tomorrow on our Discord at 10 am EDT / 4 pm CET - sign up here and make sure to mark yourself as interested!

Join us to meet the team and to be the first to get a sneak peek into the latest features! We'll follow up with a casual AMA session, showcase selected community projects and discuss all together about what we'd like to see in Wasp next.

LW2 launch party instructions

P.S. : The word is out that there will be a raffle and that the most lucky one(s) will win some cool Wasp swag! (Da Boi included, ofc).

Thursday, Apr 13 - Deploy your app to Fly.io with a single CLI command

Deploying to Fly.io

When developing your app is blazingly fast, the last thing you want to slow you down is deployment. Figuring out how to exactly setup client/server, dealing with CORS, configuring ports and env vars, ... - well, now you don't have to think about it anymore!

This release of Wasp introduces first CLI deployment helper, for Fly.io (others coming soon, and you're free to contribute)!

How deployment feels now
Deployment in Wasp before vs now

Friday, Apr 14 - Improved database tooling & DX

Database seeding

Introducing two main quality-of-life features here:

  • wasp start db - Fully managed development database - (don't ever run docker run postgres ... again)
  • Database seeding - populate your database with some initial, "seed" data

This was something we ourselves ended up needing often when developing a new app, and although not a huge thing at the first glance, it's feels so good to have it taken care of! Given that Wasp is a fully managed full-stack framework that "understands" all parts of your dev process, we were in unique position to offer this functionality.

P.S. - you haven't been connecting to the prod database all along during development, have you?

Saturday, Apr 15 - More launch goodness: Custom API routes + Email sending ✉️

It's Saturday, so you get two features for the price of one!

Add custom API routes

Custom API routes
Adding a custom route handler at /foo/bar endpoint

Although for typical CRUD you don't have to define an API since Wasp offers a typesafe RPC layer via operations, sometimes you need extra flexibility (e.g. for implementing webhooks). Now you can easily do it, in a typical boilerplate-free Wasp style.

Email sending: Wasp + Sendgrid/Mailgun/...

Laurence Fishburne messenger pigeons
Don't end up like this, use Wasp for sending emails

Email sending - another feature that sounds like you should be able to implement it in 30 minutes (looking at you, auth), but then you find yourselves a week later cursing web development and having an inexplicable urge to start breeding messenger pigeons (that's what happened to Laurence Fishburne in John Wick, if you ever wondered).

Email sending code example

Wasp offers unified interface for different providers (e.g. Sendgrid or Mailgun, or a custom SMTP server). It also works great with our latest auth method, email - you get email verification and password reset out of the box!

Sunday, Apr 16 - Frontend testing and full-stack type safety!

We continue with our buy-one-get-one-free scheme (although both are free in all fairness):

Frontend testing, powered by Vitest

Frontend testing via Vitest

All you have to do to run your frontend tests is run wasp test client in your CLI! Backed by Vitest, while mocking is powered by MSW and additional Wasp helpers sprinkled on top. Now you really have no excuses to write your tests (except on the backend, support for them is coming next, so enjoy while it lasts)!

Full-stack type safety

Our RPC is now doing serious type-fu
Our typesafe RPC is now doing some serious type-fu

We already introduced glimpses of this in our Beta launch, but now things got even better! Whatever types you define and use on the server, be it entities or your custom types, they immediately get propagated to the client and typecheck in your IDE.

Monday, Apr 17 - SaaS GPT template + Waspathon #2 kick-off!

SaaS GPT template

Aaand we saved the best for the last - we'll put a special highlight on our SaaS GPT starter, which lets you build GPT-powered apps (such as CoverLetterGPT.xyz or SocialPostGPT.xyz) in a day and with all the good stuff pre-included - auth (social, email), Tailwind, deployment, Stripe and GPT API integration, ... - all you need to do is run it and start coding!

Our second hackathon - Waspathon #2!

Hacking away
Hate it when this happens.

And what a better reason to try out the SaaS GPT template than a hackathon! It will be an open format and you're free to build whatever you want - there will be a few categories will grade and award, but more on that coming soon!

The same for the prizes - expect cool wasp-themed swag and useful stuff that makes dev's life easier (no, it doesn't include getting rid of your PM).

We'll share more info and the registration link soon.

Recap

  • We are kicking off Launch Week #2 on Wed, April 12, at 10am EDT / 4pm CET - make sure to register for the event!
  • Launch Week #2 brings a ton of new exciting features - we’ll highlight one each day, starting tomorrow
  • On Monday, April 17, we’ll announce a hackathon - follow us on twitter and join our Discord to stay in the loop!

That’s it, Waspeteers - put your pizzazz (buzzazz?) on and see you tomorrow! 🐝

Matija, Martin & the Wasp team

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Launch Week #2

· 7 min read
Matija Sosic

Here we go again! After three months of building and talking to our community about what features they'd like to see next, we're proud to kick off our second Launch Week. It stars tomorrow, and you can sign up for the launch event here!

Launch Week 2 is coming

Wasp Beta introduced a lot of core features that enabled developers to a build full-fledged SaaS-es. Since then, our community grew rapidly and we watched you deploy numerous apps and some of you even making their startups and earning their first revenue on top of Wasp!

Seeing that all the essential building blocks are now in place, our next goal became to make Wasp really easy (and fun) to use. We've had a bunch of ideas on everything we'd like to improve with DX for a while, and now finally came the right time to do it.

Nonetheless, the theme and sentiment of this launch week is best captured by an ancient term that poets used to describe some of the most beautiful and marvelous wonders of the world (e.g. pyramids, or the hanging gardens of Babylon): pizzazz 🍕.

Wednesday, Apr 12 - Launch event 🚀 + Pizzazz opener: Auth UI 💅

Wasp's easy auth has been by a long shot one of the most popular features in the community. We decided to take it one step further - Wasp now offers beautifully designed, pre-made auth components that you can simply plug into your app and immediately get that razzle dazzle on!

Auth UI Demo
On your localhost, tomorrow

We'll present this and much more at our Kick-off event, starting tomorrow on our Discord at 10 am EDT / 4 pm CET - sign up here and make sure to mark yourself as interested!

Join us to meet the team and to be the first to get a sneak peek into the latest features! We'll follow up with a casual AMA session, showcase selected community projects and discuss all together about what we'd like to see in Wasp next.

LW2 launch party instructions

P.S. : The word is out that there will be a raffle and that the most lucky one(s) will win some cool Wasp swag! (Da Boi included, ofc).

Thursday, Apr 13 - Deploy your app to Fly.io with a single CLI command

Deploying to Fly.io

When developing your app is blazingly fast, the last thing you want to slow you down is deployment. Figuring out how to exactly setup client/server, dealing with CORS, configuring ports and env vars, ... - well, now you don't have to think about it anymore!

This release of Wasp introduces first CLI deployment helper, for Fly.io (others coming soon, and you're free to contribute)!

How deployment feels now
Deployment in Wasp before vs now

Friday, Apr 14 - Improved database tooling & DX

Database seeding

Introducing two main quality-of-life features here:

  • wasp start db - Fully managed development database - (don't ever run docker run postgres ... again)
  • Database seeding - populate your database with some initial, "seed" data

This was something we ourselves ended up needing often when developing a new app, and although not a huge thing at the first glance, it's feels so good to have it taken care of! Given that Wasp is a fully managed full-stack framework that "understands" all parts of your dev process, we were in unique position to offer this functionality.

P.S. - you haven't been connecting to the prod database all along during development, have you?

Saturday, Apr 15 - More launch goodness: Custom API routes + Email sending ✉️

It's Saturday, so you get two features for the price of one!

Add custom API routes

Custom API routes
Adding a custom route handler at /foo/bar endpoint

Although for typical CRUD you don't have to define an API since Wasp offers a typesafe RPC layer via operations, sometimes you need extra flexibility (e.g. for implementing webhooks). Now you can easily do it, in a typical boilerplate-free Wasp style.

Email sending: Wasp + Sendgrid/Mailgun/...

Laurence Fishburne messenger pigeons
Don't end up like this, use Wasp for sending emails

Email sending - another feature that sounds like you should be able to implement it in 30 minutes (looking at you, auth), but then you find yourselves a week later cursing web development and having an inexplicable urge to start breeding messenger pigeons (that's what happened to Laurence Fishburne in John Wick, if you ever wondered).

Email sending code example

Wasp offers unified interface for different providers (e.g. Sendgrid or Mailgun, or a custom SMTP server). It also works great with our latest auth method, email - you get email verification and password reset out of the box!

Sunday, Apr 16 - Frontend testing and full-stack type safety!

We continue with our buy-one-get-one-free scheme (although both are free in all fairness):

Frontend testing, powered by Vitest

Frontend testing via Vitest

All you have to do to run your frontend tests is run wasp test client in your CLI! Backed by Vitest, while mocking is powered by MSW and additional Wasp helpers sprinkled on top. Now you really have no excuses to write your tests (except on the backend, support for them is coming next, so enjoy while it lasts)!

Full-stack type safety

Our RPC is now doing serious type-fu
Our typesafe RPC is now doing some serious type-fu

We already introduced glimpses of this in our Beta launch, but now things got even better! Whatever types you define and use on the server, be it entities or your custom types, they immediately get propagated to the client and typecheck in your IDE.

Monday, Apr 17 - SaaS GPT template + Waspathon #2 kick-off!

SaaS GPT template

Aaand we saved the best for the last - we'll put a special highlight on our SaaS GPT starter, which lets you build GPT-powered apps (such as CoverLetterGPT.xyz or SocialPostGPT.xyz) in a day and with all the good stuff pre-included - auth (social, email), Tailwind, deployment, Stripe and GPT API integration, ... - all you need to do is run it and start coding!

Our second hackathon - Waspathon #2!

Hacking away
Hate it when this happens.

And what a better reason to try out the SaaS GPT template than a hackathon! It will be an open format and you're free to build whatever you want - there will be a few categories will grade and award, but more on that coming soon!

The same for the prizes - expect cool wasp-themed swag and useful stuff that makes dev's life easier (no, it doesn't include getting rid of your PM).

We'll share more info and the registration link soon.

Recap

  • We are kicking off Launch Week #2 on Wed, April 12, at 10am EDT / 4pm CET - make sure to register for the event!
  • Launch Week #2 brings a ton of new exciting features - we’ll highlight one each day, starting tomorrow
  • On Monday, April 17, we’ll announce a hackathon - follow us on twitter and join our Discord to stay in the loop!

That’s it, Waspeteers - put your pizzazz (buzzazz?) on and see you tomorrow! 🐝

Matija, Martin & the Wasp team

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/04/12/auth-ui.html b/blog/2023/04/12/auth-ui.html index c19b83e3f6..33a4e82d82 100644 --- a/blog/2023/04/12/auth-ui.html +++ b/blog/2023/04/12/auth-ui.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Auth UI: The first full-stack auth with self-updating forms!

· 2 min read
Matija Sosic

One of the main benefits of Wasp is having deep understanding of your entire full-stack app - e.g. what routes you have, what data models you defined, but also what methods you use for authentication. And that enables us to do some pretty cool stuff for you!

Auth UI Demo
Customize auth forms to fit your brand!

Once you've listed auth methods you want to use in your .wasp config file, you're done - from that Wasp generates a full authentication form that you simply import as a React component. And the best part is that is updates dynamically as you add/remove auth providers!

You can see the docs and give it a try here.

Auto-updating magic 🔮

Auth UI Demo gif
Add GitHub as another auth provider -> the form updates automatically!

tip

Since .wasp config file contains a high-level description of your app's requirements, Wasp can deduce a lot of stuff for you from it, and this is just a single example.

When you update your .wasp file by adding/removing an auth method (GitHub in this case), Wasp will detect it and automatically regenerate the auth form. No need to configure anything else, or change your React code - just a single line change in .wasp file and everything else will get taken care of!

Mind exploding
When you realize Wasp is a compiler and actually understands your app 🤯

Customize it! 🎨

Although it looks nice, all of this wouldn't be really useful if you couldn't customize it to fit your brand. That's easily done through the component's props:

Customizing auth form through props
Easily customize your auth form through props!

And that's it! You can see the whole list of tokens you can customize here. More are coming in the future!

Wasp out 🐝 🎤- give it a try and let us know how you liked it in our Discord !

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Auth UI: The first full-stack auth with self-updating forms!

· 2 min read
Matija Sosic

One of the main benefits of Wasp is having deep understanding of your entire full-stack app - e.g. what routes you have, what data models you defined, but also what methods you use for authentication. And that enables us to do some pretty cool stuff for you!

Auth UI Demo
Customize auth forms to fit your brand!

Once you've listed auth methods you want to use in your .wasp config file, you're done - from that Wasp generates a full authentication form that you simply import as a React component. And the best part is that is updates dynamically as you add/remove auth providers!

You can see the docs and give it a try here.

Auto-updating magic 🔮

Auth UI Demo gif
Add GitHub as another auth provider -> the form updates automatically!

tip

Since .wasp config file contains a high-level description of your app's requirements, Wasp can deduce a lot of stuff for you from it, and this is just a single example.

When you update your .wasp file by adding/removing an auth method (GitHub in this case), Wasp will detect it and automatically regenerate the auth form. No need to configure anything else, or change your React code - just a single line change in .wasp file and everything else will get taken care of!

Mind exploding
When you realize Wasp is a compiler and actually understands your app 🤯

Customize it! 🎨

Although it looks nice, all of this wouldn't be really useful if you couldn't customize it to fit your brand. That's easily done through the component's props:

Customizing auth form through props
Easily customize your auth form through props!

And that's it! You can see the whole list of tokens you can customize here. More are coming in the future!

Wasp out 🐝 🎤- give it a try and let us know how you liked it in our Discord !

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/04/13/db-start-and-seed.html b/blog/2023/04/13/db-start-and-seed.html index c4ff2dcb84..4e86914234 100644 --- a/blog/2023/04/13/db-start-and-seed.html +++ b/blog/2023/04/13/db-start-and-seed.html @@ -19,13 +19,13 @@ - - + +
-

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

· 6 min read
Martin Sosic

As a full-stack framework, Wasp doesn’t care “just” about frontend and backend, but it also covers the database!

It does this by allowing you to define Prisma data models in a Wasp file, connecting them to the relevant Wasp Operations, warning you if you need to do database migrations, deploying the database for you (if you choose so), … .

Since Wasp knows so much about your database, that puts us in a good position to keep finding ways to improve the developer experience regarding dealing with the database. For Wasp v0.10, we focused on:

  1. Wasp running the dev database for you with no config needed → Fully Managed Dev Database 🚀
  2. Wasp helping you to initialize the database with some data → Db Seeding 🌱

strong wasp database
Wasp now has `wasp start db` and `wasp db seed`!

Fully Managed Dev Database 🚀

You might have asked yourself:

If Wasp already knows so much about my database, why do I need to bother running it on my own!?

Ok, when you start a new Wasp project it is easy because you are using an SQLite database, but once you switch to Postgres, it falls onto you to take care of it: run it, provide its URL to Wasp via env var, handle multiple databases if you have multiple Wasp apps, … .

This can get tedious quickly, especially if you are visiting your Wasp project that you haven’t worked on for a bit and need to figure out again how to run the db, or you need to check out somebody else’s Wasp project and don’t have it all set up yet. It is something most of us are used to, especially with other frameworks, but still, we can do better at Wasp!

This is where wasp start db comes in!

wasp start db running in terminal
wasp start db in action, running a posgtres dev db for you

Now, all you need to do to run the development database, is run wasp start db, and Wasp will run it for you and will know how to connect to it during development.

No env var setting, no remembering how to run the db. The only requirement is that you have Docker installed on your machine. Data from your database will be persisted on the disk between the runs, and each Wasp app will have its own database assigned.

Btw, you can still use a custom database that you ran on your own if you want, the same way it was done before in Wasp: by setting env var DATABASE_URL.

Database seeding 🌱

Database seeding is a term for populating the database with some initial data.

Seeding is most commonly used for two following scenarios:

  1. To put the development database into a state convenient for testing / playing with it.
  2. To initialize the dev/staging/prod database with some essential data needed for it to be useful, for example, default currencies in a Currency table.

Wasp so far had no direct support for seeding, so you had to either come up with your own solution (e.g. script that connects to the db and executes some queries), or massage data manually via Prisma Studio (wasp db studio).

There is one big drawback to both of the approaches I mentioned above though: there is no easy way to reuse logic that you have already implemented in your Wasp app, especially Actions (e.g. createTask)! This is pretty bad, as it makes your seeding logic brittle.

This is where wasp db seed comes in! Now, Wasp allows you to write a JS/TS function, import any server logic (including Actions) into it as you wish, and then seed the database with it.

wasp db seed running in terminal
wasp db seed in action, initializing the db with dev data

Registering seed functions in Wasp is easy:

app MyApp {
// ...
db: {
// ...
seeds: [
import { devSeedSimple } from "@server/dbSeeds.js",
import { prodSeed } from "@server/dbSeeds.js"
]
}
}

Example of a seed function from above, devSeedSimple:

import { createTask } from './actions.js'

export const devSeedSimple = async (prismaClient) => {
const user = await createUser(prismaClient, {
username: "RiuTheDog",
password: "bark1234"
})

await createTask(
{ description: "Chase the cat" },
{ user, entities: { Task: prismaClient.task } }
)
}

async function createUser (prismaClient, data) {
const { password, ...newUser } = await prismaClient.user.create({ data })
return newUser
}

Finally, to run these seeds, you can either do:

  • wasp db seed: If you have just one seed function, it will run it. If you have multiple, it will interactively ask you to choose one to run.
  • wasp db seed <seed-name>: It will run the seed function with the specified name, where the name is the identifier you used in its import expression in the app.db.seeds list. Example: wasp db seed devSeedSimple.

We also added wasp db reset command (calls prisma db reset in the background) that cleans up the database for you (removes all data and tables and re-applies migrations), which is great to use in combination with wasp db seed, as a precursor.

Plans for the future 🔮

  • allow customization of managed dev database (Postgres plugins, custom Dockerfile, …)
  • have Wasp run the managed dev database automatically whenever it needs it (instead of you having to run wasp start db manually)
  • dynamically find a free port for managed dev database (right now it requires port 5432)
  • provide utility functions to make writing seeding functions easier (e.g. functions for creating new users)
  • right now seeding functions are defined as part of a Wasp server code → it might be interesting to separate them in a standalone “project” in the future, while still keeping their easy access to the server logic.
  • do you have any ideas/suggestions? Let us know in our Discord !
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

· 6 min read
Martin Sosic

As a full-stack framework, Wasp doesn’t care “just” about frontend and backend, but it also covers the database!

It does this by allowing you to define Prisma data models in a Wasp file, connecting them to the relevant Wasp Operations, warning you if you need to do database migrations, deploying the database for you (if you choose so), … .

Since Wasp knows so much about your database, that puts us in a good position to keep finding ways to improve the developer experience regarding dealing with the database. For Wasp v0.10, we focused on:

  1. Wasp running the dev database for you with no config needed → Fully Managed Dev Database 🚀
  2. Wasp helping you to initialize the database with some data → Db Seeding 🌱

strong wasp database
Wasp now has `wasp start db` and `wasp db seed`!

Fully Managed Dev Database 🚀

You might have asked yourself:

If Wasp already knows so much about my database, why do I need to bother running it on my own!?

Ok, when you start a new Wasp project it is easy because you are using an SQLite database, but once you switch to Postgres, it falls onto you to take care of it: run it, provide its URL to Wasp via env var, handle multiple databases if you have multiple Wasp apps, … .

This can get tedious quickly, especially if you are visiting your Wasp project that you haven’t worked on for a bit and need to figure out again how to run the db, or you need to check out somebody else’s Wasp project and don’t have it all set up yet. It is something most of us are used to, especially with other frameworks, but still, we can do better at Wasp!

This is where wasp start db comes in!

wasp start db running in terminal
wasp start db in action, running a posgtres dev db for you

Now, all you need to do to run the development database, is run wasp start db, and Wasp will run it for you and will know how to connect to it during development.

No env var setting, no remembering how to run the db. The only requirement is that you have Docker installed on your machine. Data from your database will be persisted on the disk between the runs, and each Wasp app will have its own database assigned.

Btw, you can still use a custom database that you ran on your own if you want, the same way it was done before in Wasp: by setting env var DATABASE_URL.

Database seeding 🌱

Database seeding is a term for populating the database with some initial data.

Seeding is most commonly used for two following scenarios:

  1. To put the development database into a state convenient for testing / playing with it.
  2. To initialize the dev/staging/prod database with some essential data needed for it to be useful, for example, default currencies in a Currency table.

Wasp so far had no direct support for seeding, so you had to either come up with your own solution (e.g. script that connects to the db and executes some queries), or massage data manually via Prisma Studio (wasp db studio).

There is one big drawback to both of the approaches I mentioned above though: there is no easy way to reuse logic that you have already implemented in your Wasp app, especially Actions (e.g. createTask)! This is pretty bad, as it makes your seeding logic brittle.

This is where wasp db seed comes in! Now, Wasp allows you to write a JS/TS function, import any server logic (including Actions) into it as you wish, and then seed the database with it.

wasp db seed running in terminal
wasp db seed in action, initializing the db with dev data

Registering seed functions in Wasp is easy:

app MyApp {
// ...
db: {
// ...
seeds: [
import { devSeedSimple } from "@server/dbSeeds.js",
import { prodSeed } from "@server/dbSeeds.js"
]
}
}

Example of a seed function from above, devSeedSimple:

import { createTask } from './actions.js'

export const devSeedSimple = async (prismaClient) => {
const user = await createUser(prismaClient, {
username: "RiuTheDog",
password: "bark1234"
})

await createTask(
{ description: "Chase the cat" },
{ user, entities: { Task: prismaClient.task } }
)
}

async function createUser (prismaClient, data) {
const { password, ...newUser } = await prismaClient.user.create({ data })
return newUser
}

Finally, to run these seeds, you can either do:

  • wasp db seed: If you have just one seed function, it will run it. If you have multiple, it will interactively ask you to choose one to run.
  • wasp db seed <seed-name>: It will run the seed function with the specified name, where the name is the identifier you used in its import expression in the app.db.seeds list. Example: wasp db seed devSeedSimple.

We also added wasp db reset command (calls prisma db reset in the background) that cleans up the database for you (removes all data and tables and re-applies migrations), which is great to use in combination with wasp db seed, as a precursor.

Plans for the future 🔮

  • allow customization of managed dev database (Postgres plugins, custom Dockerfile, …)
  • have Wasp run the managed dev database automatically whenever it needs it (instead of you having to run wasp start db manually)
  • dynamically find a free port for managed dev database (right now it requires port 5432)
  • provide utility functions to make writing seeding functions easier (e.g. functions for creating new users)
  • right now seeding functions are defined as part of a Wasp server code → it might be interesting to separate them in a standalone “project” in the future, while still keeping their easy access to the server logic.
  • do you have any ideas/suggestions? Let us know in our Discord !
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/04/17/How-I-Built-CoverLetterGPT.html b/blog/2023/04/17/How-I-Built-CoverLetterGPT.html index 6581fa212c..aa62504048 100644 --- a/blog/2023/04/17/How-I-Built-CoverLetterGPT.html +++ b/blog/2023/04/17/How-I-Built-CoverLetterGPT.html @@ -19,13 +19,13 @@ - - + +
-

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

· 3 min read
Vinny


Like many other software developers, I enjoy trying out new technologies even if it's just to get a feel for what they can do.

So when I first learned about the OpenAI API, I knew I wanted to give it a try. I had already wanted to create a SaaS app that could help manage the process of applying to numerous jobs, and the prospect of adding GPT into the mix made it even more interesting. So with API access and a bit of free time, I decided to give it a shot.

I threw together a simple version of the app in about 3-4 days and CoverLetterGPT was born, a SaaS app that uses GPT-3.5-turbo to generate, revise, and manage cover letters for you based on your skills and the specific job descriptions.

Even though I did think it had potential as a SaaS app, I was approaching it mostly as a way to learn how to build one for the first time. And after seeing so many people "building in public" and sharing their progress, I thought it would be fun to try it out myself.

Hey peeps. Check out http://coverlettergpt.xyz. You can try it out now and create your own cover letters for free (no Payment/API key). I'm working on A LOT more features. Stay Tuned!

So I started sharing my progress on Twitter, Reddit, and Indie Hackers. I made my first post about it on March 9th, and because I was just experimenting and trying my hand at a SaaS app for the first time, I also open-sourced the app to share the code and what I was learning with others. This led to a lot of interest and great feedback, and I ended up getting featured in the indiehackers newsletter, which led to even more interest.

Within the first month, I got over 1,000 sign-ups along with my first paying customers. Pretty surprising, to say the least!

So to continue in the spirit of curiosity, learning, and just "wingin' it," I decided to make a code walkthrough video that explains how I built the app, the tools I used to build it, and a little bit about how I marketed the app without spending any money.

As an extra bonus, I also give a quick introduction to the free SaaS template I created for building your own SaaS app, with or without GPT, on the PERN stack (PostgreSQL/Prisma, Express, React, NodeJS).

My hope is that others will learn something from my experience, and that it could inspire them to try out new technologies and build that app idea they've had in mind (and if they do, they should make sure to share it with me on Twitter @hot_town -- I'd love to see it!)

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

· 3 min read
Vinny


Like many other software developers, I enjoy trying out new technologies even if it's just to get a feel for what they can do.

So when I first learned about the OpenAI API, I knew I wanted to give it a try. I had already wanted to create a SaaS app that could help manage the process of applying to numerous jobs, and the prospect of adding GPT into the mix made it even more interesting. So with API access and a bit of free time, I decided to give it a shot.

I threw together a simple version of the app in about 3-4 days and CoverLetterGPT was born, a SaaS app that uses GPT-3.5-turbo to generate, revise, and manage cover letters for you based on your skills and the specific job descriptions.

Even though I did think it had potential as a SaaS app, I was approaching it mostly as a way to learn how to build one for the first time. And after seeing so many people "building in public" and sharing their progress, I thought it would be fun to try it out myself.

Hey peeps. Check out http://coverlettergpt.xyz. You can try it out now and create your own cover letters for free (no Payment/API key). I'm working on A LOT more features. Stay Tuned!

So I started sharing my progress on Twitter, Reddit, and Indie Hackers. I made my first post about it on March 9th, and because I was just experimenting and trying my hand at a SaaS app for the first time, I also open-sourced the app to share the code and what I was learning with others. This led to a lot of interest and great feedback, and I ended up getting featured in the indiehackers newsletter, which led to even more interest.

Within the first month, I got over 1,000 sign-ups along with my first paying customers. Pretty surprising, to say the least!

So to continue in the spirit of curiosity, learning, and just "wingin' it," I decided to make a code walkthrough video that explains how I built the app, the tools I used to build it, and a little bit about how I marketed the app without spending any money.

As an extra bonus, I also give a quick introduction to the free SaaS template I created for building your own SaaS app, with or without GPT, on the PERN stack (PostgreSQL/Prisma, Express, React, NodeJS).

My hope is that others will learn something from my experience, and that it could inspire them to try out new technologies and build that app idea they've had in mind (and if they do, they should make sure to share it with me on Twitter @hot_town -- I'd love to see it!)

Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/04/27/wasp-hackathon-two.html b/blog/2023/04/27/wasp-hackathon-two.html index 8938a5e892..4edae50e41 100644 --- a/blog/2023/04/27/wasp-hackathon-two.html +++ b/blog/2023/04/27/wasp-hackathon-two.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Hackathon #2 - Let's "hack-a-ton"!

· 2 min read
Vinny


So Launch Week #2 has officially come to an end, and as the tradition goes, the end of the launch week means the beginning of a hackathon!

We've launched a ton of new features for you to build your Hackathon project with, including:

You can read all it in this blog post, or watch a 1-minute video showing how it all works in practice 🎬!

Launch Week #2 Features -- YouTube Short
Launch Week #2 Features -- YouTube Short

Even better, we've got a new starter templates feature that lets you create a new project with a pre-built template, so you can get started even faster! Like this sweet SaaS template with GPT, Stripe, SendGrid, and Tailwind UI already integrated:

Wasp SaaS Template w/ GPT, Stripe, and more 🎊

Just run wasp new my-project -t saas and you're good to go.

The prizes for the hackathon include an awesome Wasp-themed mechanical keyboard, tons of Wasp swag, and more cool stuff (e.g., virtual hugs from the team)!

The only rule is to use Wasp, and you can build whatever you want (but both you and I know it's going to be a GPT-powered app, so make sure to use our template).

The applications are open, and the hackathon starts on April 28th and ends May 7th. You can apply (solo or with a team) here:


Good luck and Happy Hacking 🐝🚀!



Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Hackathon #2 - Let's "hack-a-ton"!

· 2 min read
Vinny


So Launch Week #2 has officially come to an end, and as the tradition goes, the end of the launch week means the beginning of a hackathon!

We've launched a ton of new features for you to build your Hackathon project with, including:

You can read all it in this blog post, or watch a 1-minute video showing how it all works in practice 🎬!

Launch Week #2 Features -- YouTube Short
Launch Week #2 Features -- YouTube Short

Even better, we've got a new starter templates feature that lets you create a new project with a pre-built template, so you can get started even faster! Like this sweet SaaS template with GPT, Stripe, SendGrid, and Tailwind UI already integrated:

Wasp SaaS Template w/ GPT, Stripe, and more 🎊

Just run wasp new my-project -t saas and you're good to go.

The prizes for the hackathon include an awesome Wasp-themed mechanical keyboard, tons of Wasp swag, and more cool stuff (e.g., virtual hugs from the team)!

The only rule is to use Wasp, and you can build whatever you want (but both you and I know it's going to be a GPT-powered app, so make sure to use our template).

The applications are open, and the hackathon starts on April 28th and ends May 7th. You can apply (solo or with a team) here:


Good luck and Happy Hacking 🐝🚀!



Want to stay in the loop? → Join our newsletter!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/05/19/hackathon-2-review.html b/blog/2023/05/19/hackathon-2-review.html index 76f25dce82..024968a905 100644 --- a/blog/2023/05/19/hackathon-2-review.html +++ b/blog/2023/05/19/hackathon-2-review.html @@ -19,13 +19,13 @@ - - + +
-

Hackathon #2: Results & Review

· 6 min read
Vinny

To finalize Wasp's Launch Week #2, we held our second Hackathon. Just like the "Betathon" before it, it was an open hackathon where the only requirement was to build something cool with Wasp!

In this post, I’ll give a quick overview of:

  • the hackathon results 🏆
  • how the hackathon was organized
  • how we promoted it
  • the community response

…and the Winners Are:

What’s a hackathon without the participants!? Let’s get this post off to a proper start by congratulating our winners and showcasing their work. 🔍

🥇 Typergotchi

Typergotchi

Our unofficial mascot, Da Boi, makes his debut app appearance in this fun, feature-rich typing game!

Wasp makes building fullstack apps fast and fun. We've done lots of hackthons in the past, and we couldn't pass up the chance to win a mechanical keyboard :)” - Umbrien & kg04ls

🥈 Office Wars

Office Wars

A turn-based, multi-player strategy game where you command your tank across a hexagonal map. A great way to keep your coworkers engaged while you wait for your code to compile!

We love how Wasp brings the tools that are already being used by developers under the same umbrella. It's very streamlined and makes building fullstack apps easy to accomplish... like django but w/ more superpowers” - Roland & Luís

🥉 Tied for Third: Bee Pretty & StorAI

Bee Pretty

StorAI

After 5 minutes of working with Wasp I thought, this is phenomenal! So much just works out of the box -- everything was flawless" - mkinkela1

🥳 And A Big Round of Applause for the Rest of the Participants!

Thanks so much to rest of the participants:

  • Max for submitting Feedback Hub, which we award "the most SaaS-y app".
  • Richard for submitting Promise, for winning the "best last-minute minimal-effort submission" award.
  • Swarnavo for submitting his Dashboard Panel app.

Hackathon How-to

For our first hackathon, the "Betathon", we announced and started it on the final day of our launch week. Looking back, this probably wasn't the best approach because it didn't give people much time to prepare. This time around, we announced the hackathon a week in advance, giving people a bit more time to prepare their projects.

Wasp Betathon Homepage
Our dedicated hackathon landing page w/ intro video & countdown timer

And just like last time, we kept the Hackathon rules simple: no categories, no constraints, just 10 days to create any fullstack web app using Wasp, alone or in a team of up to 4 people.

Keyboard

We may be a bit unoriginal here, but we also decided to offer the same grand prize as the Betathon: a Wasp-colored mechanical keyboard. On top of that, runner-ups also got some project-related prizes, as well as Wasp beanies, shirts, and other swag. Of course, we also spotlight the winner’s on our social media accounts.

Something new we did this time was hold a post-hackathon presentation event on Discord, thanks to a suggestion made by Max, one of our most dedicated contributers. We gave each team a chance to present their projects and talk a bit about their experience. The turnout was great, with almost all the teams participating, and it helped us to get to know the faces behind the apps. Not only was this a great way to connect more with our community, but it also gave us some insight into where are users are coming from, what they're interested in, and what they're looking for in Wasp.

Promotion

As of late, we've made an effort to promote exemplary apps built with Wasp, as well as create some of our own. This has been a great way to show off Wasp's capabilities, and has resulted in a noticeable increase in interest and traffic. Therefore, for the Hackathon, we let the organic interest in Wasp be the driver for the Hackathon, as we didn't do much promotion outside of our own channels, nor did we partner with any other sponsors this time. We simply announced the Hackathon and directed people to our Hacakthon homepage we created.

The hackathon page is nice to have as a central spot for all the rules and relevant info. We also added a fun intro video using AI-generated narration of a possibly well-known actor 😎. Overall, the effort put into the homepage gives participants the feeling that they’re entering into a serious contest and committing to something of substance, while the light-heartedness of the promotion material lets them know that it's more about fun than serious prizes. But even in the abscence of big winnings, the quality of the submissions were suprisingly high. Intrinsic motivation, ftw! 🤩

Hackathon Wasp app repo
Wanna host your own Hackathon? Use our template app!

Again, just like we did previously, we wrote the Hackathon Homepage with Wasp, and put the source code up on our GitHub. We thought it might inspire people to build with Wasp, using it as a guide while creating their own projects for the hackathon, plus it could be used by others in the future if they want to host their own hackathon. 💻

The Response 2.0

We were really pleased to see the response to the Hackathon surpass our expectations, yet again. The number, quality, and creativity of the submissions were even better than the Betathon. We also had a lot of fun interacting with the participants, and we're looking forward to doing it again soon.

It's reaffirming to see Wasp grow along with our community, as they build more and more cool stuff with it. Events like this give us a morale and confidence boost as it confirms that we're building something the community wants.

Thanks so much again to the participants for their hard work and contributions. We're grateful and happy to have you along for the ride! 🐝🚀

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Hackathon #2: Results & Review

· 6 min read
Vinny

To finalize Wasp's Launch Week #2, we held our second Hackathon. Just like the "Betathon" before it, it was an open hackathon where the only requirement was to build something cool with Wasp!

In this post, I’ll give a quick overview of:

  • the hackathon results 🏆
  • how the hackathon was organized
  • how we promoted it
  • the community response

…and the Winners Are:

What’s a hackathon without the participants!? Let’s get this post off to a proper start by congratulating our winners and showcasing their work. 🔍

🥇 Typergotchi

Typergotchi

Our unofficial mascot, Da Boi, makes his debut app appearance in this fun, feature-rich typing game!

Wasp makes building fullstack apps fast and fun. We've done lots of hackthons in the past, and we couldn't pass up the chance to win a mechanical keyboard :)” - Umbrien & kg04ls

🥈 Office Wars

Office Wars

A turn-based, multi-player strategy game where you command your tank across a hexagonal map. A great way to keep your coworkers engaged while you wait for your code to compile!

We love how Wasp brings the tools that are already being used by developers under the same umbrella. It's very streamlined and makes building fullstack apps easy to accomplish... like django but w/ more superpowers” - Roland & Luís

🥉 Tied for Third: Bee Pretty & StorAI

Bee Pretty

StorAI

After 5 minutes of working with Wasp I thought, this is phenomenal! So much just works out of the box -- everything was flawless" - mkinkela1

🥳 And A Big Round of Applause for the Rest of the Participants!

Thanks so much to rest of the participants:

  • Max for submitting Feedback Hub, which we award "the most SaaS-y app".
  • Richard for submitting Promise, for winning the "best last-minute minimal-effort submission" award.
  • Swarnavo for submitting his Dashboard Panel app.

Hackathon How-to

For our first hackathon, the "Betathon", we announced and started it on the final day of our launch week. Looking back, this probably wasn't the best approach because it didn't give people much time to prepare. This time around, we announced the hackathon a week in advance, giving people a bit more time to prepare their projects.

Wasp Betathon Homepage
Our dedicated hackathon landing page w/ intro video & countdown timer

And just like last time, we kept the Hackathon rules simple: no categories, no constraints, just 10 days to create any fullstack web app using Wasp, alone or in a team of up to 4 people.

Keyboard

We may be a bit unoriginal here, but we also decided to offer the same grand prize as the Betathon: a Wasp-colored mechanical keyboard. On top of that, runner-ups also got some project-related prizes, as well as Wasp beanies, shirts, and other swag. Of course, we also spotlight the winner’s on our social media accounts.

Something new we did this time was hold a post-hackathon presentation event on Discord, thanks to a suggestion made by Max, one of our most dedicated contributers. We gave each team a chance to present their projects and talk a bit about their experience. The turnout was great, with almost all the teams participating, and it helped us to get to know the faces behind the apps. Not only was this a great way to connect more with our community, but it also gave us some insight into where are users are coming from, what they're interested in, and what they're looking for in Wasp.

Promotion

As of late, we've made an effort to promote exemplary apps built with Wasp, as well as create some of our own. This has been a great way to show off Wasp's capabilities, and has resulted in a noticeable increase in interest and traffic. Therefore, for the Hackathon, we let the organic interest in Wasp be the driver for the Hackathon, as we didn't do much promotion outside of our own channels, nor did we partner with any other sponsors this time. We simply announced the Hackathon and directed people to our Hacakthon homepage we created.

The hackathon page is nice to have as a central spot for all the rules and relevant info. We also added a fun intro video using AI-generated narration of a possibly well-known actor 😎. Overall, the effort put into the homepage gives participants the feeling that they’re entering into a serious contest and committing to something of substance, while the light-heartedness of the promotion material lets them know that it's more about fun than serious prizes. But even in the abscence of big winnings, the quality of the submissions were suprisingly high. Intrinsic motivation, ftw! 🤩

Hackathon Wasp app repo
Wanna host your own Hackathon? Use our template app!

Again, just like we did previously, we wrote the Hackathon Homepage with Wasp, and put the source code up on our GitHub. We thought it might inspire people to build with Wasp, using it as a guide while creating their own projects for the hackathon, plus it could be used by others in the future if they want to host their own hackathon. 💻

The Response 2.0

We were really pleased to see the response to the Hackathon surpass our expectations, yet again. The number, quality, and creativity of the submissions were even better than the Betathon. We also had a lot of fun interacting with the participants, and we're looking forward to doing it again soon.

It's reaffirming to see Wasp grow along with our community, as they build more and more cool stuff with it. Events like this give us a morale and confidence boost as it confirms that we're building something the community wants.

Thanks so much again to the participants for their hard work and contributions. We're grateful and happy to have you along for the ride! 🐝🚀

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/06/07/wasp-beta-update-may-23.html b/blog/2023/06/07/wasp-beta-update-may-23.html index e71f2f3998..be12cab598 100644 --- a/blog/2023/06/07/wasp-beta-update-may-23.html +++ b/blog/2023/06/07/wasp-beta-update-may-23.html @@ -19,14 +19,14 @@ - - + +
-

Wasp Beta - May 2023

· 6 min read
Matija Sosic

Wasp Update May 23

Want to stay in the loop? → Join our newsletter!

Hola Waspeteers 🐝,

What did one plant say to the other? Aloe! Long thyme no see. 🌱

Now that we've set the tone, let me guide you through what's new in Waspworld (that would be a cool theme park, right?):

Wasp Hackathon 2.0 is over - congrats to the winners! 🐝 🏆 🐝

Congrats to the hackathon winners!
Shoutout to the winning team - Typergotchi! They even made a cool illustration with our mascot, Da Boi 🐝 😎

We had more submissions than ever, and the quality and creativity of your apps were really at the next level. We had everything from admin dashboards and GPT-powered story-telling apps to the actual games.

Hackathon testimonial

See all the winners and read a full Hackathon 2.0 review 👉 here 👈.

Wasp Launch Week #3 is in the making - get ready for the Magic 🔮 🧙

As it always happens in the wilderness, after one launch week, there comes another one. And who are we to defy the laws of nature - thus, get ready for Launch Week #3!

We are aiming for the end of June, but we'll announce the exact date soon. Make sure to follow us on Twitter or/and join our Discord to stay in the loop.

Beautiful
When you see it ✨

After Pizzazz 🍕 ...

As you might remember, the motto/topic of our last launch was Pizzaz, which referred to improving the developer experience in Wasp - full-stack auth, one-line deployment, type safety, db tooling, ...

... Comes Magic! 🔮

While DX will always be our top priority, we're now shifting gears a bit - the keyword we chose to represent our next launch is ✨ Magic ✨. The reason is that now that we have a majority of the features you'd expect in a web framework in place, we can start utilizing Wasp's unique compiler-driven approach to offer next-level features no other framework can!

LW3 Sneak Peek 🤫 👀

More details coming soon, but in the meanwhile, here are some of the features we're most excited about:

🚧 Wasp AI 🤖 ✨

There is no mAgIc without AI! We cannot share many details on this yet, but it is something we've been exploring a lot lately. Our previous experiments have shown that, due to its declarative and human-readable nature, Wasp is naturally a very good fit for LLMs.

We'll take this to the next level for our next launch - stay tuned!

🚧 Auto CRUD

Although Wasp helps a lot with bootstrapping your app, one repetitive thing that you have to do every time is implement "standard" CRUD operations for your data models.

We decided to put a stop to it - welcome our new (incoming) feature, Auto CRUD!

Auto CRUD
Syntax proposal for the new Auto CRUD feature

All you have to do is specify in your .wasp file which CRUD operations you want, and they will be auto-generated for you to use in your JS/TS code. The best part is when you update your data model, these will get updated as well! 🤯

This feature is also a really good showcase of Wasp's compiler muscles - the best you could get with a traditional framework approach is scaffolding, which means spitting out code that will quickly get outdated and that you have to maintain.

See a 2-min demo of Wasp Auto CRUD in action - by our founding engineer Miho

Showing off compiler muscles
Our compiler right now

🚧 Advanced syntax completion for .wasp files (LSP)

Improved LSP

We're making our VS Code extension even better! So far it has provided highlighting and auto-completion for top-level declarations (e.g., route, entity, query, ...), but now it's going even deeper. Every property will display its full type as you are typing it out + you'll get a context-aware auto-completion.

🚧 Support for web sockets 🔌 🧦!

Wasp will soon support Web Sockets! This will allow you to have a persistent, real-time connection between your client and server, which is great for chat apps, games, and more.

Web sockets in Wasp
Defining a new web socket in Wasp config file

For now it is a stand-alone feature, but it opens some really interesting possibilities - e.g. combining this with Wasp's query/action system and letting you declare a particular query to be "live". Just an idea for now but something to keep in mind as we test and receive more feedback on this feature.

From the blog 📖

The community buzz 🐝 💬

Last month was super buzzy! We got several awesome reviews, and Wasp also got picked up by a couple of YouTube dev influencers:

Wasp testimonial

Wasp GitHub Star Growth - 2,825 ⭐

Getting close to the big 3,000! Huge thanks to all our contributors and stargazers - you are amazing!

GitHub stars - almost 3,000!
Almost 3,000 stars! 🐝 🚀

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

That's a wrap! 🌯

A dramatic goodbye gif
A dramatic goodbye - don't ever let go

That's it for this month and thanks for reading! Since you've come this far, you deserve one final treat - a Wasp-themed joke generated by ChatGPT:

GPT Wasp joke
Good one, dad.

Fly high, and we'll see you soon 🐝 🐝,
+

Wasp Beta - May 2023

· 6 min read
Matija Sosic

Wasp Update May 23

Want to stay in the loop? → Join our newsletter!

Hola Waspeteers 🐝,

What did one plant say to the other? Aloe! Long thyme no see. 🌱

Now that we've set the tone, let me guide you through what's new in Waspworld (that would be a cool theme park, right?):

Wasp Hackathon 2.0 is over - congrats to the winners! 🐝 🏆 🐝

Congrats to the hackathon winners!
Shoutout to the winning team - Typergotchi! They even made a cool illustration with our mascot, Da Boi 🐝 😎

We had more submissions than ever, and the quality and creativity of your apps were really at the next level. We had everything from admin dashboards and GPT-powered story-telling apps to the actual games.

Hackathon testimonial

See all the winners and read a full Hackathon 2.0 review 👉 here 👈.

Wasp Launch Week #3 is in the making - get ready for the Magic 🔮 🧙

As it always happens in the wilderness, after one launch week, there comes another one. And who are we to defy the laws of nature - thus, get ready for Launch Week #3!

We are aiming for the end of June, but we'll announce the exact date soon. Make sure to follow us on Twitter or/and join our Discord to stay in the loop.

Beautiful
When you see it ✨

After Pizzazz 🍕 ...

As you might remember, the motto/topic of our last launch was Pizzaz, which referred to improving the developer experience in Wasp - full-stack auth, one-line deployment, type safety, db tooling, ...

... Comes Magic! 🔮

While DX will always be our top priority, we're now shifting gears a bit - the keyword we chose to represent our next launch is ✨ Magic ✨. The reason is that now that we have a majority of the features you'd expect in a web framework in place, we can start utilizing Wasp's unique compiler-driven approach to offer next-level features no other framework can!

LW3 Sneak Peek 🤫 👀

More details coming soon, but in the meanwhile, here are some of the features we're most excited about:

🚧 Wasp AI 🤖 ✨

There is no mAgIc without AI! We cannot share many details on this yet, but it is something we've been exploring a lot lately. Our previous experiments have shown that, due to its declarative and human-readable nature, Wasp is naturally a very good fit for LLMs.

We'll take this to the next level for our next launch - stay tuned!

🚧 Auto CRUD

Although Wasp helps a lot with bootstrapping your app, one repetitive thing that you have to do every time is implement "standard" CRUD operations for your data models.

We decided to put a stop to it - welcome our new (incoming) feature, Auto CRUD!

Auto CRUD
Syntax proposal for the new Auto CRUD feature

All you have to do is specify in your .wasp file which CRUD operations you want, and they will be auto-generated for you to use in your JS/TS code. The best part is when you update your data model, these will get updated as well! 🤯

This feature is also a really good showcase of Wasp's compiler muscles - the best you could get with a traditional framework approach is scaffolding, which means spitting out code that will quickly get outdated and that you have to maintain.

See a 2-min demo of Wasp Auto CRUD in action - by our founding engineer Miho

Showing off compiler muscles
Our compiler right now

🚧 Advanced syntax completion for .wasp files (LSP)

Improved LSP

We're making our VS Code extension even better! So far it has provided highlighting and auto-completion for top-level declarations (e.g., route, entity, query, ...), but now it's going even deeper. Every property will display its full type as you are typing it out + you'll get a context-aware auto-completion.

🚧 Support for web sockets 🔌 🧦!

Wasp will soon support Web Sockets! This will allow you to have a persistent, real-time connection between your client and server, which is great for chat apps, games, and more.

Web sockets in Wasp
Defining a new web socket in Wasp config file

For now it is a stand-alone feature, but it opens some really interesting possibilities - e.g. combining this with Wasp's query/action system and letting you declare a particular query to be "live". Just an idea for now but something to keep in mind as we test and receive more feedback on this feature.

From the blog 📖

The community buzz 🐝 💬

Last month was super buzzy! We got several awesome reviews, and Wasp also got picked up by a couple of YouTube dev influencers:

Wasp testimonial

Wasp GitHub Star Growth - 2,825 ⭐

Getting close to the big 3,000! Huge thanks to all our contributors and stargazers - you are amazing!

GitHub stars - almost 3,000!
Almost 3,000 stars! 🐝 🚀

And if you haven't yet, please star us on Github! Yes, we are shameless star beggars, but if you believe in the project and want to support it that's one of the best ways to do it (next to actually building something with Wasp - go do that too! :D)

That's a wrap! 🌯

A dramatic goodbye gif
A dramatic goodbye - don't ever let go

That's it for this month and thanks for reading! Since you've come this far, you deserve one final treat - a Wasp-themed joke generated by ChatGPT:

GPT Wasp joke
Good one, dad.

Fly high, and we'll see you soon 🐝 🐝,
Matija, Martin and the Wasp team

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/06/22/wasp-launch-week-three.html b/blog/2023/06/22/wasp-launch-week-three.html index 4399208ef0..7a884df97d 100644 --- a/blog/2023/06/22/wasp-launch-week-three.html +++ b/blog/2023/06/22/wasp-launch-week-three.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Launch Week #3: Magic

· 6 min read
Matija Sosic

Launch Week 3 is coming

By now, it is a tradition. For the every upcoming launch week, we ask ourselves how can we top the last one? How can we make building full-stack web apps easier, more enjoyable and get rid of even more boilerplate?

If this is the first time you're joining, check our previous launches:

Our first launch week was about making the promise of Wasp Alpha a reality, so you can build what you envisioned and deploy your app to production. The second launch made the whole experience much more polished, getting closer to the DX you'd expect from a mature web framework.

Why Magic?

For this launch, with all the basics in place and you having built thousands of apps with Wasp (thank you!), we started pushing the boundaries of what web frameworks can do, utilising Wasp's unique DSL/compiler approach. This is still barely scratching the surface, but you'll be able to try it out yourself and get a taste of what the future of web development will look like.

Magic - LW3 in a nutshell
This launch week in a nutshell.

What's coming 🐝

Every day next week, starting Monday, June 26, we'll highlight a major new feature in Wasp. We'll update this post daily as we reveal each feature, so make sure to keep coming back! Follow us on twitter (@wasplang) to stay in the loop and also join our Discord to join the community and get help as you're trying Wasp out.

Launch party 🚀🎉

launch event 2 - screenshot
A bit of the atmosphere from our last launch party

What would a launch be without a proper event and a party? A boring, heartless event, that's what!

That's why we'll get together to celebrate the launch, our community (you!) and all the hard work that's been put into this new, fresh edition of Wasp. You will also get to meet the team and hear first-hand from the makers about the latest features and plans for the future.

The party starts at 9.30 am EDT / 3.30 pm CET - sign up here and make sure to mark yourself as "interested"!

launch event - how to join

As per usual, there will be memes, swag and lots of interesting dev discussions!

Auto CRUD | Monday: The future is now 🛸

The future is now

We'll immediately kick things off with a bang! What's the one thing that all developers universally agree is something they'd like to do less of? Writing boilerplate CRUD logic, of course! Yet, it's 2023 and the best we managed to do is get an AI write it half-correctly for us and we still have to maintain it.

That's what we are coming after - is it possible to avoid writing (or generating) CRUD code in the first place? How far can we take it and what's then even left for your to code? Join us on Monday and find out!

When: Monday, June 26 2023

Read more about it:

WebSocket Support | Tuesday: Be real, time 🔌⏱

Realtime

Sometimes, you just want to keep it real. Especially when you are dealing with time. I've been dropping some hints here - have you figured out what is this about? If yes, drop us a line on twitter (@wasplang) and the first one to get it right will get a special (real and timely) award!

Another situation where you might want to keep things real is when chatting to someone, especially via the text (wink wink hint hint 🧦).

When: Tuesday, June 27 2023

Read more about it:

Wednesday: Community Day 🤗

Community
Just let it all out

Community is at the centre of Wasp, and Wednesday is at the centre of the week, so it's only appropriate to marry the two together. We'll spotlight the amazing OSS tools Wasp is built on top of and also you - all the cool stuff you have built with Wasp and how you're contributing every day to make our community better!

When: Wednesday, June 28 2023

Read more about it: What can you build with Wasp?

Wasp LSP 2.0 | Thursday: Take care of your tools 🛠

Tools

It's a well known fact that a developer is only as good as the tools they are using. That actually applies to anybody - if Gimli hadn't spent time sharpening his axe, he wouldn't stand a chance against these orcs, would he?

Us at Wasp, we are pretty much the same as Gimli - we take our tools seriously. As we are innovating on the framework features, our goal is to do the same with the tooling you use with Wasp. Get ready to get your hands dirty (with code).

When: Thursday, June 29 2023

Read more about it: A blog post introing Wasp LSP 2.0

GPT Web App Generator | Friday: Waspularity 🤖 + Tutorial-o-thon!

Waspularity

For the final day of the launch week, we have a really cool surprise for you. I'll just say it's something like Matrix but the robots are your friends and there's no that weird guy with sunglasses to ruin everything. And there might be cake.

To wrap the week up, we'll also start another hackathon, but this time in a bit different format. Since the best way to learn something is to teach it to others, we'll focus on tutorials this time! May the best tutorial win - more info coming soon.

When: Friday, June 30 2023

Read more about it:

Recap

  • We are kicking off Launch Week #3 on Mon, June 26, at 9.30am EDT / 3.30pm CET - make sure to register for the event!
  • Launch Week #3 brings a ton of new exciting features - we’ll highlight one each day, starting Monday. Follow us on twitter and join our Discord to stay in the loop!
  • Following launch week, we’ll announce a tutorial-o-thon - get your writing gear ready!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Launch Week #3: Magic

· 6 min read
Matija Sosic

Launch Week 3 is coming

By now, it is a tradition. For the every upcoming launch week, we ask ourselves how can we top the last one? How can we make building full-stack web apps easier, more enjoyable and get rid of even more boilerplate?

If this is the first time you're joining, check our previous launches:

Our first launch week was about making the promise of Wasp Alpha a reality, so you can build what you envisioned and deploy your app to production. The second launch made the whole experience much more polished, getting closer to the DX you'd expect from a mature web framework.

Why Magic?

For this launch, with all the basics in place and you having built thousands of apps with Wasp (thank you!), we started pushing the boundaries of what web frameworks can do, utilising Wasp's unique DSL/compiler approach. This is still barely scratching the surface, but you'll be able to try it out yourself and get a taste of what the future of web development will look like.

Magic - LW3 in a nutshell
This launch week in a nutshell.

What's coming 🐝

Every day next week, starting Monday, June 26, we'll highlight a major new feature in Wasp. We'll update this post daily as we reveal each feature, so make sure to keep coming back! Follow us on twitter (@wasplang) to stay in the loop and also join our Discord to join the community and get help as you're trying Wasp out.

Launch party 🚀🎉

launch event 2 - screenshot
A bit of the atmosphere from our last launch party

What would a launch be without a proper event and a party? A boring, heartless event, that's what!

That's why we'll get together to celebrate the launch, our community (you!) and all the hard work that's been put into this new, fresh edition of Wasp. You will also get to meet the team and hear first-hand from the makers about the latest features and plans for the future.

The party starts at 9.30 am EDT / 3.30 pm CET - sign up here and make sure to mark yourself as "interested"!

launch event - how to join

As per usual, there will be memes, swag and lots of interesting dev discussions!

Auto CRUD | Monday: The future is now 🛸

The future is now

We'll immediately kick things off with a bang! What's the one thing that all developers universally agree is something they'd like to do less of? Writing boilerplate CRUD logic, of course! Yet, it's 2023 and the best we managed to do is get an AI write it half-correctly for us and we still have to maintain it.

That's what we are coming after - is it possible to avoid writing (or generating) CRUD code in the first place? How far can we take it and what's then even left for your to code? Join us on Monday and find out!

When: Monday, June 26 2023

Read more about it:

WebSocket Support | Tuesday: Be real, time 🔌⏱

Realtime

Sometimes, you just want to keep it real. Especially when you are dealing with time. I've been dropping some hints here - have you figured out what is this about? If yes, drop us a line on twitter (@wasplang) and the first one to get it right will get a special (real and timely) award!

Another situation where you might want to keep things real is when chatting to someone, especially via the text (wink wink hint hint 🧦).

When: Tuesday, June 27 2023

Read more about it:

Wednesday: Community Day 🤗

Community
Just let it all out

Community is at the centre of Wasp, and Wednesday is at the centre of the week, so it's only appropriate to marry the two together. We'll spotlight the amazing OSS tools Wasp is built on top of and also you - all the cool stuff you have built with Wasp and how you're contributing every day to make our community better!

When: Wednesday, June 28 2023

Read more about it: What can you build with Wasp?

Wasp LSP 2.0 | Thursday: Take care of your tools 🛠

Tools

It's a well known fact that a developer is only as good as the tools they are using. That actually applies to anybody - if Gimli hadn't spent time sharpening his axe, he wouldn't stand a chance against these orcs, would he?

Us at Wasp, we are pretty much the same as Gimli - we take our tools seriously. As we are innovating on the framework features, our goal is to do the same with the tooling you use with Wasp. Get ready to get your hands dirty (with code).

When: Thursday, June 29 2023

Read more about it: A blog post introing Wasp LSP 2.0

GPT Web App Generator | Friday: Waspularity 🤖 + Tutorial-o-thon!

Waspularity

For the final day of the launch week, we have a really cool surprise for you. I'll just say it's something like Matrix but the robots are your friends and there's no that weird guy with sunglasses to ruin everything. And there might be cake.

To wrap the week up, we'll also start another hackathon, but this time in a bit different format. Since the best way to learn something is to teach it to others, we'll focus on tutorials this time! May the best tutorial win - more info coming soon.

When: Friday, June 30 2023

Read more about it:

Recap

  • We are kicking off Launch Week #3 on Mon, June 26, at 9.30am EDT / 3.30pm CET - make sure to register for the event!
  • Launch Week #3 brings a ton of new exciting features - we’ll highlight one each day, starting Monday. Follow us on twitter and join our Discord to stay in the loop!
  • Following launch week, we’ll announce a tutorial-o-thon - get your writing gear ready!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/06/27/build-your-own-twitter-agent-langchain.html b/blog/2023/06/27/build-your-own-twitter-agent-langchain.html index 5ca1d920f6..de54cacf31 100644 --- a/blog/2023/06/27/build-your-own-twitter-agent-langchain.html +++ b/blog/2023/06/27/build-your-own-twitter-agent-langchain.html @@ -19,13 +19,13 @@ - - + +
-

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

· 46 min read
Vinny

TL;DR

LangChain, ChatGPT, and other emerging technology have made it possible to build some really creative tools.

In this tutorial, we’ll build a full-stack web app that acts as our own personal Twitter Agent, or “intern”, as I like to call it. It keeps track of your notes and ideas, and uses them — along with tweets from trending-setting twitter users — to brainstorm new ideas and write tweet drafts for you! 💥

BTW, If you get stuck during the tutorial, or at any point just want to check out the full, final repo of the app we're building, here it is: https://github.com/vincanger/twitter-intern

Before We Begin

Wasp = } is the only open-source, completely serverful fullstack React/Node framework with a built in compiler that lets you build your app in a day and deploy with a single CLI command.

We’re working hard to help you build performant web apps as easily as possibly — including making these tutorials, which are 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://media2.giphy.com/media/d0Pkp9OMIBdC0/giphy.gif?cid=7941fdc6b39mgj7h8orvi0f4bjebceyx4gj0ih1xb6s05ujc&ep=v1_gifs_search&rid=giphy.gif&ct=g

…even Ron would star Wasp on GitHub 🤩

Background

Twitter is a great marketing tool. It’s also a great way to explore ideas and refine your own. But it can be time-consuming and difficult to maintain a tweeting habit.

https://media0.giphy.com/media/WSrR5xkvljaFMe7UPo/giphy.gif?cid=7941fdc6g9o3drj567dbwyuo1c66x76eq8awc2r1oop8oypl&ep=v1_gifs_search&rid=giphy.gif&ct=g

That’s why I decided to build my own personal twitter agent with LangChain on the basis of these assumptions:

🧠 LLMs (like ChatGPT) aren’t the best writers, but they ARE great at brainstorming new ideas.

📊 Certain twitter users drive the majority of discourse within certain niches, i.e. trend-setters influence what’s being discussed at the moment.

💡 the Agent needs context in order to generate ideas relevant to YOU and your opinions, so it should have access to your notes, ideas, tweets, etc.

So instead of trying to build a fully autonomous agent that does the tweeting for you, I thought it would be better to build an agent that does the BRAINSTORMING for you, based on your favorite trend-setting twitter users as well as your own ideas.

Imagine it like an intern that does the grunt work, while you do the curating!

https://media.giphy.com/media/26DNdV3b6dqn1jzR6/giphy.gif

In order to accomplish this, we need to take advantage of a few hot AI tools:

  • Embeddings and Vector Databases
  • LLMs (Large Language Models), such as ChatGPT
  • LangChain and sequential “chains” of LLM calls

Embeddings and Vector Databases give us a powerful way to perform similarity searches on our own notes and ideas.

If you’re not familiar with similarity search, the simplest way to describe what similarity search is by comparing it to a normal google search. In a normal search, the phrase “a mouse eats cheese” will return results with a combination of those words only. But a vector-based similarity search, on the other hand, would return those words, as well as results with related words such as “dog”, “cat”, “bone”, and “fish”.

You can see why that’s so powerful, because if we have non-exact but related notes, our similarity search will still return them!

https://media2.giphy.com/media/xUySTD7evBn33BMq3K/giphy.gif?cid=7941fdc6273if8qfk83gbnv8uabc4occ0tnyzk0g0gfh0qg5&ep=v1_gifs_search&rid=giphy.gif&ct=g

For example, if our favorite trend-setting twitter user makes a post about the benefits of typescript, but we only have a note on “our favorite React hooks”, our similarity search would still likely return such a result. And that’s huge!

Once we get those notes, we can pass them to the ChatGPT completion API along with a prompt to generate more ideas. The result from this prompt will then be sent to another prompt with instructions to generate a draft tweet. We save these sweet results to our Postgres relational database.

This “chain” of prompting is essentially where the LangChain package gets its name 🙂

The flow of information through the app

This approach will give us a wealth of new ideas and tweet drafts related to our favorite trend-setting twitter users’ tweets. We can look through these, edit and save our favorite ideas to our “notes” vector store, or maybe send off some tweets.

I’ve personally been using this app for a while now, and not only has it generated some great ideas, but it also helps to inspire new ones (even if some of the ideas it generates are “meh”), which is why I included an “Add Note” feature front and center to the nav bar

twitter-agent-add-note.png

Ok. Enough background. Let’s start building your own personal twitter intern! 🤖

BTW, if you get stuck at all while following the tutorial, you can always reference this tutorial’s repo, which has the finished app: Twitter Intern GitHub Repo

Configuration

Set up your Wasp project

We’re going to make this a full-stack React/NodeJS web app so we need to get that set up first. But don’t worry, it won’t take long AT ALL, because we will be using Wasp as the framework.

Wasp does all the heavy lifting for us. You’ll see what I mean in a second.

# First, install Wasp by running this in your terminal:

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

# next, create a new project:

wasp new twitter-agent

# cd into the new directory and start the project:

cd twitter-agent && wasp start

Great! When running wasp start, Wasp will install all the necessary npm packages, start our server on port 3001, and our React client on port 3000. Head to localhost:3000 in your browser to check it out.

Untitled

Tip ℹ️

you can install the Wasp vscode extension for the best developer experience.

You’ll notice Wasp sets up your full-stack app with a file structure like so:

.
├── main.wasp # The wasp config file.
└── src
   ├── client # Your React client code (JS/CSS/HTML) goes here.
   ├── server # Your server code (Node JS) goes here.
   └── shared # Your shared (runtime independent) code goes here.

Let’s start adding some server-side code.

Server-Side & Database Entities

Start by adding a .env.server file in the root directory of your project:

# https://platform.openai.com/account/api-keys
OPENAI_API_KEY=

# sign up for a free tier account at https://www.pinecone.io/
PINECONE_API_KEY=
# will be a location, e.g 'us-west4-gcp-free'
PINECONE_ENV=

# We will fill these in later during the Twitter Scraping section
# Twitter details -- only needed once for Rettiwt.account.login() to get the tokens
TWITTER_EMAIL=
TWITTER_HANDLE=
TWITTER_PASSWORD=

# TOKENS -- fill these in after running the getTwitterTokens script in the Twitter Scraping section
KDT=
TWID=
CT0=
AUTH_TOKEN=

We need a way for us to store all our great ideas. So let’s first head to Pinecone.io and set up a free trial account.

Untitled

In the Pinecone dashboard, go to API keys and create a new one. Copy and paste your Environment and API Key into .env.server

Do the same for OpenAI, by creating an account and key at https://platform.openai.com/account/api-keys

Now let’s replace the contents of the main.wasp config file, which is like the “skeleton” of your app, with the code below. This will configure most of the fullstack app for you 🤯

app twitterAgent {
wasp: {
version: "^0.10.6"
},
title: "twitter-agent",
head: [
"<script async src='https://platform.twitter.com/widgets.js' charset='utf-8'></script>"
],
db: {
system: PostgreSQL,
},
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
methods: {
usernameAndPassword: {},
}
},
dependencies: [
("openai", "3.2.1"),
("rettiwt-api", "1.1.8"),
("langchain", "0.0.91"),
("@pinecone-database/pinecone", "0.1.6"),
("@headlessui/react", "1.7.15"),
("react-icons", "4.8.0"),
("react-twitter-embed", "4.0.4")
],
}

// ### Database Models

entity Tweet {=psl
id Int @id @default(autoincrement())
tweetId String
authorUsername String
content String
tweetedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
drafts TweetDraft[]
ideas GeneratedIdea[]
psl=}

entity TweetDraft {=psl
id Int @id @default(autoincrement())
content String
notes String
originalTweet Tweet @relation(fields: [originalTweetId], references: [id])
originalTweetId Int
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
psl=}

entity GeneratedIdea {=psl
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
originalTweet Tweet? @relation(fields: [originalTweetId], references: [id])
originalTweetId Int?
isEmbedded Boolean @default(false)
psl=}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
createdAt DateTime @default(now())
favUsers String[]
originalTweets Tweet[]
tweetDrafts TweetDraft[]
generatedIdeas GeneratedIdea[]
psl=}

// <<< Client Pages & Routes

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

//...
note

You might have noticed this {=psl psl=} syntax in the entities above. This denotes that anything in between these psl brackets is actually a different language, in this case, Prisma Schema Language. Wasp uses Prisma under the hood, so if you've used Prisma before, it should be straightforward.

As you can see, our main.wasp config file has our:

  • dependencies,
  • authentication method,
  • database type, and
  • database models (”entities”)

With this, our app structure is mostly defined and Wasp will take care of a ton of configuration for us.

Database Setup

But we still need to get a postgres database running. Usually this can be pretty annoying, but with Wasp, just have Docker Deskop installed and running, then open up another separate terminal tab/window and then run:

wasp start db

This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 Just leave this terminal tab, along with docker desktop, open and running in the background.

In a different terminal tab, run:

wasp db migrate-dev

and make sure to give your database migration a name.

If you stopped the wasp dev server to run this command, go ahead and start it again with wasp start.

At this point, our app will be navigating us to localhost:3000/login but because we haven’t implemented a login screen/flow yet, we will be seeing a blank screen. Don’t worry, we’ll get to that.

Embedding Ideas & Notes

Server Action

First though, in the main.wasp config file, let’s define a server action for saving notes and ideas. Go ahead and add the code below to the bottom of the file:

// main.wasp

//...
// <<< Client Pages & Routes

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

// !!! Actions

action embedIdea {
fn: import { embedIdea } from "@server/ideas.js",
entities: [GeneratedIdea]
}

With the action declared, let’s create it. Make a new file, .src/server/ideas.ts in and add the following code:

import type { EmbedIdea } from '@wasp/actions/types';
import type { GeneratedIdea } from '@wasp/entities';
import HttpError from '@wasp/core/HttpError.js';
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { Document } from 'langchain/document';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { PineconeClient } from '@pinecone-database/pinecone';

const pinecone = new PineconeClient();
export const initPinecone = async () => {
await pinecone.init({
environment: process.env.PINECONE_ENV!,
apiKey: process.env.PINECONE_API_KEY!,
});
return pinecone;
};

export const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});

/**
* Embeds a single idea into the vector store
*/
export const embedIdea: EmbedIdea<{ idea: string }, GeneratedIdea> = async ({ idea }, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

console.log('idea: ', idea);

try {
let newIdea = await context.entities.GeneratedIdea.create({
data: {
content: idea,
userId: context.user.id,
},
});


if (!newIdea) {
throw new HttpError(404, 'Idea not found');
}

const pinecone = await initPinecone();

// we need to create an index to save the vector embeddings to
// an index is similar to a table in relational database world
const availableIndexes = await pinecone.listIndexes();
if (!availableIndexes.includes('embeds-test')) {
console.log('creating index');
await pinecone.createIndex({
createRequest: {
name: 'embeds-test',
// open ai uses 1536 dimensions for their embeddings
dimension: 1536,
},
});
}

const pineconeIndex = pinecone.Index('embeds-test');

// the LangChain vectorStore wrapper
const vectorStore = new PineconeStore(embeddings, {
pineconeIndex: pineconeIndex,
namespace: context.user.username,
});

// create a document with the idea's content to be embedded
const ideaDoc = new Document({
metadata: { type: 'note' },
pageContent: newIdea.content,
});

// add the document to the vectore store along with its id
await vectorStore.addDocuments([ideaDoc], [newIdea.id.toString()]);

newIdea = await context.entities.GeneratedIdea.update({
where: {
id: newIdea.id,
},
data: {
isEmbedded: true,
},
});
console.log('idea embedded successfully!', newIdea);
return newIdea;
} catch (error: any) {
throw new Error(error);
}
};
info

We’ve defined the action function in our main.wasp file as coming from ‘@server/ideas.js’ but we’re creating an ideas.ts file. What's up with that?!

Well, Wasp internally uses esnext module resolution, which always requires specifying the extension as .js (i.e., the extension used in the emitted JS file). This applies to all @server imports (and files on the server in general). It does not apply to client files.

Great! Now we have a server action for adding notes and ideas to our vector database. And we didn’t even have to configure a server ourselves (thanks, Wasp 🙂).

Let's take a step back and walk through the code we just wrote though:

  1. We create a new Pinecone client and initialize it with our API key and environment.
  2. We create a new OpenAIEmbeddings client and initialize it with our OpenAI API key.
  3. We create a new index in our Pinecone database to store our vector embeddings.
  4. We create a new PineconeStore, which is a LangChain wrapper around our Pinecone client and our OpenAIEmbeddings client.
  5. We create a new Document with the idea’s content to be embedded.
  6. We add the document to the vector store along with its id.
  7. We also update the idea in our Postgres database to mark it as embedded.

Now we want to create the client-side functionality for adding ideas, but you’ll remember we defined an auth object in our wasp config file. So we’ll need to add the ability to log in before we do anything on the frontend.

Authentication

Let’s add that quickly by adding a new a Route and Page definition to our main.wasp file

//...

route LoginPageRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/LoginPage"
}

…and create the file src/client/LoginPage.tsx with the following content:

import { LoginForm } from '@wasp/auth/forms/Login';
import { SignupForm } from '@wasp/auth/forms/Signup';
import { useState } from 'react';

export default () => {
const [showSignupForm, setShowSignupForm] = useState(false);

const handleShowSignupForm = () => {
setShowSignupForm((x) => !x);
};

return (
<>
{showSignupForm ? <SignupForm /> : <LoginForm />}
<div onClick={handleShowSignupForm} className='underline cursor-pointer hover:opacity-80'>
{showSignupForm ? 'Already Registered? Login!' : 'No Account? Sign up!'}
</div>
</>
);
};
info

In the auth object on the main.wasp file, we used the usernameAndPassword method which is the simplest form of auth Wasp offers. If you’re interested, Wasp does provide abstractions for Google, Github, and Email Verified Authentication, but we will stick with the simplest auth for this tutorial.

With authentication all set up, if we try to go to localhost:3000 we will be automatically directed to the login/register form.

You’ll see that Wasp creates Login and Signup forms for us because of the auth object we defined in the main.wasp file. Sweet! 🎉

But even though we’ve added some style classes, we haven’t set up any css styling so it will probably be pretty ugly right about now.

🤢 Barf.

Untitled

Adding Tailwind CSS

Luckily, Wasp comes with tailwind css support, so all we have to do to get that working is add the following files in the root directory of the project:

.
├── main.wasp
├── src
│ ├── client
│ ├── server
│ └── shared
├── postcss.config.cjs # add this file here
├── tailwind.config.cjs # and this here too
└── .wasproot

postcss.config.cjs

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

tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

Finally, replace the contents of your src/client/Main.css file with these lines:

@tailwind base;
@tailwind components;
@tailwind utilities;

Now we’ve got the magic of tailwind css on our sides! 🎨 We’ll get to styling later though. Patience, young grasshopper.

Adding Notes Client-side

From here, let’s create the complimentary client-side components for adding notes to the vector store. Create a new .src/client/AddNote.tsx file with the following contents:

import { useState } from 'react';
import embedIdea from '@wasp/actions/embedIdea';

export default function AddNote() {
const [idea, setIdea] = useState('');
const [isIdeaEmbedding, setIsIdeaEmbedding] = useState(false);

const handleEmbedIdea = async (e: any) => {
try {
setIsIdeaEmbedding(true);
if (!idea) {
throw new Error('Idea cannot be empty');
}
const embedIdeaResponse = await embedIdea({
idea,
});

console.log('embedIdeaResponse: ', embedIdeaResponse);
} catch (error: any) {
alert(error.message);
} finally {
setIdea('');
setIsIdeaEmbedding(false);
}
};

return (
<div className='flex flex-row gap-2 justify-center items-end w-full'>
<textarea
autoFocus
onChange={(e) => setIdea(e.target.value)}
value={idea}
placeholder='LLMs are great for brainstorming!'
className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'
/>
<button
onClick={handleEmbedIdea}
className='flex flex-row justify-center items-center bg-neutral-100 hover:bg-neutral-200 border border-neutral-300 font-bold px-3 py-1 text-sm text-blue-500 whitespace-nowrap rounded-lg'
>
{isIdeaEmbedding ? 'Loading...' : 'Save Note'}
</button>
</div>
);
}

Here we’re using the embedIdea action we defined earlier to add our idea to the vector store. We’re also using the useState hook to keep track of the idea we’re adding, as well as the loading state of the button.

So now we have a way to add our own ideas and notes to our vector store. Pretty sweet!

Generating New Ideas & Tweet Drafts

Using LangChain's Sequential Chains

Now we need to set up the sequential chain of LLM calls that LangChain is so great at.

Here are the steps we will take:

  1. define a function that uses LangChain to initiate a “chain” of API calls to OpenAI’s ChatGPT completions endpoint.
    1. this function takes a tweet that we pulled from one of our favorite twitter users as an argument, searches our vector store for similar notes & ideas, and returns a list of new “brainstormed” based on the example tweet and our notes.
  2. define a new action that loops through our favorite users array, pulls their most recent tweets, and sends them to our LangChain function mentioned above

So let’s start again by creating our LangChain function. Make a new src/server/chain.ts file:

import { ChatOpenAI } from 'langchain/chat_models/openai';
import { LLMChain, SequentialChain } from 'langchain/chains';
import { PromptTemplate } from 'langchain/prompts';
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { PineconeClient } from '@pinecone-database/pinecone';

const pinecone = new PineconeClient();
export const initPinecone = async () => {
await pinecone.init({
environment: process.env.PINECONE_ENV!,
apiKey: process.env.PINECONE_API_KEY!,
});
return pinecone;
};

const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});

export const generateIdeas = async (exampleTweet: string, username: string) => {
try {
// remove quotes and curly braces as not to confuse langchain template parser
exampleTweet = exampleTweet.replace(/"/g, '');
exampleTweet = exampleTweet.replace(/{/g, '');
exampleTweet = exampleTweet.replace(/}/g, '');

const pinecone = await initPinecone();

console.log('list indexes', await pinecone.listIndexes());

// find the index we created earlier
const pineconeIndex = pinecone.Index('embeds-test');

const vectorStore = new PineconeStore(embeddings, {
pineconeIndex: pineconeIndex,
namespace: username,
});

//
// sequential tweet chain begin --- >
//
/**
* vector store results for notes similar to the original tweet
*/
const searchRes = await vectorStore.similaritySearchWithScore(exampleTweet, 2);
console.log('searchRes: ', searchRes);
let notes = searchRes
.filter((res) => res[1] > 0.7) // filter out strings that have less than %70 similarity
.map((res) => res[0].pageContent)
.join(' ');

console.log('\n\n similarity search results of our notes-> ', notes);

if (!notes || notes.length <= 2) {
notes = exampleTweet;
}

const tweetLlm = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
temperature: 0.8, // 0 - 2 with 0 being more deterministic and 2 being most "loose". Past 1.3 the results tend to be more incoherent.
modelName: 'gpt-3.5-turbo',
});

const tweetTemplate = `You are an expert idea generator. You will be given a user's notes and your goal is to use this information to brainstorm other novel ideas.

Notes: {notes}

Ideas Brainstorm:
-`;

const tweetPromptTemplate = new PromptTemplate({
template: tweetTemplate,
inputVariables: ['notes'],
});

const tweetChain = new LLMChain({
llm: tweetLlm,
prompt: tweetPromptTemplate,
outputKey: 'newTweetIdeas',
});

const interestingTweetTemplate = `You are an expert interesting tweet generator. You will be given some tweet ideas and your goal is to choose one, and write a tweet based on it. Structure the tweet in an informal yet serious tone and do NOT include hashtags in the tweet!

Tweet Ideas: {newTweetIdeas}

Interesting Tweet:`;

const interestingTweetLlm = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
temperature: 1.1,
modelName: 'gpt-3.5-turbo',
});

const interestingTweetPrompt = new PromptTemplate({
template: interestingTweetTemplate,
inputVariables: ['newTweetIdeas'],
});

const interestingTweetChain = new LLMChain({
llm: interestingTweetLlm,
prompt: interestingTweetPrompt,
outputKey: 'interestingTweet',
});

const overallChain = new SequentialChain({
chains: [tweetChain, interestingTweetChain],
inputVariables: ['notes'],
outputVariables: ['newTweetIdeas', 'interestingTweet'],
verbose: false,
});

type ChainDraftResponse = {
newTweetIdeas: string;
interestingTweet: string;
notes: string;
};

const res1 = (await overallChain.call({
notes,
})) as ChainDraftResponse;

return {
...res1,
notes,
};
} catch (error: any) {
throw new Error(error);
}
};

Great! Let's run through the above code real quick:

  1. Initialize the Pinecone client
  2. Find our pinecone index (i.e. table) that we created earlier and initialize a new PineconeStore with LangChain
  3. Search our vector store for notes similar to the example tweet, filtering out any results that have less than %70 similarity
  4. Create a new ChatGPT completion chain that takes our notes as input and generates new tweet ideas
  5. Create a new ChatGPT completion chain that takes the new tweet ideas as input and generates a new tweet draft
  6. Create a new SequentialChain and combine the above two chains together so that we can pass it our notes as input and it returns the new tweet ideas and the new tweet draft as output
VECTOR COSINE SIMILARITY SCORES

A good similarity threshold for cosine similarity search on text strings depends on the specific application and the desired level of strictness in matching. Cosine similarity scores range between 0 and 1, with 0 meaning no similarity and 1 meaning completely identical text strings.

  • 0.8-0.9 = strict
  • 0.6-0.8 = moderate
  • 0.5 = relaxed.

In our case, we went for a moderate similarity threshold of 0.7, which means that we will only return notes that are at least 70% similar to the example tweet.

With this function, we will get our newTweetIdeas and our interestingTweet draft back as results that we can use within our server-side action.

Scraping Twitter

Before we can pass an exampleTweet as an argument to our newly created Sequential Chain, we need to fetch it first!

To do this, we're going to use the Rettiwt-Api (which is just Twitter written backwards). Because it's an unofficial API there are a few caveats:

  1. We have to use the rettiwt client to login to our twitter account once. We will output the tokens it returns via a script and save those in our .env.server file for later.
  2. It's best to use an alternative account for this process. If you don't have an alternative account, go ahead and register a new one now.
⚠️

The use of an unofficial Twitter client, Rettiwt, is for illustrative purposes only. It's crucial that you familiarize yourself with Twitter's policies and rules regarding scraping before implementing these methods. Any abuse or misuse of these scripts and techniques may lead to actions taken against your Twitter account. We hold no responsibility for any consequences arising from your personal use of this tutorial and/or the related scripts. It is intended purely for learning and educational purposes.

Let's go ahead and create a new folder in src/server called scripts with a file inside called tokens.ts. This will be our script that we will run only once, just so that we get the necessary tokens to pass to our Rettiwt client.

We want to avoid running this script many times otherwise our account could get rate-limited. This shouldn't be an issue though, because once we return the tokens, they are valid for up to a year.

So inside src/server/scripts/tokens.ts add the following code:

import { Rettiwt } from 'rettiwt-api'; 

/**
* This is a script we can now run from the cli with `wasp db seed`
* IMPORTANT! We only want to run this script once, after which we save the tokens
* in the .env.server file. They should be good for up to a year.
*/
export const getTwitterTokens = async () => {
const tokens = await Rettiwt().account.login(
process.env.TWITTER_EMAIL!,
process.env.TWITTER_HANDLE!,
process.env.TWITTER_PASSWORD!
);

console.log('tokens: ', tokens)
};

Make sure to add your twitter login details to our .env.server file, if you haven't already!

Great. To be able to run this script via a simple Wasp CLI command, add it via the seeds array within the db object at the top of your main.wasp file:

app twitterAgent {
wasp: {
version: "^0.10.6"
},
//...
db: {
system: PostgreSQL,
seeds: [ // <---------- add this
import { getTwitterTokens } from "@server/scripts/tokens.js",
]
},
//...

Nice! Now for the fun part :)

in your terminal, at the root of your project, run wasp db seed, and you should see the tokens output to the terminal similar to this:

[Db]      Running seed: getTwitterTokens
[Db] tokens: { // your tokens... }

Copy and paste those tokens into your .env.server file:


# TOKENS -- fill these in after running the getTwitterTokens script in the Twitter Scraping section
KDT='...'
TWID='...'
CT0='...'
AUTH_TOKEN='...'

Now with that, we should be able to access our favorite trend-setting users' recent tweets and use them to help us brainstorm new ideas!

Server Action

Ok, so we've got the tokens we need to get our trend-setting example tweets, and we've got a function that runs our similarity search and sequential chain of LLM calls.

Now let’s define an action in our main.wasp file that pulls it all together:

// actions...

action generateNewIdeas {
fn: import { generateNewIdeas } from "@server/ideas.js",
entities: [GeneratedIdea, Tweet, TweetDraft, User]
}

…and then create that action within src/server/ideas.ts


import type {
EmbedIdea,
GenerateNewIdeas // < ---- add this type here -----
} from '@wasp/actions/types';
// ... other imports ...
import { generateIdeas } from './chain.js'; // < ---- this too -----
import { Rettiwt } from 'rettiwt-api'; // < ---- and this here -----

const twitter = Rettiwt({ // < ---- and this -----
kdt: process.env.KDT!,
twid: process.env.TWID!,
ct0: process.env.CT0!,
auth_token: process.env.AUTH_TOKEN!,
});

//... other stuff ...

export const generateNewIdeas: GenerateNewIdeas<unknown, void> = async (_args, context) => {
try {
// get the logged in user that Wasp passes to the action via the context
const user = context.user

if (!user) {
throw new HttpError(401, 'User is not authorized');
}

for (let h = 0; h < user.favUsers.length; h++) {
const favUser = user.favUsers[h];
const oneDayFromNow = new Date(Date.now() + 24 * 60 * 60 * 1000);
// convert oneDayFromNow to format YYYY-MM-DD
const endDate = oneDayFromNow.toISOString().split('T')[0];

// find the most recent tweet from the favUser
const mostRecentTweet = await context.entities.Tweet.findFirst({
where: {
authorUsername: favUser,
},
orderBy: {
tweetedAt: 'desc',
},
});

console.log('mostRecentTweet: ', mostRecentTweet)

const favUserTweets = await twitter.tweets.getTweets({
fromUsers: [favUser],
sinceId: mostRecentTweet?.tweetId || undefined, // get tweets since the most recent tweet if it exists
endDate: endDate, // endDate in format YYYY-MM-DD
});

const favUserTweetTexts = favUserTweets.list

for (let i = 0; i < favUserTweetTexts.length; i++) {
const tweet = favUserTweetTexts[i];

const existingTweet = await context.entities.User.findFirst({
where: {
id: user.id,
},
select: {
originalTweets: {
where: {
tweetId: tweet.id,
},
},
},
});

/**
* If the tweet already exists in the database, skip generating drafts and ideas for it.
*/
if (existingTweet) {
console.log('tweet already exists in db, skipping generating drafts...');
continue;
}

/**
* this is where the magic happens
*/
const draft = await generateIdeas(tweet.fullText, user.username);
console.log('draft: ', draft);

const originalTweet = await context.entities.Tweet.create({
data: {
tweetId: tweet.id,
content: tweet.fullText,
authorUsername: favUser,
tweetedAt: new Date(tweet.createdAt),
userId: user.id
},
});

let newTweetIdeas = draft.newTweetIdeas.split('\n');
newTweetIdeas = newTweetIdeas
.filter((idea) => idea.trim().length > 0)
.map((idea) => {
// remove all dashes that are not directly followed by a letter
idea = idea.replace(/-(?![a-zA-Z])/g, '');
idea = idea.replace(/"/g, '');
idea = idea.replace(/{/g, '');
idea = idea.replace(/}/g, '');
// remove hashtags and the words that follow them
idea = idea.replace(/#[a-zA-Z0-9]+/g, '');
idea = idea.replace(/^\s*[\r\n]/gm, ''); // remove new line breaks
idea = idea.trim();
// check if last character contains punctuation and if not add a period
if (idea.length > 1 && !idea[idea.length - 1].match(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g)) {
idea += '.';
}
return idea;
});
for (let j = 0; j < newTweetIdeas.length; j++) {
const newTweetIdea = newTweetIdeas[j];
const newIdea = await context.entities.GeneratedIdea.create({
data: {
content: newTweetIdea,
originalTweetId: originalTweet.id,
userId: user.id
},
});
console.log('newIdea saved to DB: ', newIdea);
}

const interestingTweetDraft = await context.entities.TweetDraft.create({
data: {
content: draft.interestingTweet,
originalTweetId: originalTweet.id,
notes: draft.notes,
userId: user.id
},
});

console.log('interestingTweetDraft saved to DB: ', interestingTweetDraft);

// create a delay to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, 1000));

}
await new Promise((resolve) => setTimeout(resolve, 1000));
}

} catch (error: any) {
console.log('error', error);
throw new HttpError(500, error);
}
}

Ok! Nice work. There’s a lot going on above, so let’s just recap:

  • We loop through the array of our favorite users, as defined on our user entity in main.wasp,
  • Pull that user’s most recent tweets
  • Send that tweet to our generateIdeas function, which
    • searches our vector store for similar notes
    • asks GPT to generate similar, new ideas
    • sends those ideas in another prompt GPT to create a new, interesting tweet
    • returns the new ideas and interesting tweet
  • Create new GeneratedIdeas and a TweetDraft and saves them to our Postgres DB

Phew! We’re doing it 💪 

Fetching & Displaying Ideas

Defining a Server-side Query

Since we now have our chain of GPT prompts defined via LangChain and our server-side action, let’s go ahead and start implementing some front-end logic to fetch that data and display it to our users… which is basically only us at this point 🫂.

Just as we added a server-side action to generateNewIdeas we will now define a query to fetch those ideas.

Add the following query to your main.wasp file:

query getTweetDraftsWithIdeas {
fn: import { getTweetDraftsWithIdeas } from "@server/ideas.js",
entities: [TweetDraft]
}

In your src/server/ideas.ts file, below your generateNewIdeas action, add the query we just defined in our wasp file:

//... other imports ...
import type { GetTweetDraftsWithIdeas } from '@wasp/queries/types'; // <--- add this ---

// ... other functions ...

type TweetDraftsWithIdeas = {
id: number;
content: string;
notes: string;
createdAt: Date;
originalTweet: {
id: number;
content: string;
tweetId: string;
tweetedAt: Date;
ideas: GeneratedIdea[];
authorUsername: string;
};
}[];

export const getTweetDraftsWithIdeas: GetTweetDraftsWithIdeas<unknown, TweetDraftsWithIdeas> = async (_args, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

const drafts = await context.entities.TweetDraft.findMany({
orderBy: {
originalTweet: {
tweetedAt: 'desc',
}
},
where: {
userId: context.user.id,
createdAt: {
gte: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // Get drafts created within the last 2 days
},
},
select: {
id: true,
content: true,
notes: true,
createdAt: true,
originalTweet: {
select: {
id: true,
tweetId: true,
content: true,
ideas: true,
tweetedAt: true,
authorUsername: true,
},
},
},
});

return drafts;
};

With this function we will be returning the tweet drafts we generate, along with our notes, the original tweet that inspired it, and the newly generated ideas.

Sweet!

Ok, but what good is a function that fetches the data if we’ve got nowhere to display it!?

Displaying Ideas Client-side

Let’s go now to our src/client/MainPage.tsx file (make sure it’s got the .tsx extension and not .jsx) and replace the contents with these below:

import waspLogo from './waspLogo.png'
import './Main.css'

const MainPage = () => {
return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col justify-center items-center mx-auto pt-12'>
<img src={waspLogo} className='w-5' />
</div>
</div>
)
}
export default MainPage

At this point, you. might need to restart the wasp dev server running in your terminal to get the tailwind configuration to take effect (ctrl + c, then wasp start again).

You’ll now be prompted with the login / register screen. Go ahead and click on register and you will be automatically logged in and redirected to the main page, which at this point only has this:

Untitled

Let’s go back to our MainPage.tsx file and add the magic!

https://media3.giphy.com/media/ekv45izCuyXkXoHRaL/giphy.gif?cid=7941fdc6c3dszwj4xaoxg2kyj6xxdubjxn69m4qruhomhkut&ep=v1_gifs_search&rid=giphy.gif&ct=g

First, let’s create a buttons component so we don’t have to constantly style a new button. Create a new src/client/Button.tsx file:

import { ButtonHTMLAttributes } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
isLoading?: boolean;
}

export default function Button({ isLoading, children, ...otherProps }: ButtonProps) {
return (
<button
{...otherProps}
className={`flex flex-row justify-center items-center bg-neutral-100 hover:bg-neutral-200 border border-neutral-300 text-blue-500 font-bold px-3 py-1 text-sm rounded-lg ${isLoading ? ' pointer-events-none opacity-70' : 'cursor-pointer'}`}
>
{isLoading? 'Loading...' : children}
</button>
);
}

Now let’s add it to your AddNote.tsx component, replacing the original button with this one. The whole file should look like this:

import { useState } from 'react';
import embedIdea from '@wasp/actions/embedIdea';
import Button from './Button';

export default function AddNote() {
const [idea, setIdea] = useState('');
const [isIdeaEmbedding, setIsIdeaEmbedding] = useState(false);

const handleEmbedIdea = async (e: any) => {
try {
setIsIdeaEmbedding(true);
if (!idea) {
throw new Error('Idea cannot be empty');
}
const embedIdeaResponse = await embedIdea({
idea,
});

console.log('embedIdeaResponse: ', embedIdeaResponse);
} catch (error: any) {
alert(error.message);
} finally {
setIdea('');
setIsIdeaEmbedding(false);
}
};

return (
<div className='flex flex-row gap-2 justify-center items-end w-full'>
<textarea
autoFocus
onChange={(e) => setIdea(e.target.value)}
value={idea}
placeholder='LLMs are great for brainstorming!'
className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'
/>
<Button isLoading={isIdeaEmbedding} onClick={handleEmbedIdea}>
Save Note
</Button>
</div>
);
}

Noice.

Next, we want our page to perform the following actions:

  1. create a button that runs our generateNewIdeas action when clicked
  2. define the query that fetches and caches the tweet drafts and ideas
  3. loop through the results and display them on the page

That’s exactly what the below code will do. Go ahead and replace the MainPage with it and take a minute to review what’s going on:

import waspLogo from './waspLogo.png';
import './Main.css';
import { useState } from 'react';
import generateNewIdeas from '@wasp/actions/generateNewIdeas';
import { useQuery } from '@wasp/queries';
import getTweetDraftsWithIdeas from '@wasp/queries/getTweetDraftsWithIdeas';
import AddNote from './AddNote';
import Button from './Button';

const MainPage = () => {
const [isGenerating, setIsGenerating] = useState(false);

const {
data: tweetDrafts,
isLoading: isTweetDraftsLoading,
error: tweetDraftsError,
} = useQuery(getTweetDraftsWithIdeas);

const handleNewIdeas = async (e: any) => {
try {
setIsGenerating(true);
await generateNewIdeas();
} catch (error: any) {
alert(error.message);
} finally {
setIsGenerating(false);
}
};

if (isTweetDraftsLoading) {
return 'Loading...';
}

if (tweetDraftsError) {
return 'Error: ' + tweetDraftsError.message;
}

return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col gap-6 justify-center items-center mx-auto pt-12'>
<img src={waspLogo} className='w-5' />
<div className='flex flex-col gap-4 justify-center items-center w-2/4'>
<AddNote />
<hr className='border border-t-1 border-neutral-100/70 w-full' />
<div className='flex flex-row justify-center w-1/4'>
<Button onClick={handleNewIdeas} isLoading={isGenerating}>
Generate New Ideas
</Button>
</div>
<div className='flex flex-col gap-4 justify-center items-center w-full'>
{tweetDrafts.map((tweetDraft) => (
<>
<h2 className='text-2xl font-bold'>Generated Ideas</h2>
<div key={tweetDraft.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Tweet Draft</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{tweetDraft.content}</div>
</div>

{!!tweetDraft.notes && tweetDraft.notes !== tweetDraft.originalTweet.content && (
<>
<h2>Your Similar Notes</h2>
{tweetDraft.notes}
</>
)}
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Ideas</h2>
{tweetDraft.originalTweet.ideas.map((idea) => (
<div key={idea.id} className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'>{idea.content}</div>
</div>
</div>
))}
</div>
</div>
</>
))}
</div>
</div>
</div>
</div>
);
};
export default MainPage;

This is what you should see on the homepage now! 🎉

Untitled

But, if you clicked ‘generate new ideas’ and nothing happened, well that’s because we haven’t defined any favorite trend-setting twitter users to scrape tweets from. And there’s no way to do that from the UI at the moment, so let’s open up the database manager and add some manually.

In a new terminal tab, in the root of your project, run:

wasp db studio

Then, in a new browswer tab, at localhost:5555 you should see your database.

Go to user, and you should be the only user in there. Add the usernames of a couple of your favorite trend-setting twitter users.

Untitled

Make sure the accounts have tweeted recently or your function won’t be able to scrape or generate anything!

Hey ✋

While you’re at it, if you’re liking this tutorial, give me a follow @hot_town for more future content like this

After adding the twitter usernames, make sure you click save 1 change.

Go back to your client and click the Generate New Ideas button again. This might take a while depending on how many tweets it’s generating ideas for, so be patient — and watch the console output in your terminal if you’re curious ;)

Untitled

Awesome! Now we should be getting back some generated ideas from our twitter “intern” which will help us brainstorm further notes and generate our own BANGER TWEETS.

But it would be cool to also display the tweet these ideas are referencing from the beginning. That way we’d have a bit more context on where the ideas came from.

Let’s do that then! In your MainPage file, at the very top, add the following import:

import { TwitterTweetEmbed } from 'react-twitter-embed';

This allows us to embed tweets with that nice twitter styling.

We already added this dependency to our main.wasp file at the beginning of the tutorial, so we can just import and start embedding tweets.

Let’s try it out now in our MainPage by adding the following snippet above our <h2>Tweet Draft</h2> element:

//...

<h2>Original Tweet</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<TwitterTweetEmbed tweetId={tweetDraft.originalTweet.tweetId} />
</div>

<h2>Tweet Draft</h2>
//...

Great. Now we should be sitting pretty 😻

Untitled

You might remember from the beginning of the tutorial when we defined the LLM calls, that if your vector store notes don’t turn back a cosine similarity of at least 0.7, your agent will generate its own ideas entirely without using your notes as a guide.

And since we have NO notes in our vector store at the moment, that’s exactly what it is doing. Which is fine, because we can let it brainstorm for us, and we can select our favorite notes and edit and add them as we see fit.

So you can go ahead and start adding notes whenever you feel like it 📝.

But, we’ve added our favorite twitter users to the database manually. It would be preferable to do it via an account settings page, right? Let’s make one then.

Creating an Account Settings Page

First, add the route and page to your main.wasp config file, under the other routes:

//...

route AccountRoute { path: "/account", to: AccountPage }
page AccountPage {
authRequired: true,
component: import Account from "@client/AccountPage"
}

Next, let’s create a new page, src/client/AccountPage.tsx:

import Button from './Button';
import { ChangeEvent, useEffect, useState } from 'react';
import logout from '@wasp/auth/logout';
import type { User } from '@wasp/entities';

const AccountPage = ({ user }: { user: User }) => {
return (
<div className='flex flex-col justify-center items-center mt-12 w-full'>
<div className='flex flex-col items-center justify-center gap-4 border border-neutral-700 bg-neutral-100/40 rounded-xl p-1 sm:p-4 w-full'>
<div className='flex flex-row justify-end w-full px-4 pt-2'>
<Button onClick={logout}>Logout</Button>
</div>
{JSON.stringify(user, null, 2)}
</div>
</div>
);
};

export default AccountPage;

When you navigate to localhost:3000/account, you’ll notice two things, one of them being a logout button. You can see in our SettingsPage above that we imported a Wasp-provided logout function. We get this “for free” since we defined our auth strategy in the main.wasp file — a big time-saver!

Untitled

Because we also defined the AccountPage route with the authRequired: true property, Wasp will automatically pass the logged in user as a prop argument to our page. We can use the user object to display and update our favUsers, just as we can see in the image above.

To do that, let’s define a new updateAccount action in our main.wasp file:

action updateAccount {
fn: import { updateAccount } from "@server/account.js",
entities: [User]
}

Next, let’s create the updateAccount action in a new file, src/server/account.ts:

import type { UpdateAccount } from "@wasp/actions/types";
import HttpError from "@wasp/core/HttpError.js";

export const updateAccount: UpdateAccount<{ favUsers: string[] }, void> = async ({ favUsers }, context) => {
if (!context.user) {
throw new HttpError(401, "User is not authorized");
}

try {
await context.entities.User.update({
where: { id: context.user.id },
data: { favUsers },
});

} catch (error: any) {
throw new HttpError(500, error.message);
}
}

Right. Now it’s time to put it all together in our Account page. We’re going to create a form for adding new twitter users to scrape tweets from, so at the bottom of your src/client/AccountPage.tsx, below your other code, add the following component:

function InputFields({ user }: { user: User }) {
const [isLoading, setIsLoading] = useState(false);
const [fields, setFields] = useState(['']);

useEffect(() => {
if (user?.favUsers.length > 0) {
setFields(user.favUsers);
}
}, [user?.favUsers]);

const handleAdd = () => {
setFields([...fields, '']);
};

const handleRemove = () => {
const newFields = [...fields];
newFields.splice(fields.length - 1, 1);
setFields(newFields);
};

const handleChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
const newFields = [...fields];
newFields[index] = e.target.value;
setFields(newFields);
};

const handleSubmit = async () => {
//...
};

return (
<div className='w-full p-4'>
<div className='flex flex-row justify-start items-start'>
<h2 className='ml-1 font-bold'>Trend-Setting Twitter Accounts</h2>
</div>
{fields.map((field, index) => (
<div key={index} className='my-2'>
<input
type='text'
placeholder='Twitter Username'
className='w-full bg-white border border-gray-300 rounded-lg py-2 px-4 text-gray-700 focus:border-blue-400 focus:outline-none'
value={field}
onChange={(e) => handleChange(e, index)}
/>
</div>
))}
<div className='my-2 flex flex-row justify-end gap-1'>
{fields.length > 1 && <Button onClick={handleRemove}>-</Button>}
{fields.length < 10 && (
<Button onClick={handleAdd} className='bg-blue-500 text-white px-4 py-2 rounded'>
+
</Button>
)}
</div>
<Button onClick={handleSubmit} isLoading={isLoading}>
<span>Save</span>
</Button>
</div>
);
}

This component takes care of adding the logged in user’s favUsers array to state, and displaying that in information in a set of input components.

The only thing missing from it is to add our updateAccount action we just defined earlier. So at the top of the file, let’s import it and add the logic to our InputFields submit handler

import updateAccount from '@wasp/actions/updateAccount'; // <--- add this import

//...

const handleSubmit = async () => { // < --- add this function
try {
setIsLoading(true);
await updateAccount({ favUsers: fields });
} catch (err: any) {
alert(err.message);
} finally {
setIsLoading(false);
}
};

Also, in your AccountPage make sure to replace the line {JSON.stringify(user, null, 2)} with the newly created component <InputFields user={user} />.

Here is what the entire AccountPage.tsx file should now look like in case you get stuck:

import Button from './Button';
import { ChangeEvent, useEffect, useState } from 'react';
import logout from '@wasp/auth/logout';
import type { User } from '@wasp/entities';
import updateAccount from '@wasp/actions/updateAccount'

const AccountPage = ({ user }: { user: User }) => {
return (
<div className='flex flex-col justify-center items-center mt-12 w-full'>
<div className='flex flex-col items-center justify-center gap-4 border border-neutral-700 bg-neutral-100/40 rounded-xl p-1 sm:p-4 w-full'>
<div className='flex flex-row justify-end w-full px-4 pt-2'>
<Button onClick={logout}>Logout</Button>
</div>
<InputFields user={user} />
</div>
</div>
);
};

export default AccountPage;

function InputFields({ user }: { user: User }) {
const [isLoading, setIsLoading] = useState(false);
const [fields, setFields] = useState(['']);

useEffect(() => {
if (user?.favUsers.length > 0) {
setFields(user.favUsers);
}
}, [user?.favUsers]);

const handleAdd = () => {
setFields([...fields, '']);
};

const handleRemove = () => {
const newFields = [...fields];
newFields.splice(fields.length - 1, 1);
setFields(newFields);
};

const handleChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
const newFields = [...fields];
newFields[index] = e.target.value;
setFields(newFields);
};

const handleSubmit = async () => {
try {
setIsLoading(true);
await updateAccount({ favUsers: fields });
} catch (err: any) {
alert(err.message);
} finally {
setIsLoading(false);
}
};

return (
<div className='w-full p-4'>
<div className='flex flex-row justify-start items-start'>
<h2 className='ml-1 font-bold'>Trend-Setting Twitter Accounts</h2>
</div>
{fields.map((field, index) => (
<div key={index} className='my-2'>
<input
type='text'
placeholder='Twitter Username'
className='w-full bg-white border border-gray-300 rounded-lg py-2 px-4 text-gray-700 focus:border-blue-400 focus:outline-none'
value={field}
onChange={(e) => handleChange(e, index)}
/>
</div>
))}
<div className='my-2 flex flex-row justify-end gap-1'>
{fields.length > 1 && <Button onClick={handleRemove}>-</Button>}
{fields.length < 10 && (
<Button onClick={handleAdd} className='bg-blue-500 text-white px-4 py-2 rounded'>
+
</Button>
)}
</div>
<Button onClick={handleSubmit} isLoading={isLoading}>
<span>Save</span>
</Button>
</div>
);
}

And here’s what your AccountPage should look like when navigating to localhost:3000/account (note: the styling may be a bit ugly, but we’ll take care of that later):

Untitled

Fantastic. So we’ve got the majority of the app logic finished — our own personal twitter “intern” to help us all become thought leaders and thread bois 🤣.

Adding a Cron Job

But wouldn’t it be cool if we could automate the Generate New Ideas process? Each time you click the button, it takes quite a while for tweets to be scraped, and ideas to be generated, especially if we are generating ideas for a lot of new tweets.

So it would be nicer if we had a cron job (recurring task), that ran automatically in the background at a set interval.

With Wasp, that’s also super easy to set up. To do so, let’s go to our main.wasp file and add our job at the very bottom:

//...

job newIdeasJob {
executor: PgBoss,
perform: {
fn: import generateNewIdeasWorker from "@server/worker/generateNewIdeasWorker.js"
},
entities: [User, GeneratedIdea, Tweet, TweetDraft],
schedule: {
// run cron job every 30 minutes
cron: "*/30 * * * *",
executorOptions: {
pgBoss: {=json { "retryLimit": 2 } json=},
}
}
}

Let’s run through the code above:

  • Jobs use pg-boss, a postgres extension, to queue and run tasks under the hood.
  • with perform we’re telling the job what function we want it to call: generateNewIdeasWorker
  • just like actions and queries, we have to tell the job which entities we want to give it access to. In this case, we will need access to all of our entities.
  • the schedule allows us to pass some options to pg-boss so that we can make it a recurring task. In this case, I set it to run every 30 minutes, but you can set it to any interval you’d like (tip: change the comment and let github co-pilot write the cron for you). We also tell pg-boss to retry a failed job two times.

Perfect. So now, our app will automatically scrape our favorite users’ tweets and generate new ideas for us every 30 minutes. This way, if we revisit the app after a few days, all the content will already be there and we won’t have to wait a long time for it to generate it for us. We also make sure we never miss out on generating ideas for older tweets.

But for that to happen, we have to define the function our job will call. To do this, create a new directory worker within the server folder, and within it a new file: src/server/worker/generateNewIdeasWorker

import { generateNewIdeas } from '../ideas.js';

export default async function generateNewIdeasWorker(_args: unknown, context: any) {
try {
console.log('Running recurring task: generateNewIdeasWorker')
const allUsers = await context.entities.User.findMany({});

for (const user of allUsers) {
context.user = user;
console.log('Generating new ideas for user: ', user.username);
await generateNewIdeas(undefined as never, context);
console.log('Done generating new ideas for user: ', user.username)
}

} catch (error: any) {
console.log('Recurring task error: ', error);
}
}

In this file, all we’re doing is looping through all the users in our database, and passing them via the context object to our generateNewIdeas action. The nice thing about jobs is that Wasp automatically passes the context object to these functions, which we can then pass along to our action.

So now, at the interval that you set (e.g. 30 minutes), you should notice the logs being printed to the console whenever your job starts automatically running.

[Server]  Generating new ideas for user:  vinny

Alright, things are looking pretty good now, but let’s not forget to add a page to view all the notes we added and embedded to our vector store!

Adding a Notes Page

Go ahead and add the following route to your main.wasp file:

route NotesPage { path: "/notes", to: NotesPage }
page NotesPage {
authRequired: true,
component: import Notes from "@client/NotesPage"
}

Create the complementary page, src/client/NotesPage.tsx and add the following boilerplate just to get started (we’ll add the rest later):

const NotesPage = () => {

return (
<>Notes</>
);
};

export default NotesPage;

It would be nice if we had a simple Nav Bar to navigate back and forth between our two pages. It would also be cool if we had our <AddNote /> input component on all pages, that way it’s easy for us to add an idea whenever inspiration strikes.

Rather than copying the NavBar and AddNote code to both pages, let’s create a wrapper, or “root”, component for our entire app so that all of our pages have the same Nav Bar and layout.

To do that, in our main.wasp file, let’s define our root component by adding a client property to our app configuration at the very top of the file. This is how the entire app object should look like now:

app twitterAgent {
wasp: {
version: "^0.10.6"
},
title: "twitter-agent",
client: {
rootComponent: import App from "@client/App",
},
db: {
system: PostgreSQL,
},
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
methods: {
usernameAndPassword: {},
}
},
dependencies: [
("openai", "3.2.1"),
("rettiwt-api", "1.1.8"),
("langchain", "0.0.91"),
("@pinecone-database/pinecone", "0.1.6"),
("@headlessui/react", "1.7.15"),
("react-icons", "4.8.0"),
("react-twitter-embed", "4.0.4")
],
}

// entities, operations, routes, and other stuff...

Next, create a new file src/client/App.tsx with the following content:

import './Main.css';
import AddNote from './AddNote';
import { ReactNode } from 'react';
import useAuth from '@wasp/auth/useAuth';

const App = ({ children }: { children: ReactNode }) => {

const { data: user } = useAuth();

return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col gap-6 justify-center items-center mx-auto pt-12'>
<div className='flex flex-row justify-between items-center w-1/2 mb-6 text-neutral-600 px-2'>
<div className='flex justify-start w-1/3'>
<a href='/' className='hover:underline cursor-pointer'>
🤖 Generated Ideas
</a>
</div>
<div className='flex justify-center w-1/3'>
<a href='/notes' className='hover:underline cursor-pointer'>
📝 My Notes
</a>
</div>
<div className='flex justify-end w-1/3'>
<a href='/account' className='hover:underline cursor-pointer'>
👤 Account
</a>
</div>
</div>

<div className='flex flex-col gap-4 justify-center items-center w-2/4'>
{!!user && <AddNote />}
<hr className='border border-t-1 border-neutral-100/70 w-full' />
{children}
</div>
</div>
</div>
);
};

export default App;

With this defined, Wasp will know to pass all other routes as children through our App component. That way, we will always show the Nav Bar and AddNote component on the top of every page.

We also take advantage of Wasp’s handy useAuth hook to check if a user is logged in, and if so we show the AddNote component.

Now, we can delete the duplicate code on our MainPage. This is what it should look like now:

import { useState } from 'react';
import generateNewIdeas from '@wasp/actions/generateNewIdeas';
import { useQuery } from '@wasp/queries';
import getTweetDraftsWithIdeas from '@wasp/queries/getTweetDraftsWithIdeas';
import Button from './Button';
import { TwitterTweetEmbed } from 'react-twitter-embed';

const MainPage = () => {
const [isGenerating, setIsGenerating] = useState(false);

const {
data: tweetDrafts,
isLoading: isTweetDraftsLoading,
error: tweetDraftsError,
} = useQuery(getTweetDraftsWithIdeas);

const handleNewIdeas = async (e: any) => {
try {
setIsGenerating(true);
await generateNewIdeas();
} catch (error: any) {
alert(error.message);
} finally {
setIsGenerating(false);
}
};

if (isTweetDraftsLoading) {
return 'Loading...';
}

if (tweetDraftsError) {
return 'Error: ' + tweetDraftsError.message;
}

return (
<>
<div className='flex flex-row justify-center w-full'>
<Button onClick={handleNewIdeas} isLoading={isGenerating}>
Generate New Ideas
</Button>
</div>
<div className='flex flex-col gap-4 justify-center items-center w-full'>
{tweetDrafts.map((tweetDraft) => (
<>
<h2 className='text-2xl font-bold'>Generated Ideas</h2>
<div key={tweetDraft.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Original Tweet</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<TwitterTweetEmbed tweetId={tweetDraft.originalTweet.tweetId} />
</div>
<h2>Tweet Draft</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{tweetDraft.content}</div>
</div>

{!!tweetDraft.notes && tweetDraft.notes !== tweetDraft.originalTweet.content && (
<>
<h2>Your Similar Notes</h2>
{tweetDraft.notes}
</>
)}
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Ideas</h2>
{tweetDraft.originalTweet.ideas.map((idea) => (
<div key={idea.id} className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'>{idea.content}</div>
</div>
</div>
))}
</div>
</div>
</>
))}
</div>
</>
);
};
export default MainPage;

Next, we need to create a query that allows us to fetch all of our added notes and ideas that have been embedded in our vector store.

For that, we need to define a new query in our main.wasp file:

query getEmbeddedNotes {
fn: import { getEmbeddedNotes } from "@server/ideas.js",
entities: [GeneratedIdea]
}

We then need to create that query at the bottom of our src/actions/ideas.ts file:

// first import the type at the top of the file
import type { GetEmbeddedNotes, GetTweetDraftsWithIdeas } from '@wasp/queries/types';

//...

export const getEmbeddedNotes: GetEmbeddedNotes<never, GeneratedIdea[]> = async (_args, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

const notes = await context.entities.GeneratedIdea.findMany({
where: {
userId: context.user.id,
isEmbedded: true,
},
orderBy: {
createdAt: 'desc',
},
});

return notes;
}

Now let’s go back to our src/client/NotesPage.tsx and add our query. Our new file will look like this:

import { useQuery } from '@wasp/queries';
import getEmbeddedNotes from '@wasp/queries/getEmbeddedNotes';

const NotesPage = () => {
const { data: notes, isLoading, error } = useQuery(getEmbeddedNotes);

if (isLoading) <div>Loading...</div>;
if (error) <div>Error: {error.message}</div>;

return (
<>
<h2 className='text-2xl font-bold'>My Notes</h2>
{notes && notes.length > 0 ? (
notes.map((note) => (
<div key={note.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{note.content}</div>
</div>
</div>
))
) : notes && notes.length === 0 && (
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>No notes yet</div>
</div>
)}
</>
);
};

export default NotesPage;

Cool! Now we should be fetching all our embedded notes and ideas, signified by the isEmbedded tag in our postgres database. Your Notes page should now look something like this:

Untitled

You Did it! Your own Twitter Intern 🤖

Help me help you

🌟 If you haven’t yet, please star us on GitHub, 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

And that’s it! You’ve now got yourself a semi-autonomous twitter brainstorming agent to help inspire new ideas and keep you actively contributing 🚀

There’s way more you can do with these tools, but this is a great start.

Remember, if you want to see a more advanced version of this app which utilizes the official Twitter API to send tweets, gives you the ability to edit and add generated notes on the fly, has manual similarity search for all your notes, and more, then you can check out the 💥 Banger Tweet Bot 🤖.

And, once again, here's the repo for the finished app we built in this tutorial: Personal Twitter Intern

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

· 46 min read
Vinny

TL;DR

LangChain, ChatGPT, and other emerging technology have made it possible to build some really creative tools.

In this tutorial, we’ll build a full-stack web app that acts as our own personal Twitter Agent, or “intern”, as I like to call it. It keeps track of your notes and ideas, and uses them — along with tweets from trending-setting twitter users — to brainstorm new ideas and write tweet drafts for you! 💥

BTW, If you get stuck during the tutorial, or at any point just want to check out the full, final repo of the app we're building, here it is: https://github.com/vincanger/twitter-intern

Before We Begin

Wasp = } is the only open-source, completely serverful fullstack React/Node framework with a built in compiler that lets you build your app in a day and deploy with a single CLI command.

We’re working hard to help you build performant web apps as easily as possibly — including making these tutorials, which are 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://media2.giphy.com/media/d0Pkp9OMIBdC0/giphy.gif?cid=7941fdc6b39mgj7h8orvi0f4bjebceyx4gj0ih1xb6s05ujc&ep=v1_gifs_search&rid=giphy.gif&ct=g

…even Ron would star Wasp on GitHub 🤩

Background

Twitter is a great marketing tool. It’s also a great way to explore ideas and refine your own. But it can be time-consuming and difficult to maintain a tweeting habit.

https://media0.giphy.com/media/WSrR5xkvljaFMe7UPo/giphy.gif?cid=7941fdc6g9o3drj567dbwyuo1c66x76eq8awc2r1oop8oypl&ep=v1_gifs_search&rid=giphy.gif&ct=g

That’s why I decided to build my own personal twitter agent with LangChain on the basis of these assumptions:

🧠 LLMs (like ChatGPT) aren’t the best writers, but they ARE great at brainstorming new ideas.

📊 Certain twitter users drive the majority of discourse within certain niches, i.e. trend-setters influence what’s being discussed at the moment.

💡 the Agent needs context in order to generate ideas relevant to YOU and your opinions, so it should have access to your notes, ideas, tweets, etc.

So instead of trying to build a fully autonomous agent that does the tweeting for you, I thought it would be better to build an agent that does the BRAINSTORMING for you, based on your favorite trend-setting twitter users as well as your own ideas.

Imagine it like an intern that does the grunt work, while you do the curating!

https://media.giphy.com/media/26DNdV3b6dqn1jzR6/giphy.gif

In order to accomplish this, we need to take advantage of a few hot AI tools:

  • Embeddings and Vector Databases
  • LLMs (Large Language Models), such as ChatGPT
  • LangChain and sequential “chains” of LLM calls

Embeddings and Vector Databases give us a powerful way to perform similarity searches on our own notes and ideas.

If you’re not familiar with similarity search, the simplest way to describe what similarity search is by comparing it to a normal google search. In a normal search, the phrase “a mouse eats cheese” will return results with a combination of those words only. But a vector-based similarity search, on the other hand, would return those words, as well as results with related words such as “dog”, “cat”, “bone”, and “fish”.

You can see why that’s so powerful, because if we have non-exact but related notes, our similarity search will still return them!

https://media2.giphy.com/media/xUySTD7evBn33BMq3K/giphy.gif?cid=7941fdc6273if8qfk83gbnv8uabc4occ0tnyzk0g0gfh0qg5&ep=v1_gifs_search&rid=giphy.gif&ct=g

For example, if our favorite trend-setting twitter user makes a post about the benefits of typescript, but we only have a note on “our favorite React hooks”, our similarity search would still likely return such a result. And that’s huge!

Once we get those notes, we can pass them to the ChatGPT completion API along with a prompt to generate more ideas. The result from this prompt will then be sent to another prompt with instructions to generate a draft tweet. We save these sweet results to our Postgres relational database.

This “chain” of prompting is essentially where the LangChain package gets its name 🙂

The flow of information through the app

This approach will give us a wealth of new ideas and tweet drafts related to our favorite trend-setting twitter users’ tweets. We can look through these, edit and save our favorite ideas to our “notes” vector store, or maybe send off some tweets.

I’ve personally been using this app for a while now, and not only has it generated some great ideas, but it also helps to inspire new ones (even if some of the ideas it generates are “meh”), which is why I included an “Add Note” feature front and center to the nav bar

twitter-agent-add-note.png

Ok. Enough background. Let’s start building your own personal twitter intern! 🤖

BTW, if you get stuck at all while following the tutorial, you can always reference this tutorial’s repo, which has the finished app: Twitter Intern GitHub Repo

Configuration

Set up your Wasp project

We’re going to make this a full-stack React/NodeJS web app so we need to get that set up first. But don’t worry, it won’t take long AT ALL, because we will be using Wasp as the framework.

Wasp does all the heavy lifting for us. You’ll see what I mean in a second.

# First, install Wasp by running this in your terminal:

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

# next, create a new project:

wasp new twitter-agent

# cd into the new directory and start the project:

cd twitter-agent && wasp start

Great! When running wasp start, Wasp will install all the necessary npm packages, start our server on port 3001, and our React client on port 3000. Head to localhost:3000 in your browser to check it out.

Untitled

Tip ℹ️

you can install the Wasp vscode extension for the best developer experience.

You’ll notice Wasp sets up your full-stack app with a file structure like so:

.
├── main.wasp # The wasp config file.
└── src
   ├── client # Your React client code (JS/CSS/HTML) goes here.
   ├── server # Your server code (Node JS) goes here.
   └── shared # Your shared (runtime independent) code goes here.

Let’s start adding some server-side code.

Server-Side & Database Entities

Start by adding a .env.server file in the root directory of your project:

# https://platform.openai.com/account/api-keys
OPENAI_API_KEY=

# sign up for a free tier account at https://www.pinecone.io/
PINECONE_API_KEY=
# will be a location, e.g 'us-west4-gcp-free'
PINECONE_ENV=

# We will fill these in later during the Twitter Scraping section
# Twitter details -- only needed once for Rettiwt.account.login() to get the tokens
TWITTER_EMAIL=
TWITTER_HANDLE=
TWITTER_PASSWORD=

# TOKENS -- fill these in after running the getTwitterTokens script in the Twitter Scraping section
KDT=
TWID=
CT0=
AUTH_TOKEN=

We need a way for us to store all our great ideas. So let’s first head to Pinecone.io and set up a free trial account.

Untitled

In the Pinecone dashboard, go to API keys and create a new one. Copy and paste your Environment and API Key into .env.server

Do the same for OpenAI, by creating an account and key at https://platform.openai.com/account/api-keys

Now let’s replace the contents of the main.wasp config file, which is like the “skeleton” of your app, with the code below. This will configure most of the fullstack app for you 🤯

app twitterAgent {
wasp: {
version: "^0.10.6"
},
title: "twitter-agent",
head: [
"<script async src='https://platform.twitter.com/widgets.js' charset='utf-8'></script>"
],
db: {
system: PostgreSQL,
},
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
methods: {
usernameAndPassword: {},
}
},
dependencies: [
("openai", "3.2.1"),
("rettiwt-api", "1.1.8"),
("langchain", "0.0.91"),
("@pinecone-database/pinecone", "0.1.6"),
("@headlessui/react", "1.7.15"),
("react-icons", "4.8.0"),
("react-twitter-embed", "4.0.4")
],
}

// ### Database Models

entity Tweet {=psl
id Int @id @default(autoincrement())
tweetId String
authorUsername String
content String
tweetedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
drafts TweetDraft[]
ideas GeneratedIdea[]
psl=}

entity TweetDraft {=psl
id Int @id @default(autoincrement())
content String
notes String
originalTweet Tweet @relation(fields: [originalTweetId], references: [id])
originalTweetId Int
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
psl=}

entity GeneratedIdea {=psl
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId Int
originalTweet Tweet? @relation(fields: [originalTweetId], references: [id])
originalTweetId Int?
isEmbedded Boolean @default(false)
psl=}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
createdAt DateTime @default(now())
favUsers String[]
originalTweets Tweet[]
tweetDrafts TweetDraft[]
generatedIdeas GeneratedIdea[]
psl=}

// <<< Client Pages & Routes

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

//...
note

You might have noticed this {=psl psl=} syntax in the entities above. This denotes that anything in between these psl brackets is actually a different language, in this case, Prisma Schema Language. Wasp uses Prisma under the hood, so if you've used Prisma before, it should be straightforward.

As you can see, our main.wasp config file has our:

  • dependencies,
  • authentication method,
  • database type, and
  • database models (”entities”)

With this, our app structure is mostly defined and Wasp will take care of a ton of configuration for us.

Database Setup

But we still need to get a postgres database running. Usually this can be pretty annoying, but with Wasp, just have Docker Deskop installed and running, then open up another separate terminal tab/window and then run:

wasp start db

This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 Just leave this terminal tab, along with docker desktop, open and running in the background.

In a different terminal tab, run:

wasp db migrate-dev

and make sure to give your database migration a name.

If you stopped the wasp dev server to run this command, go ahead and start it again with wasp start.

At this point, our app will be navigating us to localhost:3000/login but because we haven’t implemented a login screen/flow yet, we will be seeing a blank screen. Don’t worry, we’ll get to that.

Embedding Ideas & Notes

Server Action

First though, in the main.wasp config file, let’s define a server action for saving notes and ideas. Go ahead and add the code below to the bottom of the file:

// main.wasp

//...
// <<< Client Pages & Routes

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

// !!! Actions

action embedIdea {
fn: import { embedIdea } from "@server/ideas.js",
entities: [GeneratedIdea]
}

With the action declared, let’s create it. Make a new file, .src/server/ideas.ts in and add the following code:

import type { EmbedIdea } from '@wasp/actions/types';
import type { GeneratedIdea } from '@wasp/entities';
import HttpError from '@wasp/core/HttpError.js';
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { Document } from 'langchain/document';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { PineconeClient } from '@pinecone-database/pinecone';

const pinecone = new PineconeClient();
export const initPinecone = async () => {
await pinecone.init({
environment: process.env.PINECONE_ENV!,
apiKey: process.env.PINECONE_API_KEY!,
});
return pinecone;
};

export const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});

/**
* Embeds a single idea into the vector store
*/
export const embedIdea: EmbedIdea<{ idea: string }, GeneratedIdea> = async ({ idea }, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

console.log('idea: ', idea);

try {
let newIdea = await context.entities.GeneratedIdea.create({
data: {
content: idea,
userId: context.user.id,
},
});


if (!newIdea) {
throw new HttpError(404, 'Idea not found');
}

const pinecone = await initPinecone();

// we need to create an index to save the vector embeddings to
// an index is similar to a table in relational database world
const availableIndexes = await pinecone.listIndexes();
if (!availableIndexes.includes('embeds-test')) {
console.log('creating index');
await pinecone.createIndex({
createRequest: {
name: 'embeds-test',
// open ai uses 1536 dimensions for their embeddings
dimension: 1536,
},
});
}

const pineconeIndex = pinecone.Index('embeds-test');

// the LangChain vectorStore wrapper
const vectorStore = new PineconeStore(embeddings, {
pineconeIndex: pineconeIndex,
namespace: context.user.username,
});

// create a document with the idea's content to be embedded
const ideaDoc = new Document({
metadata: { type: 'note' },
pageContent: newIdea.content,
});

// add the document to the vectore store along with its id
await vectorStore.addDocuments([ideaDoc], [newIdea.id.toString()]);

newIdea = await context.entities.GeneratedIdea.update({
where: {
id: newIdea.id,
},
data: {
isEmbedded: true,
},
});
console.log('idea embedded successfully!', newIdea);
return newIdea;
} catch (error: any) {
throw new Error(error);
}
};
info

We’ve defined the action function in our main.wasp file as coming from ‘@server/ideas.js’ but we’re creating an ideas.ts file. What's up with that?!

Well, Wasp internally uses esnext module resolution, which always requires specifying the extension as .js (i.e., the extension used in the emitted JS file). This applies to all @server imports (and files on the server in general). It does not apply to client files.

Great! Now we have a server action for adding notes and ideas to our vector database. And we didn’t even have to configure a server ourselves (thanks, Wasp 🙂).

Let's take a step back and walk through the code we just wrote though:

  1. We create a new Pinecone client and initialize it with our API key and environment.
  2. We create a new OpenAIEmbeddings client and initialize it with our OpenAI API key.
  3. We create a new index in our Pinecone database to store our vector embeddings.
  4. We create a new PineconeStore, which is a LangChain wrapper around our Pinecone client and our OpenAIEmbeddings client.
  5. We create a new Document with the idea’s content to be embedded.
  6. We add the document to the vector store along with its id.
  7. We also update the idea in our Postgres database to mark it as embedded.

Now we want to create the client-side functionality for adding ideas, but you’ll remember we defined an auth object in our wasp config file. So we’ll need to add the ability to log in before we do anything on the frontend.

Authentication

Let’s add that quickly by adding a new a Route and Page definition to our main.wasp file

//...

route LoginPageRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/LoginPage"
}

…and create the file src/client/LoginPage.tsx with the following content:

import { LoginForm } from '@wasp/auth/forms/Login';
import { SignupForm } from '@wasp/auth/forms/Signup';
import { useState } from 'react';

export default () => {
const [showSignupForm, setShowSignupForm] = useState(false);

const handleShowSignupForm = () => {
setShowSignupForm((x) => !x);
};

return (
<>
{showSignupForm ? <SignupForm /> : <LoginForm />}
<div onClick={handleShowSignupForm} className='underline cursor-pointer hover:opacity-80'>
{showSignupForm ? 'Already Registered? Login!' : 'No Account? Sign up!'}
</div>
</>
);
};
info

In the auth object on the main.wasp file, we used the usernameAndPassword method which is the simplest form of auth Wasp offers. If you’re interested, Wasp does provide abstractions for Google, Github, and Email Verified Authentication, but we will stick with the simplest auth for this tutorial.

With authentication all set up, if we try to go to localhost:3000 we will be automatically directed to the login/register form.

You’ll see that Wasp creates Login and Signup forms for us because of the auth object we defined in the main.wasp file. Sweet! 🎉

But even though we’ve added some style classes, we haven’t set up any css styling so it will probably be pretty ugly right about now.

🤢 Barf.

Untitled

Adding Tailwind CSS

Luckily, Wasp comes with tailwind css support, so all we have to do to get that working is add the following files in the root directory of the project:

.
├── main.wasp
├── src
│ ├── client
│ ├── server
│ └── shared
├── postcss.config.cjs # add this file here
├── tailwind.config.cjs # and this here too
└── .wasproot

postcss.config.cjs

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

tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

Finally, replace the contents of your src/client/Main.css file with these lines:

@tailwind base;
@tailwind components;
@tailwind utilities;

Now we’ve got the magic of tailwind css on our sides! 🎨 We’ll get to styling later though. Patience, young grasshopper.

Adding Notes Client-side

From here, let’s create the complimentary client-side components for adding notes to the vector store. Create a new .src/client/AddNote.tsx file with the following contents:

import { useState } from 'react';
import embedIdea from '@wasp/actions/embedIdea';

export default function AddNote() {
const [idea, setIdea] = useState('');
const [isIdeaEmbedding, setIsIdeaEmbedding] = useState(false);

const handleEmbedIdea = async (e: any) => {
try {
setIsIdeaEmbedding(true);
if (!idea) {
throw new Error('Idea cannot be empty');
}
const embedIdeaResponse = await embedIdea({
idea,
});

console.log('embedIdeaResponse: ', embedIdeaResponse);
} catch (error: any) {
alert(error.message);
} finally {
setIdea('');
setIsIdeaEmbedding(false);
}
};

return (
<div className='flex flex-row gap-2 justify-center items-end w-full'>
<textarea
autoFocus
onChange={(e) => setIdea(e.target.value)}
value={idea}
placeholder='LLMs are great for brainstorming!'
className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'
/>
<button
onClick={handleEmbedIdea}
className='flex flex-row justify-center items-center bg-neutral-100 hover:bg-neutral-200 border border-neutral-300 font-bold px-3 py-1 text-sm text-blue-500 whitespace-nowrap rounded-lg'
>
{isIdeaEmbedding ? 'Loading...' : 'Save Note'}
</button>
</div>
);
}

Here we’re using the embedIdea action we defined earlier to add our idea to the vector store. We’re also using the useState hook to keep track of the idea we’re adding, as well as the loading state of the button.

So now we have a way to add our own ideas and notes to our vector store. Pretty sweet!

Generating New Ideas & Tweet Drafts

Using LangChain's Sequential Chains

Now we need to set up the sequential chain of LLM calls that LangChain is so great at.

Here are the steps we will take:

  1. define a function that uses LangChain to initiate a “chain” of API calls to OpenAI’s ChatGPT completions endpoint.
    1. this function takes a tweet that we pulled from one of our favorite twitter users as an argument, searches our vector store for similar notes & ideas, and returns a list of new “brainstormed” based on the example tweet and our notes.
  2. define a new action that loops through our favorite users array, pulls their most recent tweets, and sends them to our LangChain function mentioned above

So let’s start again by creating our LangChain function. Make a new src/server/chain.ts file:

import { ChatOpenAI } from 'langchain/chat_models/openai';
import { LLMChain, SequentialChain } from 'langchain/chains';
import { PromptTemplate } from 'langchain/prompts';
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { PineconeClient } from '@pinecone-database/pinecone';

const pinecone = new PineconeClient();
export const initPinecone = async () => {
await pinecone.init({
environment: process.env.PINECONE_ENV!,
apiKey: process.env.PINECONE_API_KEY!,
});
return pinecone;
};

const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});

export const generateIdeas = async (exampleTweet: string, username: string) => {
try {
// remove quotes and curly braces as not to confuse langchain template parser
exampleTweet = exampleTweet.replace(/"/g, '');
exampleTweet = exampleTweet.replace(/{/g, '');
exampleTweet = exampleTweet.replace(/}/g, '');

const pinecone = await initPinecone();

console.log('list indexes', await pinecone.listIndexes());

// find the index we created earlier
const pineconeIndex = pinecone.Index('embeds-test');

const vectorStore = new PineconeStore(embeddings, {
pineconeIndex: pineconeIndex,
namespace: username,
});

//
// sequential tweet chain begin --- >
//
/**
* vector store results for notes similar to the original tweet
*/
const searchRes = await vectorStore.similaritySearchWithScore(exampleTweet, 2);
console.log('searchRes: ', searchRes);
let notes = searchRes
.filter((res) => res[1] > 0.7) // filter out strings that have less than %70 similarity
.map((res) => res[0].pageContent)
.join(' ');

console.log('\n\n similarity search results of our notes-> ', notes);

if (!notes || notes.length <= 2) {
notes = exampleTweet;
}

const tweetLlm = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
temperature: 0.8, // 0 - 2 with 0 being more deterministic and 2 being most "loose". Past 1.3 the results tend to be more incoherent.
modelName: 'gpt-3.5-turbo',
});

const tweetTemplate = `You are an expert idea generator. You will be given a user's notes and your goal is to use this information to brainstorm other novel ideas.

Notes: {notes}

Ideas Brainstorm:
-`;

const tweetPromptTemplate = new PromptTemplate({
template: tweetTemplate,
inputVariables: ['notes'],
});

const tweetChain = new LLMChain({
llm: tweetLlm,
prompt: tweetPromptTemplate,
outputKey: 'newTweetIdeas',
});

const interestingTweetTemplate = `You are an expert interesting tweet generator. You will be given some tweet ideas and your goal is to choose one, and write a tweet based on it. Structure the tweet in an informal yet serious tone and do NOT include hashtags in the tweet!

Tweet Ideas: {newTweetIdeas}

Interesting Tweet:`;

const interestingTweetLlm = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
temperature: 1.1,
modelName: 'gpt-3.5-turbo',
});

const interestingTweetPrompt = new PromptTemplate({
template: interestingTweetTemplate,
inputVariables: ['newTweetIdeas'],
});

const interestingTweetChain = new LLMChain({
llm: interestingTweetLlm,
prompt: interestingTweetPrompt,
outputKey: 'interestingTweet',
});

const overallChain = new SequentialChain({
chains: [tweetChain, interestingTweetChain],
inputVariables: ['notes'],
outputVariables: ['newTweetIdeas', 'interestingTweet'],
verbose: false,
});

type ChainDraftResponse = {
newTweetIdeas: string;
interestingTweet: string;
notes: string;
};

const res1 = (await overallChain.call({
notes,
})) as ChainDraftResponse;

return {
...res1,
notes,
};
} catch (error: any) {
throw new Error(error);
}
};

Great! Let's run through the above code real quick:

  1. Initialize the Pinecone client
  2. Find our pinecone index (i.e. table) that we created earlier and initialize a new PineconeStore with LangChain
  3. Search our vector store for notes similar to the example tweet, filtering out any results that have less than %70 similarity
  4. Create a new ChatGPT completion chain that takes our notes as input and generates new tweet ideas
  5. Create a new ChatGPT completion chain that takes the new tweet ideas as input and generates a new tweet draft
  6. Create a new SequentialChain and combine the above two chains together so that we can pass it our notes as input and it returns the new tweet ideas and the new tweet draft as output
VECTOR COSINE SIMILARITY SCORES

A good similarity threshold for cosine similarity search on text strings depends on the specific application and the desired level of strictness in matching. Cosine similarity scores range between 0 and 1, with 0 meaning no similarity and 1 meaning completely identical text strings.

  • 0.8-0.9 = strict
  • 0.6-0.8 = moderate
  • 0.5 = relaxed.

In our case, we went for a moderate similarity threshold of 0.7, which means that we will only return notes that are at least 70% similar to the example tweet.

With this function, we will get our newTweetIdeas and our interestingTweet draft back as results that we can use within our server-side action.

Scraping Twitter

Before we can pass an exampleTweet as an argument to our newly created Sequential Chain, we need to fetch it first!

To do this, we're going to use the Rettiwt-Api (which is just Twitter written backwards). Because it's an unofficial API there are a few caveats:

  1. We have to use the rettiwt client to login to our twitter account once. We will output the tokens it returns via a script and save those in our .env.server file for later.
  2. It's best to use an alternative account for this process. If you don't have an alternative account, go ahead and register a new one now.
⚠️

The use of an unofficial Twitter client, Rettiwt, is for illustrative purposes only. It's crucial that you familiarize yourself with Twitter's policies and rules regarding scraping before implementing these methods. Any abuse or misuse of these scripts and techniques may lead to actions taken against your Twitter account. We hold no responsibility for any consequences arising from your personal use of this tutorial and/or the related scripts. It is intended purely for learning and educational purposes.

Let's go ahead and create a new folder in src/server called scripts with a file inside called tokens.ts. This will be our script that we will run only once, just so that we get the necessary tokens to pass to our Rettiwt client.

We want to avoid running this script many times otherwise our account could get rate-limited. This shouldn't be an issue though, because once we return the tokens, they are valid for up to a year.

So inside src/server/scripts/tokens.ts add the following code:

import { Rettiwt } from 'rettiwt-api'; 

/**
* This is a script we can now run from the cli with `wasp db seed`
* IMPORTANT! We only want to run this script once, after which we save the tokens
* in the .env.server file. They should be good for up to a year.
*/
export const getTwitterTokens = async () => {
const tokens = await Rettiwt().account.login(
process.env.TWITTER_EMAIL!,
process.env.TWITTER_HANDLE!,
process.env.TWITTER_PASSWORD!
);

console.log('tokens: ', tokens)
};

Make sure to add your twitter login details to our .env.server file, if you haven't already!

Great. To be able to run this script via a simple Wasp CLI command, add it via the seeds array within the db object at the top of your main.wasp file:

app twitterAgent {
wasp: {
version: "^0.10.6"
},
//...
db: {
system: PostgreSQL,
seeds: [ // <---------- add this
import { getTwitterTokens } from "@server/scripts/tokens.js",
]
},
//...

Nice! Now for the fun part :)

in your terminal, at the root of your project, run wasp db seed, and you should see the tokens output to the terminal similar to this:

[Db]      Running seed: getTwitterTokens
[Db] tokens: { // your tokens... }

Copy and paste those tokens into your .env.server file:


# TOKENS -- fill these in after running the getTwitterTokens script in the Twitter Scraping section
KDT='...'
TWID='...'
CT0='...'
AUTH_TOKEN='...'

Now with that, we should be able to access our favorite trend-setting users' recent tweets and use them to help us brainstorm new ideas!

Server Action

Ok, so we've got the tokens we need to get our trend-setting example tweets, and we've got a function that runs our similarity search and sequential chain of LLM calls.

Now let’s define an action in our main.wasp file that pulls it all together:

// actions...

action generateNewIdeas {
fn: import { generateNewIdeas } from "@server/ideas.js",
entities: [GeneratedIdea, Tweet, TweetDraft, User]
}

…and then create that action within src/server/ideas.ts


import type {
EmbedIdea,
GenerateNewIdeas // < ---- add this type here -----
} from '@wasp/actions/types';
// ... other imports ...
import { generateIdeas } from './chain.js'; // < ---- this too -----
import { Rettiwt } from 'rettiwt-api'; // < ---- and this here -----

const twitter = Rettiwt({ // < ---- and this -----
kdt: process.env.KDT!,
twid: process.env.TWID!,
ct0: process.env.CT0!,
auth_token: process.env.AUTH_TOKEN!,
});

//... other stuff ...

export const generateNewIdeas: GenerateNewIdeas<unknown, void> = async (_args, context) => {
try {
// get the logged in user that Wasp passes to the action via the context
const user = context.user

if (!user) {
throw new HttpError(401, 'User is not authorized');
}

for (let h = 0; h < user.favUsers.length; h++) {
const favUser = user.favUsers[h];
const oneDayFromNow = new Date(Date.now() + 24 * 60 * 60 * 1000);
// convert oneDayFromNow to format YYYY-MM-DD
const endDate = oneDayFromNow.toISOString().split('T')[0];

// find the most recent tweet from the favUser
const mostRecentTweet = await context.entities.Tweet.findFirst({
where: {
authorUsername: favUser,
},
orderBy: {
tweetedAt: 'desc',
},
});

console.log('mostRecentTweet: ', mostRecentTweet)

const favUserTweets = await twitter.tweets.getTweets({
fromUsers: [favUser],
sinceId: mostRecentTweet?.tweetId || undefined, // get tweets since the most recent tweet if it exists
endDate: endDate, // endDate in format YYYY-MM-DD
});

const favUserTweetTexts = favUserTweets.list

for (let i = 0; i < favUserTweetTexts.length; i++) {
const tweet = favUserTweetTexts[i];

const existingTweet = await context.entities.User.findFirst({
where: {
id: user.id,
},
select: {
originalTweets: {
where: {
tweetId: tweet.id,
},
},
},
});

/**
* If the tweet already exists in the database, skip generating drafts and ideas for it.
*/
if (existingTweet) {
console.log('tweet already exists in db, skipping generating drafts...');
continue;
}

/**
* this is where the magic happens
*/
const draft = await generateIdeas(tweet.fullText, user.username);
console.log('draft: ', draft);

const originalTweet = await context.entities.Tweet.create({
data: {
tweetId: tweet.id,
content: tweet.fullText,
authorUsername: favUser,
tweetedAt: new Date(tweet.createdAt),
userId: user.id
},
});

let newTweetIdeas = draft.newTweetIdeas.split('\n');
newTweetIdeas = newTweetIdeas
.filter((idea) => idea.trim().length > 0)
.map((idea) => {
// remove all dashes that are not directly followed by a letter
idea = idea.replace(/-(?![a-zA-Z])/g, '');
idea = idea.replace(/"/g, '');
idea = idea.replace(/{/g, '');
idea = idea.replace(/}/g, '');
// remove hashtags and the words that follow them
idea = idea.replace(/#[a-zA-Z0-9]+/g, '');
idea = idea.replace(/^\s*[\r\n]/gm, ''); // remove new line breaks
idea = idea.trim();
// check if last character contains punctuation and if not add a period
if (idea.length > 1 && !idea[idea.length - 1].match(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g)) {
idea += '.';
}
return idea;
});
for (let j = 0; j < newTweetIdeas.length; j++) {
const newTweetIdea = newTweetIdeas[j];
const newIdea = await context.entities.GeneratedIdea.create({
data: {
content: newTweetIdea,
originalTweetId: originalTweet.id,
userId: user.id
},
});
console.log('newIdea saved to DB: ', newIdea);
}

const interestingTweetDraft = await context.entities.TweetDraft.create({
data: {
content: draft.interestingTweet,
originalTweetId: originalTweet.id,
notes: draft.notes,
userId: user.id
},
});

console.log('interestingTweetDraft saved to DB: ', interestingTweetDraft);

// create a delay to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, 1000));

}
await new Promise((resolve) => setTimeout(resolve, 1000));
}

} catch (error: any) {
console.log('error', error);
throw new HttpError(500, error);
}
}

Ok! Nice work. There’s a lot going on above, so let’s just recap:

  • We loop through the array of our favorite users, as defined on our user entity in main.wasp,
  • Pull that user’s most recent tweets
  • Send that tweet to our generateIdeas function, which
    • searches our vector store for similar notes
    • asks GPT to generate similar, new ideas
    • sends those ideas in another prompt GPT to create a new, interesting tweet
    • returns the new ideas and interesting tweet
  • Create new GeneratedIdeas and a TweetDraft and saves them to our Postgres DB

Phew! We’re doing it 💪 

Fetching & Displaying Ideas

Defining a Server-side Query

Since we now have our chain of GPT prompts defined via LangChain and our server-side action, let’s go ahead and start implementing some front-end logic to fetch that data and display it to our users… which is basically only us at this point 🫂.

Just as we added a server-side action to generateNewIdeas we will now define a query to fetch those ideas.

Add the following query to your main.wasp file:

query getTweetDraftsWithIdeas {
fn: import { getTweetDraftsWithIdeas } from "@server/ideas.js",
entities: [TweetDraft]
}

In your src/server/ideas.ts file, below your generateNewIdeas action, add the query we just defined in our wasp file:

//... other imports ...
import type { GetTweetDraftsWithIdeas } from '@wasp/queries/types'; // <--- add this ---

// ... other functions ...

type TweetDraftsWithIdeas = {
id: number;
content: string;
notes: string;
createdAt: Date;
originalTweet: {
id: number;
content: string;
tweetId: string;
tweetedAt: Date;
ideas: GeneratedIdea[];
authorUsername: string;
};
}[];

export const getTweetDraftsWithIdeas: GetTweetDraftsWithIdeas<unknown, TweetDraftsWithIdeas> = async (_args, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

const drafts = await context.entities.TweetDraft.findMany({
orderBy: {
originalTweet: {
tweetedAt: 'desc',
}
},
where: {
userId: context.user.id,
createdAt: {
gte: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // Get drafts created within the last 2 days
},
},
select: {
id: true,
content: true,
notes: true,
createdAt: true,
originalTweet: {
select: {
id: true,
tweetId: true,
content: true,
ideas: true,
tweetedAt: true,
authorUsername: true,
},
},
},
});

return drafts;
};

With this function we will be returning the tweet drafts we generate, along with our notes, the original tweet that inspired it, and the newly generated ideas.

Sweet!

Ok, but what good is a function that fetches the data if we’ve got nowhere to display it!?

Displaying Ideas Client-side

Let’s go now to our src/client/MainPage.tsx file (make sure it’s got the .tsx extension and not .jsx) and replace the contents with these below:

import waspLogo from './waspLogo.png'
import './Main.css'

const MainPage = () => {
return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col justify-center items-center mx-auto pt-12'>
<img src={waspLogo} className='w-5' />
</div>
</div>
)
}
export default MainPage

At this point, you. might need to restart the wasp dev server running in your terminal to get the tailwind configuration to take effect (ctrl + c, then wasp start again).

You’ll now be prompted with the login / register screen. Go ahead and click on register and you will be automatically logged in and redirected to the main page, which at this point only has this:

Untitled

Let’s go back to our MainPage.tsx file and add the magic!

https://media3.giphy.com/media/ekv45izCuyXkXoHRaL/giphy.gif?cid=7941fdc6c3dszwj4xaoxg2kyj6xxdubjxn69m4qruhomhkut&ep=v1_gifs_search&rid=giphy.gif&ct=g

First, let’s create a buttons component so we don’t have to constantly style a new button. Create a new src/client/Button.tsx file:

import { ButtonHTMLAttributes } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
isLoading?: boolean;
}

export default function Button({ isLoading, children, ...otherProps }: ButtonProps) {
return (
<button
{...otherProps}
className={`flex flex-row justify-center items-center bg-neutral-100 hover:bg-neutral-200 border border-neutral-300 text-blue-500 font-bold px-3 py-1 text-sm rounded-lg ${isLoading ? ' pointer-events-none opacity-70' : 'cursor-pointer'}`}
>
{isLoading? 'Loading...' : children}
</button>
);
}

Now let’s add it to your AddNote.tsx component, replacing the original button with this one. The whole file should look like this:

import { useState } from 'react';
import embedIdea from '@wasp/actions/embedIdea';
import Button from './Button';

export default function AddNote() {
const [idea, setIdea] = useState('');
const [isIdeaEmbedding, setIsIdeaEmbedding] = useState(false);

const handleEmbedIdea = async (e: any) => {
try {
setIsIdeaEmbedding(true);
if (!idea) {
throw new Error('Idea cannot be empty');
}
const embedIdeaResponse = await embedIdea({
idea,
});

console.log('embedIdeaResponse: ', embedIdeaResponse);
} catch (error: any) {
alert(error.message);
} finally {
setIdea('');
setIsIdeaEmbedding(false);
}
};

return (
<div className='flex flex-row gap-2 justify-center items-end w-full'>
<textarea
autoFocus
onChange={(e) => setIdea(e.target.value)}
value={idea}
placeholder='LLMs are great for brainstorming!'
className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'
/>
<Button isLoading={isIdeaEmbedding} onClick={handleEmbedIdea}>
Save Note
</Button>
</div>
);
}

Noice.

Next, we want our page to perform the following actions:

  1. create a button that runs our generateNewIdeas action when clicked
  2. define the query that fetches and caches the tweet drafts and ideas
  3. loop through the results and display them on the page

That’s exactly what the below code will do. Go ahead and replace the MainPage with it and take a minute to review what’s going on:

import waspLogo from './waspLogo.png';
import './Main.css';
import { useState } from 'react';
import generateNewIdeas from '@wasp/actions/generateNewIdeas';
import { useQuery } from '@wasp/queries';
import getTweetDraftsWithIdeas from '@wasp/queries/getTweetDraftsWithIdeas';
import AddNote from './AddNote';
import Button from './Button';

const MainPage = () => {
const [isGenerating, setIsGenerating] = useState(false);

const {
data: tweetDrafts,
isLoading: isTweetDraftsLoading,
error: tweetDraftsError,
} = useQuery(getTweetDraftsWithIdeas);

const handleNewIdeas = async (e: any) => {
try {
setIsGenerating(true);
await generateNewIdeas();
} catch (error: any) {
alert(error.message);
} finally {
setIsGenerating(false);
}
};

if (isTweetDraftsLoading) {
return 'Loading...';
}

if (tweetDraftsError) {
return 'Error: ' + tweetDraftsError.message;
}

return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col gap-6 justify-center items-center mx-auto pt-12'>
<img src={waspLogo} className='w-5' />
<div className='flex flex-col gap-4 justify-center items-center w-2/4'>
<AddNote />
<hr className='border border-t-1 border-neutral-100/70 w-full' />
<div className='flex flex-row justify-center w-1/4'>
<Button onClick={handleNewIdeas} isLoading={isGenerating}>
Generate New Ideas
</Button>
</div>
<div className='flex flex-col gap-4 justify-center items-center w-full'>
{tweetDrafts.map((tweetDraft) => (
<>
<h2 className='text-2xl font-bold'>Generated Ideas</h2>
<div key={tweetDraft.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Tweet Draft</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{tweetDraft.content}</div>
</div>

{!!tweetDraft.notes && tweetDraft.notes !== tweetDraft.originalTweet.content && (
<>
<h2>Your Similar Notes</h2>
{tweetDraft.notes}
</>
)}
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Ideas</h2>
{tweetDraft.originalTweet.ideas.map((idea) => (
<div key={idea.id} className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'>{idea.content}</div>
</div>
</div>
))}
</div>
</div>
</>
))}
</div>
</div>
</div>
</div>
);
};
export default MainPage;

This is what you should see on the homepage now! 🎉

Untitled

But, if you clicked ‘generate new ideas’ and nothing happened, well that’s because we haven’t defined any favorite trend-setting twitter users to scrape tweets from. And there’s no way to do that from the UI at the moment, so let’s open up the database manager and add some manually.

In a new terminal tab, in the root of your project, run:

wasp db studio

Then, in a new browswer tab, at localhost:5555 you should see your database.

Go to user, and you should be the only user in there. Add the usernames of a couple of your favorite trend-setting twitter users.

Untitled

Make sure the accounts have tweeted recently or your function won’t be able to scrape or generate anything!

Hey ✋

While you’re at it, if you’re liking this tutorial, give me a follow @hot_town for more future content like this

After adding the twitter usernames, make sure you click save 1 change.

Go back to your client and click the Generate New Ideas button again. This might take a while depending on how many tweets it’s generating ideas for, so be patient — and watch the console output in your terminal if you’re curious ;)

Untitled

Awesome! Now we should be getting back some generated ideas from our twitter “intern” which will help us brainstorm further notes and generate our own BANGER TWEETS.

But it would be cool to also display the tweet these ideas are referencing from the beginning. That way we’d have a bit more context on where the ideas came from.

Let’s do that then! In your MainPage file, at the very top, add the following import:

import { TwitterTweetEmbed } from 'react-twitter-embed';

This allows us to embed tweets with that nice twitter styling.

We already added this dependency to our main.wasp file at the beginning of the tutorial, so we can just import and start embedding tweets.

Let’s try it out now in our MainPage by adding the following snippet above our <h2>Tweet Draft</h2> element:

//...

<h2>Original Tweet</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<TwitterTweetEmbed tweetId={tweetDraft.originalTweet.tweetId} />
</div>

<h2>Tweet Draft</h2>
//...

Great. Now we should be sitting pretty 😻

Untitled

You might remember from the beginning of the tutorial when we defined the LLM calls, that if your vector store notes don’t turn back a cosine similarity of at least 0.7, your agent will generate its own ideas entirely without using your notes as a guide.

And since we have NO notes in our vector store at the moment, that’s exactly what it is doing. Which is fine, because we can let it brainstorm for us, and we can select our favorite notes and edit and add them as we see fit.

So you can go ahead and start adding notes whenever you feel like it 📝.

But, we’ve added our favorite twitter users to the database manually. It would be preferable to do it via an account settings page, right? Let’s make one then.

Creating an Account Settings Page

First, add the route and page to your main.wasp config file, under the other routes:

//...

route AccountRoute { path: "/account", to: AccountPage }
page AccountPage {
authRequired: true,
component: import Account from "@client/AccountPage"
}

Next, let’s create a new page, src/client/AccountPage.tsx:

import Button from './Button';
import { ChangeEvent, useEffect, useState } from 'react';
import logout from '@wasp/auth/logout';
import type { User } from '@wasp/entities';

const AccountPage = ({ user }: { user: User }) => {
return (
<div className='flex flex-col justify-center items-center mt-12 w-full'>
<div className='flex flex-col items-center justify-center gap-4 border border-neutral-700 bg-neutral-100/40 rounded-xl p-1 sm:p-4 w-full'>
<div className='flex flex-row justify-end w-full px-4 pt-2'>
<Button onClick={logout}>Logout</Button>
</div>
{JSON.stringify(user, null, 2)}
</div>
</div>
);
};

export default AccountPage;

When you navigate to localhost:3000/account, you’ll notice two things, one of them being a logout button. You can see in our SettingsPage above that we imported a Wasp-provided logout function. We get this “for free” since we defined our auth strategy in the main.wasp file — a big time-saver!

Untitled

Because we also defined the AccountPage route with the authRequired: true property, Wasp will automatically pass the logged in user as a prop argument to our page. We can use the user object to display and update our favUsers, just as we can see in the image above.

To do that, let’s define a new updateAccount action in our main.wasp file:

action updateAccount {
fn: import { updateAccount } from "@server/account.js",
entities: [User]
}

Next, let’s create the updateAccount action in a new file, src/server/account.ts:

import type { UpdateAccount } from "@wasp/actions/types";
import HttpError from "@wasp/core/HttpError.js";

export const updateAccount: UpdateAccount<{ favUsers: string[] }, void> = async ({ favUsers }, context) => {
if (!context.user) {
throw new HttpError(401, "User is not authorized");
}

try {
await context.entities.User.update({
where: { id: context.user.id },
data: { favUsers },
});

} catch (error: any) {
throw new HttpError(500, error.message);
}
}

Right. Now it’s time to put it all together in our Account page. We’re going to create a form for adding new twitter users to scrape tweets from, so at the bottom of your src/client/AccountPage.tsx, below your other code, add the following component:

function InputFields({ user }: { user: User }) {
const [isLoading, setIsLoading] = useState(false);
const [fields, setFields] = useState(['']);

useEffect(() => {
if (user?.favUsers.length > 0) {
setFields(user.favUsers);
}
}, [user?.favUsers]);

const handleAdd = () => {
setFields([...fields, '']);
};

const handleRemove = () => {
const newFields = [...fields];
newFields.splice(fields.length - 1, 1);
setFields(newFields);
};

const handleChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
const newFields = [...fields];
newFields[index] = e.target.value;
setFields(newFields);
};

const handleSubmit = async () => {
//...
};

return (
<div className='w-full p-4'>
<div className='flex flex-row justify-start items-start'>
<h2 className='ml-1 font-bold'>Trend-Setting Twitter Accounts</h2>
</div>
{fields.map((field, index) => (
<div key={index} className='my-2'>
<input
type='text'
placeholder='Twitter Username'
className='w-full bg-white border border-gray-300 rounded-lg py-2 px-4 text-gray-700 focus:border-blue-400 focus:outline-none'
value={field}
onChange={(e) => handleChange(e, index)}
/>
</div>
))}
<div className='my-2 flex flex-row justify-end gap-1'>
{fields.length > 1 && <Button onClick={handleRemove}>-</Button>}
{fields.length < 10 && (
<Button onClick={handleAdd} className='bg-blue-500 text-white px-4 py-2 rounded'>
+
</Button>
)}
</div>
<Button onClick={handleSubmit} isLoading={isLoading}>
<span>Save</span>
</Button>
</div>
);
}

This component takes care of adding the logged in user’s favUsers array to state, and displaying that in information in a set of input components.

The only thing missing from it is to add our updateAccount action we just defined earlier. So at the top of the file, let’s import it and add the logic to our InputFields submit handler

import updateAccount from '@wasp/actions/updateAccount'; // <--- add this import

//...

const handleSubmit = async () => { // < --- add this function
try {
setIsLoading(true);
await updateAccount({ favUsers: fields });
} catch (err: any) {
alert(err.message);
} finally {
setIsLoading(false);
}
};

Also, in your AccountPage make sure to replace the line {JSON.stringify(user, null, 2)} with the newly created component <InputFields user={user} />.

Here is what the entire AccountPage.tsx file should now look like in case you get stuck:

import Button from './Button';
import { ChangeEvent, useEffect, useState } from 'react';
import logout from '@wasp/auth/logout';
import type { User } from '@wasp/entities';
import updateAccount from '@wasp/actions/updateAccount'

const AccountPage = ({ user }: { user: User }) => {
return (
<div className='flex flex-col justify-center items-center mt-12 w-full'>
<div className='flex flex-col items-center justify-center gap-4 border border-neutral-700 bg-neutral-100/40 rounded-xl p-1 sm:p-4 w-full'>
<div className='flex flex-row justify-end w-full px-4 pt-2'>
<Button onClick={logout}>Logout</Button>
</div>
<InputFields user={user} />
</div>
</div>
);
};

export default AccountPage;

function InputFields({ user }: { user: User }) {
const [isLoading, setIsLoading] = useState(false);
const [fields, setFields] = useState(['']);

useEffect(() => {
if (user?.favUsers.length > 0) {
setFields(user.favUsers);
}
}, [user?.favUsers]);

const handleAdd = () => {
setFields([...fields, '']);
};

const handleRemove = () => {
const newFields = [...fields];
newFields.splice(fields.length - 1, 1);
setFields(newFields);
};

const handleChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
const newFields = [...fields];
newFields[index] = e.target.value;
setFields(newFields);
};

const handleSubmit = async () => {
try {
setIsLoading(true);
await updateAccount({ favUsers: fields });
} catch (err: any) {
alert(err.message);
} finally {
setIsLoading(false);
}
};

return (
<div className='w-full p-4'>
<div className='flex flex-row justify-start items-start'>
<h2 className='ml-1 font-bold'>Trend-Setting Twitter Accounts</h2>
</div>
{fields.map((field, index) => (
<div key={index} className='my-2'>
<input
type='text'
placeholder='Twitter Username'
className='w-full bg-white border border-gray-300 rounded-lg py-2 px-4 text-gray-700 focus:border-blue-400 focus:outline-none'
value={field}
onChange={(e) => handleChange(e, index)}
/>
</div>
))}
<div className='my-2 flex flex-row justify-end gap-1'>
{fields.length > 1 && <Button onClick={handleRemove}>-</Button>}
{fields.length < 10 && (
<Button onClick={handleAdd} className='bg-blue-500 text-white px-4 py-2 rounded'>
+
</Button>
)}
</div>
<Button onClick={handleSubmit} isLoading={isLoading}>
<span>Save</span>
</Button>
</div>
);
}

And here’s what your AccountPage should look like when navigating to localhost:3000/account (note: the styling may be a bit ugly, but we’ll take care of that later):

Untitled

Fantastic. So we’ve got the majority of the app logic finished — our own personal twitter “intern” to help us all become thought leaders and thread bois 🤣.

Adding a Cron Job

But wouldn’t it be cool if we could automate the Generate New Ideas process? Each time you click the button, it takes quite a while for tweets to be scraped, and ideas to be generated, especially if we are generating ideas for a lot of new tweets.

So it would be nicer if we had a cron job (recurring task), that ran automatically in the background at a set interval.

With Wasp, that’s also super easy to set up. To do so, let’s go to our main.wasp file and add our job at the very bottom:

//...

job newIdeasJob {
executor: PgBoss,
perform: {
fn: import generateNewIdeasWorker from "@server/worker/generateNewIdeasWorker.js"
},
entities: [User, GeneratedIdea, Tweet, TweetDraft],
schedule: {
// run cron job every 30 minutes
cron: "*/30 * * * *",
executorOptions: {
pgBoss: {=json { "retryLimit": 2 } json=},
}
}
}

Let’s run through the code above:

  • Jobs use pg-boss, a postgres extension, to queue and run tasks under the hood.
  • with perform we’re telling the job what function we want it to call: generateNewIdeasWorker
  • just like actions and queries, we have to tell the job which entities we want to give it access to. In this case, we will need access to all of our entities.
  • the schedule allows us to pass some options to pg-boss so that we can make it a recurring task. In this case, I set it to run every 30 minutes, but you can set it to any interval you’d like (tip: change the comment and let github co-pilot write the cron for you). We also tell pg-boss to retry a failed job two times.

Perfect. So now, our app will automatically scrape our favorite users’ tweets and generate new ideas for us every 30 minutes. This way, if we revisit the app after a few days, all the content will already be there and we won’t have to wait a long time for it to generate it for us. We also make sure we never miss out on generating ideas for older tweets.

But for that to happen, we have to define the function our job will call. To do this, create a new directory worker within the server folder, and within it a new file: src/server/worker/generateNewIdeasWorker

import { generateNewIdeas } from '../ideas.js';

export default async function generateNewIdeasWorker(_args: unknown, context: any) {
try {
console.log('Running recurring task: generateNewIdeasWorker')
const allUsers = await context.entities.User.findMany({});

for (const user of allUsers) {
context.user = user;
console.log('Generating new ideas for user: ', user.username);
await generateNewIdeas(undefined as never, context);
console.log('Done generating new ideas for user: ', user.username)
}

} catch (error: any) {
console.log('Recurring task error: ', error);
}
}

In this file, all we’re doing is looping through all the users in our database, and passing them via the context object to our generateNewIdeas action. The nice thing about jobs is that Wasp automatically passes the context object to these functions, which we can then pass along to our action.

So now, at the interval that you set (e.g. 30 minutes), you should notice the logs being printed to the console whenever your job starts automatically running.

[Server]  Generating new ideas for user:  vinny

Alright, things are looking pretty good now, but let’s not forget to add a page to view all the notes we added and embedded to our vector store!

Adding a Notes Page

Go ahead and add the following route to your main.wasp file:

route NotesPage { path: "/notes", to: NotesPage }
page NotesPage {
authRequired: true,
component: import Notes from "@client/NotesPage"
}

Create the complementary page, src/client/NotesPage.tsx and add the following boilerplate just to get started (we’ll add the rest later):

const NotesPage = () => {

return (
<>Notes</>
);
};

export default NotesPage;

It would be nice if we had a simple Nav Bar to navigate back and forth between our two pages. It would also be cool if we had our <AddNote /> input component on all pages, that way it’s easy for us to add an idea whenever inspiration strikes.

Rather than copying the NavBar and AddNote code to both pages, let’s create a wrapper, or “root”, component for our entire app so that all of our pages have the same Nav Bar and layout.

To do that, in our main.wasp file, let’s define our root component by adding a client property to our app configuration at the very top of the file. This is how the entire app object should look like now:

app twitterAgent {
wasp: {
version: "^0.10.6"
},
title: "twitter-agent",
client: {
rootComponent: import App from "@client/App",
},
db: {
system: PostgreSQL,
},
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/login",
methods: {
usernameAndPassword: {},
}
},
dependencies: [
("openai", "3.2.1"),
("rettiwt-api", "1.1.8"),
("langchain", "0.0.91"),
("@pinecone-database/pinecone", "0.1.6"),
("@headlessui/react", "1.7.15"),
("react-icons", "4.8.0"),
("react-twitter-embed", "4.0.4")
],
}

// entities, operations, routes, and other stuff...

Next, create a new file src/client/App.tsx with the following content:

import './Main.css';
import AddNote from './AddNote';
import { ReactNode } from 'react';
import useAuth from '@wasp/auth/useAuth';

const App = ({ children }: { children: ReactNode }) => {

const { data: user } = useAuth();

return (
<div className='min-h-screen bg-neutral-300/70 text-center'>
<div className='flex flex-col gap-6 justify-center items-center mx-auto pt-12'>
<div className='flex flex-row justify-between items-center w-1/2 mb-6 text-neutral-600 px-2'>
<div className='flex justify-start w-1/3'>
<a href='/' className='hover:underline cursor-pointer'>
🤖 Generated Ideas
</a>
</div>
<div className='flex justify-center w-1/3'>
<a href='/notes' className='hover:underline cursor-pointer'>
📝 My Notes
</a>
</div>
<div className='flex justify-end w-1/3'>
<a href='/account' className='hover:underline cursor-pointer'>
👤 Account
</a>
</div>
</div>

<div className='flex flex-col gap-4 justify-center items-center w-2/4'>
{!!user && <AddNote />}
<hr className='border border-t-1 border-neutral-100/70 w-full' />
{children}
</div>
</div>
</div>
);
};

export default App;

With this defined, Wasp will know to pass all other routes as children through our App component. That way, we will always show the Nav Bar and AddNote component on the top of every page.

We also take advantage of Wasp’s handy useAuth hook to check if a user is logged in, and if so we show the AddNote component.

Now, we can delete the duplicate code on our MainPage. This is what it should look like now:

import { useState } from 'react';
import generateNewIdeas from '@wasp/actions/generateNewIdeas';
import { useQuery } from '@wasp/queries';
import getTweetDraftsWithIdeas from '@wasp/queries/getTweetDraftsWithIdeas';
import Button from './Button';
import { TwitterTweetEmbed } from 'react-twitter-embed';

const MainPage = () => {
const [isGenerating, setIsGenerating] = useState(false);

const {
data: tweetDrafts,
isLoading: isTweetDraftsLoading,
error: tweetDraftsError,
} = useQuery(getTweetDraftsWithIdeas);

const handleNewIdeas = async (e: any) => {
try {
setIsGenerating(true);
await generateNewIdeas();
} catch (error: any) {
alert(error.message);
} finally {
setIsGenerating(false);
}
};

if (isTweetDraftsLoading) {
return 'Loading...';
}

if (tweetDraftsError) {
return 'Error: ' + tweetDraftsError.message;
}

return (
<>
<div className='flex flex-row justify-center w-full'>
<Button onClick={handleNewIdeas} isLoading={isGenerating}>
Generate New Ideas
</Button>
</div>
<div className='flex flex-col gap-4 justify-center items-center w-full'>
{tweetDrafts.map((tweetDraft) => (
<>
<h2 className='text-2xl font-bold'>Generated Ideas</h2>
<div key={tweetDraft.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Original Tweet</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<TwitterTweetEmbed tweetId={tweetDraft.originalTweet.tweetId} />
</div>
<h2>Tweet Draft</h2>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{tweetDraft.content}</div>
</div>

{!!tweetDraft.notes && tweetDraft.notes !== tweetDraft.originalTweet.content && (
<>
<h2>Your Similar Notes</h2>
{tweetDraft.notes}
</>
)}
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<h2>Ideas</h2>
{tweetDraft.originalTweet.ideas.map((idea) => (
<div key={idea.id} className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-neutral-100 border rounded-lg w-full'>{idea.content}</div>
</div>
</div>
))}
</div>
</div>
</>
))}
</div>
</>
);
};
export default MainPage;

Next, we need to create a query that allows us to fetch all of our added notes and ideas that have been embedded in our vector store.

For that, we need to define a new query in our main.wasp file:

query getEmbeddedNotes {
fn: import { getEmbeddedNotes } from "@server/ideas.js",
entities: [GeneratedIdea]
}

We then need to create that query at the bottom of our src/actions/ideas.ts file:

// first import the type at the top of the file
import type { GetEmbeddedNotes, GetTweetDraftsWithIdeas } from '@wasp/queries/types';

//...

export const getEmbeddedNotes: GetEmbeddedNotes<never, GeneratedIdea[]> = async (_args, context) => {
if (!context.user) {
throw new HttpError(401, 'User is not authorized');
}

const notes = await context.entities.GeneratedIdea.findMany({
where: {
userId: context.user.id,
isEmbedded: true,
},
orderBy: {
createdAt: 'desc',
},
});

return notes;
}

Now let’s go back to our src/client/NotesPage.tsx and add our query. Our new file will look like this:

import { useQuery } from '@wasp/queries';
import getEmbeddedNotes from '@wasp/queries/getEmbeddedNotes';

const NotesPage = () => {
const { data: notes, isLoading, error } = useQuery(getEmbeddedNotes);

if (isLoading) <div>Loading...</div>;
if (error) <div>Error: {error.message}</div>;

return (
<>
<h2 className='text-2xl font-bold'>My Notes</h2>
{notes && notes.length > 0 ? (
notes.map((note) => (
<div key={note.id} className='flex flex-col gap-2 justify-center items-center w-full'>
<div className='flex flex-row gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>{note.content}</div>
</div>
</div>
))
) : notes && notes.length === 0 && (
<div className='flex flex-col gap-2 justify-center items-center w-full'>
<div className='w-full p-4 h-22 bg-blue-100 border rounded-lg w-full'>No notes yet</div>
</div>
)}
</>
);
};

export default NotesPage;

Cool! Now we should be fetching all our embedded notes and ideas, signified by the isEmbedded tag in our postgres database. Your Notes page should now look something like this:

Untitled

You Did it! Your own Twitter Intern 🤖

Help me help you

🌟 If you haven’t yet, please star us on GitHub, 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

And that’s it! You’ve now got yourself a semi-autonomous twitter brainstorming agent to help inspire new ideas and keep you actively contributing 🚀

There’s way more you can do with these tools, but this is a great start.

Remember, if you want to see a more advanced version of this app which utilizes the official Twitter API to send tweets, gives you the ability to edit and add generated notes on the fly, has manual similarity search for all your notes, and more, then you can check out the 💥 Banger Tweet Bot 🤖.

And, once again, here's the repo for the finished app we built in this tutorial: Personal Twitter Intern

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/06/28/what-can-you-build-with-wasp.html b/blog/2023/06/28/what-can-you-build-with-wasp.html index 3a86a72d11..5018668a4b 100644 --- a/blog/2023/06/28/what-can-you-build-with-wasp.html +++ b/blog/2023/06/28/what-can-you-build-with-wasp.html @@ -19,13 +19,13 @@ - - + +
-

What can you build with Wasp?

· 4 min read
Matija Sosic

Launch Week 3 is coming

Welcome to the 3rd day of our Launch Week #3 - Community Day! Our community is the most important aspect of everything we do at Wasp, and we believe it's only right to have a day dedicated to it.

We'll showcase some of the coolest project built with Wasp so far and through that explore together what kind of apps you can develop with it. Let's dive in!

tip

If you're looking for a quick way to start your project, check out our Ultimate SaaS Starter. It packs Tailwind, GPT, Stripe ane other popular integrations, all pre-configured for you.

CoverLetterGPT.xyz - GPT-powered cover letter generator

Try it out: coverlettergpt.xyz

Source code: https://github.com/vincanger/coverlettergpt

Wasp features used: Social login with Google + auth UI, email sending

UI Framework: Chakra UI

Created in the midst of a GPT craze, this is one of the most popular Wasp apps so far! It does exactly what it says on a tin - given job description and your CV, it generates a unique cover letter customized for you. It does that via parsing your CV and feeding it together with the job description to the GPT api, along with the additional settings such as creativity level (careful with that one!).

Although it started as a fun side project, it seems that people actually find it useful, at least as a starting point for writing your own cover letter. CoverLetterGPT has been used to generate close to 5,000 cover letters!

Try it out and have fun or use it as an inspiration for your next project!

Amicus.work - most "enterprise SaaS" app 👔 💼

Try it out: amicus.work

Wasp features used: Authentication, email sending, async/cron jobs

UI Framework: Material UI

This app really gives away those "enterprise SaaS" vibes - when you see it you know it means some serious business! The author describes it as "Asana for you lawyers" (you can read how the author got first customers for it here), or as an easy way for lawyers to manage and collaborate on their workflows.

File upload, workflow creation, calendar integration, collaboration - this app has it all! Amicus might be the most advanced project made with Wasp so far. Erlis startedbuilding it even with Wasp still in Alpha, and it has withstood the test of time since then.

Description Generator - GPT-powered product description generator - first acquired app made with Wasp! 💰💰

Try it out: description-generator.online

Wasp features used: Social login with Google + auth UI

UI Framework: Chakra UI

Another SaaS that uses GPT integration to cast its magic! Given product name and instructions on what kind of content you'd like to get, this app generates the professionaly written product listing. It's a perfect fit for marketplace owners that want to present their products in the best light but don't have a budget for the marketing agency.

What's special about Description Generator is that it was recently sold , making it the first Wasp-powered project that got acquired! Stay tuned, as the whole story is coming soon.

TweetBot - your personal Twitter intern! 🐦🤖

Try it out: banger-tweet-bot.netlify.app

Source code: https://github.com/vincanger/banger-tweet-bot

Wasp features used:Authentication, async/cron jobs

UI Framework: Tailwind

The latest and greatest from Vince's lab - an app that serves as your personal twitter brainstorming agent! It takes your raw ideas as an input, monitors current twitter trends (from the accounts you selected) and helps you brainstorm new tweets and also drafts them for you!

While the previously mentioned projects queried the GPT API directly, TweetBot makes use of the LangChain library, which does a lot of heavy lifting for you, allowing you to produce bigger prompts and preserve the context between subsequent queries.

Summary

As you could see above, Wasp can be used to build pretty much any database-backed web application! It is especially well suited for so called "workflow-based" applications where you typically have a bunch of resources (e.g. your tasks, or tweets) that you want to manipulate in some way.

With our built-in deployment support (e.g. you can deploy to Fly.io for free with a single CLI command) the whole development process is extremely streamlined.

We can't wait to see what you build next!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

What can you build with Wasp?

· 4 min read
Matija Sosic

Launch Week 3 is coming

Welcome to the 3rd day of our Launch Week #3 - Community Day! Our community is the most important aspect of everything we do at Wasp, and we believe it's only right to have a day dedicated to it.

We'll showcase some of the coolest project built with Wasp so far and through that explore together what kind of apps you can develop with it. Let's dive in!

tip

If you're looking for a quick way to start your project, check out our Ultimate SaaS Starter. It packs Tailwind, GPT, Stripe ane other popular integrations, all pre-configured for you.

CoverLetterGPT.xyz - GPT-powered cover letter generator

Try it out: coverlettergpt.xyz

Source code: https://github.com/vincanger/coverlettergpt

Wasp features used: Social login with Google + auth UI, email sending

UI Framework: Chakra UI

Created in the midst of a GPT craze, this is one of the most popular Wasp apps so far! It does exactly what it says on a tin - given job description and your CV, it generates a unique cover letter customized for you. It does that via parsing your CV and feeding it together with the job description to the GPT api, along with the additional settings such as creativity level (careful with that one!).

Although it started as a fun side project, it seems that people actually find it useful, at least as a starting point for writing your own cover letter. CoverLetterGPT has been used to generate close to 5,000 cover letters!

Try it out and have fun or use it as an inspiration for your next project!

Amicus.work - most "enterprise SaaS" app 👔 💼

Try it out: amicus.work

Wasp features used: Authentication, email sending, async/cron jobs

UI Framework: Material UI

This app really gives away those "enterprise SaaS" vibes - when you see it you know it means some serious business! The author describes it as "Asana for you lawyers" (you can read how the author got first customers for it here), or as an easy way for lawyers to manage and collaborate on their workflows.

File upload, workflow creation, calendar integration, collaboration - this app has it all! Amicus might be the most advanced project made with Wasp so far. Erlis startedbuilding it even with Wasp still in Alpha, and it has withstood the test of time since then.

Description Generator - GPT-powered product description generator - first acquired app made with Wasp! 💰💰

Try it out: description-generator.online

Wasp features used: Social login with Google + auth UI

UI Framework: Chakra UI

Another SaaS that uses GPT integration to cast its magic! Given product name and instructions on what kind of content you'd like to get, this app generates the professionaly written product listing. It's a perfect fit for marketplace owners that want to present their products in the best light but don't have a budget for the marketing agency.

What's special about Description Generator is that it was recently sold , making it the first Wasp-powered project that got acquired! Stay tuned, as the whole story is coming soon.

TweetBot - your personal Twitter intern! 🐦🤖

Try it out: banger-tweet-bot.netlify.app

Source code: https://github.com/vincanger/banger-tweet-bot

Wasp features used:Authentication, async/cron jobs

UI Framework: Tailwind

The latest and greatest from Vince's lab - an app that serves as your personal twitter brainstorming agent! It takes your raw ideas as an input, monitors current twitter trends (from the accounts you selected) and helps you brainstorm new tweets and also drafts them for you!

While the previously mentioned projects queried the GPT API directly, TweetBot makes use of the LangChain library, which does a lot of heavy lifting for you, allowing you to produce bigger prompts and preserve the context between subsequent queries.

Summary

As you could see above, Wasp can be used to build pretty much any database-backed web application! It is especially well suited for so called "workflow-based" applications where you typically have a bunch of resources (e.g. your tasks, or tweets) that you want to manipulate in some way.

With our built-in deployment support (e.g. you can deploy to Fly.io for free with a single CLI command) the whole development process is extremely streamlined.

We can't wait to see what you build next!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/06/29/new-wasp-lsp.html b/blog/2023/06/29/new-wasp-lsp.html index 91c4859a09..d6aeb36c29 100644 --- a/blog/2023/06/29/new-wasp-lsp.html +++ b/blog/2023/06/29/new-wasp-lsp.html @@ -19,13 +19,13 @@ - - + +
-

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

· 2 min read
Matija Sosic

It's the fourth day of our Launch Week #3 - today it's all about dev tooling and making sure that the time you spend looking at your IDE is as pleasurable as possible!

We present the next generation of Wasp LSP (Language Server Protocol) implementation for VS Code! As you might already know, Wasp has its own simple configuration language (.wasp) that acts as a glue between your React & Node.js code.

Although it's a very simple, declarative language (you can think of it as a bit nicer/smarter JSON), and having it allows us to completely tailor the developer experience (aka get rid of boilerplate), it also means we have to provide our own tooling for it (syntax highlighting, auto completion, ...).

We started with syntax highlighting, then basic autocompletion and snippet support, but now we really took things to the next level! Writing Wasp code now is much closer to what we had in our mind when envisioning Wasp.

Without further ado, here's what's new:

✨ Autocompletion for config object properties (auth, webSocket, ...)

Until now, Wasp offered autocompletion only for the top-level declarations such as page or app. Now, it works for any (sub)-property (as one would expect 😅)!

Fill out your Wasp configuration faster and with less typos! 💻🚀

🔍 Type Hints

Opening documentation takes you out of your editor and out of your flow. Stay in the zone with in-editor type hints! 💡

🚨 Import Diagnostics

Keep tabs on what's left to implement with JS import diagnostics! There's nothing more satisfying than watching those errors vanish. 😌

Wasp now automatically detects if the function you referenced doesn't exist or is not exported.

🔗 Goto Definition

Your Wasp file is the central hub of your project. Easily navigate your code with goto definition and make changes in a snap! 💨

Cmd/Ctrl + click and Wasp LSP takes you straight to the function body!

Don't forget to install Wasp VS Code extension and we wish you happy coding! You can get started right away and try it out here.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

· 2 min read
Matija Sosic

It's the fourth day of our Launch Week #3 - today it's all about dev tooling and making sure that the time you spend looking at your IDE is as pleasurable as possible!

We present the next generation of Wasp LSP (Language Server Protocol) implementation for VS Code! As you might already know, Wasp has its own simple configuration language (.wasp) that acts as a glue between your React & Node.js code.

Although it's a very simple, declarative language (you can think of it as a bit nicer/smarter JSON), and having it allows us to completely tailor the developer experience (aka get rid of boilerplate), it also means we have to provide our own tooling for it (syntax highlighting, auto completion, ...).

We started with syntax highlighting, then basic autocompletion and snippet support, but now we really took things to the next level! Writing Wasp code now is much closer to what we had in our mind when envisioning Wasp.

Without further ado, here's what's new:

✨ Autocompletion for config object properties (auth, webSocket, ...)

Until now, Wasp offered autocompletion only for the top-level declarations such as page or app. Now, it works for any (sub)-property (as one would expect 😅)!

Fill out your Wasp configuration faster and with less typos! 💻🚀

🔍 Type Hints

Opening documentation takes you out of your editor and out of your flow. Stay in the zone with in-editor type hints! 💡

🚨 Import Diagnostics

Keep tabs on what's left to implement with JS import diagnostics! There's nothing more satisfying than watching those errors vanish. 😌

Wasp now automatically detects if the function you referenced doesn't exist or is not exported.

🔗 Goto Definition

Your Wasp file is the central hub of your project. Easily navigate your code with goto definition and make changes in a snap! 💨

Cmd/Ctrl + click and Wasp LSP takes you straight to the function body!

Don't forget to install Wasp VS Code extension and we wish you happy coding! You can get started right away and try it out here.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/06/30/tutorial-jam.html b/blog/2023/06/30/tutorial-jam.html index 1cc55d9d8c..932253a95e 100644 --- a/blog/2023/06/30/tutorial-jam.html +++ b/blog/2023/06/30/tutorial-jam.html @@ -19,15 +19,15 @@ - - + +
-

Tutorial Jam #1 - Teach Others & Win Prizes!

· 4 min read
Vinny

Introduction

The Wasp Tutorial Jam is a contest where participants are required to create a tutorial about building a fullstack React/Node app with Wasp.

Wait, What’s Wasp?

First of all, it’s sad that you’ve never heard of Wasp.

https://media0.giphy.com/media/kr5PszPQawIRq/giphy.gif?cid=7941fdc6gwgjf866b0akslgciedh53jf9narttadkglvvcp0&ep=v1_gifs_search&rid=giphy.gif&ct=g

Wasp is a unique fullstack framework for building React/NodeJS/Prisma/Tanstack Query apps.

Because it’s based on a compiler, you write a simple config file, and Wasp can take care of generating the skeleton of your app for you (and regenerating when the config file changes). You can read more about Wasp here

Rules

The rules are simple. The tutorial must:

- - + + \ No newline at end of file diff --git a/blog/2023/07/10/gpt-web-app-generator.html b/blog/2023/07/10/gpt-web-app-generator.html index 42a495b837..42c595d6a9 100644 --- a/blog/2023/07/10/gpt-web-app-generator.html +++ b/blog/2023/07/10/gpt-web-app-generator.html @@ -19,14 +19,14 @@ - - + +
-

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

· 6 min read
Martin Sosic

This project started out as an experiment - we were interested if, given a short description, GPT can generate a full-stack web app in React & Node.js. The results went beyond our expectations!

How it works

All you have to do in order to use GPT Web App Generator is provide a short description of your app idea in plain English. You can optionally select your app's brand color and the preferred authentication method (more methods coming soon).

1. Describe your app 2. Pick the color 3. Generate your app 🚀

That's it - in a matter of minutes, a full-stack web app codebase, written in React, Node.js, Prisma, and Wasp, will be generated right in front of you, and available for you to download, run it locally and deploy with a single CLI command!

See a full one-minute demo here:


Check out this blog post if you are interested in technical details of how implemented the Generator!

The stack 📚

Besides React & Node.js, GPT Web App Generator uses Prisma and Wasp.

Prisma is a type-safe database ORM built on top of PostgreSQL. It makes it easy to deal with data models and database migrations.

Wasp is a batteries-included, full-stack framework for React & Node.js. It takes care of everything from front-end to back-end and database along with authentication, sending emails, async jobs, deployment, and more.

Additionaly, all the code behind GPT Web App Generator is completely open-source: web app, GPT code agent.

What kind of apps can I build with it?

caution

Since this is a GPT-powered project, it's output is not 100% deterministic and small mistakes will sometimes occur in the generated code. For the typical examples of web apps (as seen below) they are usually very minor and straightforward to fix. +

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

· 6 min read
Martin Sosic

This project started out as an experiment - we were interested if, given a short description, GPT can generate a full-stack web app in React & Node.js. The results went beyond our expectations!

How it works

All you have to do in order to use GPT Web App Generator is provide a short description of your app idea in plain English. You can optionally select your app's brand color and the preferred authentication method (more methods coming soon).

1. Describe your app 2. Pick the color 3. Generate your app 🚀

That's it - in a matter of minutes, a full-stack web app codebase, written in React, Node.js, Prisma, and Wasp, will be generated right in front of you, and available for you to download, run it locally and deploy with a single CLI command!

See a full one-minute demo here:


Check out this blog post if you are interested in technical details of how implemented the Generator!

The stack 📚

Besides React & Node.js, GPT Web App Generator uses Prisma and Wasp.

Prisma is a type-safe database ORM built on top of PostgreSQL. It makes it easy to deal with data models and database migrations.

Wasp is a batteries-included, full-stack framework for React & Node.js. It takes care of everything from front-end to back-end and database along with authentication, sending emails, async jobs, deployment, and more.

Additionaly, all the code behind GPT Web App Generator is completely open-source: web app, GPT code agent.

What kind of apps can I build with it?

caution

Since this is a GPT-powered project, it's output is not 100% deterministic and small mistakes will sometimes occur in the generated code. For the typical examples of web apps (as seen below) they are usually very minor and straightforward to fix. If you get stuck, ping us on our Discord.

The generated apps are full-stack and consist of front-end, back-end and database. Here are few of the examples we successfully created:

My Plants - track your plants' watering schedule 🌱🚰

  • See the generated code and run it yourself here

This app does exactly what it says - makes sure that you water your plants on time! It comes with a fully functioning front-end, back-end and the database with User and Plant entities. It also features a full-stack authentication (username & password) and a Tailwind-based design.

The next step would be to add more advanced features, such as email reminders (via Wasp email sending support) when it is time to water your plant.

You can see and download the entire source code and add more features and deploy the app yourself!

ToDo app - a classic ✅

  • See the generated code and run it yourself here

What kind of a demo would this be if it didn't include a ToDo app? GPT Web App Generator successfully scaffolded it, along with all the basic functionality - creating and marking a task as done.

With the foundations in place (full-stack code, authentication, Tailwind CSS design) you can see & download the code here and try it yourself!

Limitations

In order to reduce the complexity and therefore mistakes GPT makes, for this first version of Generator we went with the following limitations regarding generated apps:

  1. No additional npm dependencies.
  2. No additional files beyond Wasp Pages (React) and Operations (Node). So no additional files with React components, CSS, utility JS, images or similar.
  3. No TypeScript, just Javascript.
  4. No advanced Wasp features (e.g. Jobs, Auto CRUD, Websockets, Social Auth, email sending, …).

Summary & next steps

As mentioned above, our goal was to test whether GPT can be effectively used to generate full-stack web applications with React & Node.js. While it's now obvious it can, we have lot of ideas for new features and improvements.

Challenges

While we were expecting the main issue to be the size of context that GPT has, it turned out to be that the bigger issue is its “smarts”, which determine things like its planning capabilities, capacity to follow provided instructions (we had quite some laughs observing how it sometimes ignores our instructions), and capacity to not do silly mistakes. We saw GPT4 give better results than GPT3.5, but both still make mistakes, and GPT4 is also quite slow/expensive. Therefore we are quite excited about the further developments in the field of AI / LLMs, as they will directly affect the quality of the output for the tools like our Generator.

Next features wishlist

  1. Get feedback on this initial experiment - both on the Generator and the Wasp as a framework itself: best place to leave us feedback is on our Discord.
  2. Further improve code agent & web app.
  3. Release new version of wasp CLI that allows generating new Wasp project by providing short description via CLI. Our code agent will then use GPT to generate project on the disk. This is already ready and should be coming out soon.
  4. Also allow Wasp users to use code agent for scaffolding specific parts of their Wasp app → you want to add a new Wasp Page (React)? Run our code agent via Wasp CLI or via Wasp vscode extension and have it generated for you, with initial logic already implemented.
  5. As LLMs progress, try some alternative approaches, e.g. try fine-tuning an LLM with knowledge about Wasp, or give LLM more freedom while generating files and parts of the codebase.
  6. Write a detailed blog post about how we implemented the Generator, which techniques we used, how we designed our prompts, what worked and what didn’t work, … .

Support us! ⭐️

If you wish to express your support for what we are doing, consider giving us a star on Github! Everything we do at Wasp is open source, and your support motivates us and helps us to keep making web app development easier and with less boilerplate.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/07/17/how-we-built-gpt-web-app-generator.html b/blog/2023/07/17/how-we-built-gpt-web-app-generator.html index 473959231e..3bfe88b940 100644 --- a/blog/2023/07/17/how-we-built-gpt-web-app-generator.html +++ b/blog/2023/07/17/how-we-built-gpt-web-app-generator.html @@ -19,15 +19,15 @@ - - + +
-

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

· 23 min read
Martin Sosic

We created GPT Web App Generator, which lets you shortly describe the web app you would like to create, and in a matter of minutes, a full-stack codebase, written in React, Node.js, Prisma, and Wasp, will be generated right in front of you, and available to download and run locally!

We started this as an experiment, to see how well we could use GPT to generate full-stack web apps in Wasp, the open-source JS web app framework that we are developing. Since we launched, we had more than 3000 apps generated in just a couple of days!

1. Describe your app 2. Pick the color 3. Generate your app 🚀

Check out this blog post to see GPT Web App Generator in action, including a one-minute demo video, few example apps, and learn a bit more about our plans for the future. Or, try it out yourself at https://magic-app-generator.wasp-lang.dev/ !

In this blog post, we are going to explore the technical side of creating the GPT Web App Generator: techniques we used, how we engineered our prompts, challenges we encountered, and choices we made! (Note from here on we will just refer to it as the “Generator”, or “code agent” when talking about the backend)

Also, all the code behind the Generator is open source: web app, GPT code agent.

How well does it work 🤔?

First, let’s quickly explain what we ended up with and how it performs.

Input into our Generator is the app name, app description (free form text), and a couple of simple options such as primary app color, temperature, auth method, and GPT model to use.

Input for generating a Todo app

As an output, Generator spits out the whole JS codebase of a working full-stack web app: frontend, backend, and database. Frontend is React + Tailwind, the backend is NodeJS with Express, and for working with the database we used Prisma. This is all connected together with the Wasp framework.

You can see an example of generated codebase here: https://magic-app-generator.wasp-lang.dev/result/07ed440a-3155-4969-b3f5-2031fb1f622f .

Result of generating a Todo app

Generator does its best to produce code that works out of the box → you can download it to your machine and run it. For simpler apps, such as TodoApp or MyPlants, it often generates code with no mistakes, and you can run them out of the box.

What generated TodoApp looks like

For a bit more complex apps, like a blog with posts and comments, it still generates a reasonable codebase but there are some mistakes to be expected here and there. For even more complex apps, it usually doesn’t follow up completely, but stops at some level of complexity and fills in the rest with TODOs or omits functionality, so it is kind of like a simplified model of what was asked for. Overall, it is optimized for producing CRUD business web apps.

This makes it a great tool for kick-starting your next web app project with a solid prototype, or to even generate working, simple apps on the fly!

How does it work ⚙️?

When we set out to build the Generator, we gave ourselves the following goals:

  • we must be able to build it in a couple of weeks
  • it has to be relatively easy to maintain in the future
  • it needs to generate the app quickly and cheaply (a couple of minutes, < $1)
  • generated apps should have as few mistakes as possible

Therefore, to keep it simple, we don’t do any LLM-level engineering or fine-tuning, instead, we just use OpenAI API (specifically GPT3.5 and GPT4) to generate different parts of the app while giving it the right context at every moment (pieces of docs, examples, guidelines, …). To ensure the coherence and quality of the generated app, we don’t give our code agent too much freedom but instead heavily guide it, step by step, through generating the app.

As step zero, we generate some code files deterministically, without GPT, just based on the options that the user chose (primary color, auth method): those include some config files for the project, some basic global CSS, and some auth logic. You can see this logic here (we call those “skeleton” files): code on Github .

Then, the code agent takes over!

The code agent does its work in 3 main phases:

  1. Planning 📝
  2. Generating 🏭
  3. Fixing 🔧

Since GPT4 is quite slower and significantly more expensive than GPT3.5 (also has a lower rate limit regarding the number of tokens per minute, and also the number of requests per minute), we use GPT4 only for the planning, since that is the crucial step, and then after that, we use GPT3.5 for the rest.

As for cost per app 💸: one app typically consumes from 25k to 60k tokens, which comes to about $0.1 to $0.2 per app, when we use a mix of GPT4 and GPT3.5. If we run it just with GPT4, then the cost is 10x, which is from $1 to $2.

🎶 Intermezzo: short explanation of OpenAI Chat Completions API

OpenAI API offers different services, but we used only one of them: “chat completions”.

API itself is actually very simple: you send over a conversation, and you get a response from the GPT.

The conversation is just a list of messages, where each message has content and a role, where the role specifies who “said” that content → was it “user” (you), or “assistant” (GPT).

The important thing to note is that there is no concept of state/memory: every API call is completely standalone, and the only thing that GPT knows about is the conversation you provide it with at that moment!

If you are wondering how ChatGPT (the web app that uses GPT in the background) works with no memory → well, each time you write a message, the whole conversation so far is resent again! There are some additional smart mechanisms in play here, but that is really it at its core.

Official guide, official API reference.

Step #1: Planning 📝

A Wasp app consists of Entities (Prisma data models), Operations (NodeJS Queries and Actions), and Pages (React).

Once given an app description and title, the code agent first generates a Plan: it is a list of Entities, Operations (Queries and Actions), and Pages that comprise the app. So kind of like an initial draft of the app. It doesn’t generate the code yet → instead, it comes up with their names and some other details, including a short description of what they should behave like.

This is done via a single API request toward GPT, where the prompt consists of the following:

  • Short info about the Wasp framework + an example of some Wasp code.
  • We explain that we want to generate the Plan, explain what it is, and how it is represented as JSON, by describing its schema.
  • We provide some examples of the Plan, represented as JSON.
  • Some rules and guidelines we want it to follow (e.g. “plan should have at least 1 page”, “make sure to generate a User entity”).
  • Instructions to return the Plan only as a valid JSON response, and no other text.
  • App name and description (as provided by the user).

You can see how we generate such a prompt in the code here.

Also, here is an actual instance of this prompt for a TodoApp.
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.

GPT then responds with a JSON (hopefully), that we parse, and we have ourselves a Plan! We will use this Plan in the following steps, to drive our generation of other parts of the app. Note that GPT sometimes adds text to the JSON response or returns invalid JSON, so we built in some simple approaches to overcome these issues, which we explain in detail later.

🎶 Intermezzo: Common prompt design

The prompt design we just described above for generating a Plan is actually very similar for other steps (e.g. the Generation and Fixing steps along with their respective sub-steps), so let’s cover those commonalities.

All of the prompts we use more or less adhere to the same basic structure:

  • General context
    • Short info about what Wasp framework is.
    • Doc snippets (with code examples if needed) about whatever we are generating right now (e.g. examples of NodeJS code, or examples of React code).
  • Project context: stuff we generated in the previous steps that is relevant to the current step.
  • Instructions on what we want to generate right now + JSON schema for it + example of such JSON response.
  • Rules and guidelines: this is a good place to warn it about common mistakes it makes, or give it some additional advice, and emphasize what needs to happen and what must not happen.
  • Instructions to respond only with a valid JSON, and no other text.
  • Original user prompt: app name and description (as provided by the user).

We put the original user prompt at the end because then we can tell GPT in the system message after it sees the start of the original user prompt (we have a special header for it), that it needs to treat everything after it as an app description and not as instructions on what to do → this way we attempt to defend from the potential prompt injection.

Step #2: Generating 🏭

After producing the Plan, Generator goes step by step through the Plan and asks GPT to generate each web app piece, while providing it with docs, examples, and guidelines. Each time a web app piece is generated, Generator fits it into the whole app. This is where most of our work comes in: equipping GPT with the right information at the right moment.

In our case, we do it for all the Operations in the Plan (Actions and Queries: NodeJs code), and also for all the Pages in the Plan (React code), with one prompt for each. So if we have 2 queries, 3 actions, and 2 pages, that will be 2+3+2 = 7 GPT prompts/requests. Prompts are designed as explained previously.

Code on Github: generating an Operation, generating a Page.

When generating Operations, we provide GPT with the info about the previously generated Entities, while when generating Pages, we provide GPT with the info about previously generated Entities and Operations.

Step #3: Fixing 🔧

Finally, the Generator tries its best to fix any mistakes that GPT might have introduced previously. GPT loves fixing stuff it previously generated → if you first ask it to generate some code, and then just tell it to fix it, it will often improve it!

To enhance this process further, we don’t just ask it to fix previous code, but also provide it with instructions on what to keep an eye out for, like common types of mistakes that we noticed it often does, and also point it to any specific mistakes we were able to detect on our own.

Regarding detecting mistakes to report to GPT, ideally, you would have a full REPL going on → that means running the generated code through an interpreter/compiler, then sending it for repairs, and so on until all is fixed.

In our case, running the whole project through the TypeScript compiler was not feasible for us with the time limits we put on ourselves, but we used some simpler static analysis tools like Wasp’s compiler (for the .wasp file) and prisma format for Prisma model schemas, and sent those to GPT to fix them. We also wrote some simple heuristics of our own that are able to detect some of the common mistakes.

Our code (& prompt) for fixing a Page.

Our code (& prompt) for fixing Operations.

In the prompt, we would usually repeat the same guidelines we provided previously in the Generation step, while also adding a couple of additional pointers to common mistakes, and that usually helps, it fixes stuff it missed before. But, often not everything, instead something will still get through. Some things we just couldn’t get it to fix consistently, for example, Wasp-specific JS imports, no matter how much we emphasized what it needed to do with them, it would just keep messing them up. Even GPT4 wasn’t perfect in this situation. For such situations, when possible, we ended up writing our own heuristics that would fix those mistakes (fixing JS imports).

Things we tried/learned

Explanations 💬

We tried telling GPT to explain what it did while fixing mistakes: which mistakes it will fix, and which mistakes it fixed, since we read that that can help, but we didn’t see visible improvement in its performance.

Testing 🧪

Testing the performance of your code agent is hard.

In our case, it takes a couple of minutes for our code agent to generate a new app, and you need to run tests directly with the OpenAI API. Also, since results are non-deterministic, it can be pretty hard to say if output was affected by the changes you did or not.

Finally, evaluating the output itself can be hard (especially in our case when it is a whole full-stack web app).

Ideally, we would have set up a system where we can run only parts of the whole generation process, and we could automatically run a specific part a number of times for each of different sets of parameters (which would include different prompts, but also parameters like type of model (gpt4 vs gpt3.5), temperature and similar), in order to compare performance for each of those parameter sets.

Evaluation performance would also ideally be automated, e.g. we would count the mistakes during compilation and/or evaluate the quality of app design → but this is also quite hard.

We, unfortunately, didn’t have time to set up such a system, so we were mostly doing testing manually, which is quite subjective and vulnerable to randomness, and is effective only for changes that have quite a big impact, while you can’t really detect those that are minor optimizations.

Context vs smarts 🧠

When we started working on the Generator, we thought the size of GPT’s context would be the main issue. However, we didn’t have any issues with context at the end → most of what we wanted to specify would fit into 2k to max 4k tokens, while GPT3.5 has context up to 16k!

Instead, we had bigger problems with its “smarts” → meaning that GPT would not follow the rules we very explicitly told it to follow, or would do things we explicitly forbid it from doing. GPT4 proved to be better at following rules than GPT3.5, but even GPT4 would keep doing some mistakes over and over and forgetting about specific rules (even though there was more than enough context). The “fixing” step did help with this: we would repeat the rules there and GPT would pick up more of them, but often still not all of them.

Handling JSON as a response 📋

As mentioned earlier in this article, in all our interactions with GPT, we always ask it to return the response as JSON, for which we specify the schema and give some examples.

However, GPT still doesn’t always follow that rule, and will sometimes add some text around the JSON, or will make a mistake in formatting JSON.

The way we handled this is with two simple fixes:

  1. Upon receiving JSON, we would remove all the characters from the start until we hit {, and also all chars from the end until we hit }. Simple heuristic, but it works very well for removing redundant text around the JSON in practice since GPT will normally not have any { or } in that text.
  2. If we fail to parse JSON, we send it again for repairs, to GPT. We include the previous prompt and its last answer (that contains invalid JSON) and add instructions to fix it + JSON parse errors we got. We repeat this a couple of times until it gets it right (or until we give up).

In practice, these two methods took care of invalid JSON in 99% of the cases for us.

NOTE: While we were implementing our code agent, OpenAI released new functionality for GPT, “functions”, which is basically a mechanism to have GPT respond with a structured JSON, following the schema of your description. So it would likely make more sense to do this with “functions”, but we already had this working well so we just stuck with it.

Handling interruptions in the service 🚧

We were calling OpenAI API directly, so we noticed quickly that often it would return 503 - service unavailable - especially during peak hours (e.g. 3 pm CET).

Therefore, it is recommended to have some kind of retry mechanism, ideally with exponential backoff, that makes your code agent redundant to such random interruptions in the service, and also to potential rate limiting. We went with the retry mechanism with exponential backoff and it worked great.

Temperature 🌡️

Temperature determines how creative GPT is, but the more creative it gets, the less “stable” it is. It hallucinates more and also has a harder time following rules. +

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

· 23 min read
Martin Sosic

We created GPT Web App Generator, which lets you shortly describe the web app you would like to create, and in a matter of minutes, a full-stack codebase, written in React, Node.js, Prisma, and Wasp, will be generated right in front of you, and available to download and run locally!

We started this as an experiment, to see how well we could use GPT to generate full-stack web apps in Wasp, the open-source JS web app framework that we are developing. Since we launched, we had more than 3000 apps generated in just a couple of days!

1. Describe your app 2. Pick the color 3. Generate your app 🚀

Check out this blog post to see GPT Web App Generator in action, including a one-minute demo video, few example apps, and learn a bit more about our plans for the future. Or, try it out yourself at https://magic-app-generator.wasp-lang.dev/ !

In this blog post, we are going to explore the technical side of creating the GPT Web App Generator: techniques we used, how we engineered our prompts, challenges we encountered, and choices we made! (Note from here on we will just refer to it as the “Generator”, or “code agent” when talking about the backend)

Also, all the code behind the Generator is open source: web app, GPT code agent.

How well does it work 🤔?

First, let’s quickly explain what we ended up with and how it performs.

Input into our Generator is the app name, app description (free form text), and a couple of simple options such as primary app color, temperature, auth method, and GPT model to use.

Input for generating a Todo app

As an output, Generator spits out the whole JS codebase of a working full-stack web app: frontend, backend, and database. Frontend is React + Tailwind, the backend is NodeJS with Express, and for working with the database we used Prisma. This is all connected together with the Wasp framework.

You can see an example of generated codebase here: https://magic-app-generator.wasp-lang.dev/result/07ed440a-3155-4969-b3f5-2031fb1f622f .

Result of generating a Todo app

Generator does its best to produce code that works out of the box → you can download it to your machine and run it. For simpler apps, such as TodoApp or MyPlants, it often generates code with no mistakes, and you can run them out of the box.

What generated TodoApp looks like

For a bit more complex apps, like a blog with posts and comments, it still generates a reasonable codebase but there are some mistakes to be expected here and there. For even more complex apps, it usually doesn’t follow up completely, but stops at some level of complexity and fills in the rest with TODOs or omits functionality, so it is kind of like a simplified model of what was asked for. Overall, it is optimized for producing CRUD business web apps.

This makes it a great tool for kick-starting your next web app project with a solid prototype, or to even generate working, simple apps on the fly!

How does it work ⚙️?

When we set out to build the Generator, we gave ourselves the following goals:

  • we must be able to build it in a couple of weeks
  • it has to be relatively easy to maintain in the future
  • it needs to generate the app quickly and cheaply (a couple of minutes, < $1)
  • generated apps should have as few mistakes as possible

Therefore, to keep it simple, we don’t do any LLM-level engineering or fine-tuning, instead, we just use OpenAI API (specifically GPT3.5 and GPT4) to generate different parts of the app while giving it the right context at every moment (pieces of docs, examples, guidelines, …). To ensure the coherence and quality of the generated app, we don’t give our code agent too much freedom but instead heavily guide it, step by step, through generating the app.

As step zero, we generate some code files deterministically, without GPT, just based on the options that the user chose (primary color, auth method): those include some config files for the project, some basic global CSS, and some auth logic. You can see this logic here (we call those “skeleton” files): code on Github .

Then, the code agent takes over!

The code agent does its work in 3 main phases:

  1. Planning 📝
  2. Generating 🏭
  3. Fixing 🔧

Since GPT4 is quite slower and significantly more expensive than GPT3.5 (also has a lower rate limit regarding the number of tokens per minute, and also the number of requests per minute), we use GPT4 only for the planning, since that is the crucial step, and then after that, we use GPT3.5 for the rest.

As for cost per app 💸: one app typically consumes from 25k to 60k tokens, which comes to about $0.1 to $0.2 per app, when we use a mix of GPT4 and GPT3.5. If we run it just with GPT4, then the cost is 10x, which is from $1 to $2.

🎶 Intermezzo: short explanation of OpenAI Chat Completions API

OpenAI API offers different services, but we used only one of them: “chat completions”.

API itself is actually very simple: you send over a conversation, and you get a response from the GPT.

The conversation is just a list of messages, where each message has content and a role, where the role specifies who “said” that content → was it “user” (you), or “assistant” (GPT).

The important thing to note is that there is no concept of state/memory: every API call is completely standalone, and the only thing that GPT knows about is the conversation you provide it with at that moment!

If you are wondering how ChatGPT (the web app that uses GPT in the background) works with no memory → well, each time you write a message, the whole conversation so far is resent again! There are some additional smart mechanisms in play here, but that is really it at its core.

Official guide, official API reference.

Step #1: Planning 📝

A Wasp app consists of Entities (Prisma data models), Operations (NodeJS Queries and Actions), and Pages (React).

Once given an app description and title, the code agent first generates a Plan: it is a list of Entities, Operations (Queries and Actions), and Pages that comprise the app. So kind of like an initial draft of the app. It doesn’t generate the code yet → instead, it comes up with their names and some other details, including a short description of what they should behave like.

This is done via a single API request toward GPT, where the prompt consists of the following:

  • Short info about the Wasp framework + an example of some Wasp code.
  • We explain that we want to generate the Plan, explain what it is, and how it is represented as JSON, by describing its schema.
  • We provide some examples of the Plan, represented as JSON.
  • Some rules and guidelines we want it to follow (e.g. “plan should have at least 1 page”, “make sure to generate a User entity”).
  • Instructions to return the Plan only as a valid JSON response, and no other text.
  • App name and description (as provided by the user).

You can see how we generate such a prompt in the code here.

Also, here is an actual instance of this prompt for a TodoApp.
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.

GPT then responds with a JSON (hopefully), that we parse, and we have ourselves a Plan! We will use this Plan in the following steps, to drive our generation of other parts of the app. Note that GPT sometimes adds text to the JSON response or returns invalid JSON, so we built in some simple approaches to overcome these issues, which we explain in detail later.

🎶 Intermezzo: Common prompt design

The prompt design we just described above for generating a Plan is actually very similar for other steps (e.g. the Generation and Fixing steps along with their respective sub-steps), so let’s cover those commonalities.

All of the prompts we use more or less adhere to the same basic structure:

  • General context
    • Short info about what Wasp framework is.
    • Doc snippets (with code examples if needed) about whatever we are generating right now (e.g. examples of NodeJS code, or examples of React code).
  • Project context: stuff we generated in the previous steps that is relevant to the current step.
  • Instructions on what we want to generate right now + JSON schema for it + example of such JSON response.
  • Rules and guidelines: this is a good place to warn it about common mistakes it makes, or give it some additional advice, and emphasize what needs to happen and what must not happen.
  • Instructions to respond only with a valid JSON, and no other text.
  • Original user prompt: app name and description (as provided by the user).

We put the original user prompt at the end because then we can tell GPT in the system message after it sees the start of the original user prompt (we have a special header for it), that it needs to treat everything after it as an app description and not as instructions on what to do → this way we attempt to defend from the potential prompt injection.

Step #2: Generating 🏭

After producing the Plan, Generator goes step by step through the Plan and asks GPT to generate each web app piece, while providing it with docs, examples, and guidelines. Each time a web app piece is generated, Generator fits it into the whole app. This is where most of our work comes in: equipping GPT with the right information at the right moment.

In our case, we do it for all the Operations in the Plan (Actions and Queries: NodeJs code), and also for all the Pages in the Plan (React code), with one prompt for each. So if we have 2 queries, 3 actions, and 2 pages, that will be 2+3+2 = 7 GPT prompts/requests. Prompts are designed as explained previously.

Code on Github: generating an Operation, generating a Page.

When generating Operations, we provide GPT with the info about the previously generated Entities, while when generating Pages, we provide GPT with the info about previously generated Entities and Operations.

Step #3: Fixing 🔧

Finally, the Generator tries its best to fix any mistakes that GPT might have introduced previously. GPT loves fixing stuff it previously generated → if you first ask it to generate some code, and then just tell it to fix it, it will often improve it!

To enhance this process further, we don’t just ask it to fix previous code, but also provide it with instructions on what to keep an eye out for, like common types of mistakes that we noticed it often does, and also point it to any specific mistakes we were able to detect on our own.

Regarding detecting mistakes to report to GPT, ideally, you would have a full REPL going on → that means running the generated code through an interpreter/compiler, then sending it for repairs, and so on until all is fixed.

In our case, running the whole project through the TypeScript compiler was not feasible for us with the time limits we put on ourselves, but we used some simpler static analysis tools like Wasp’s compiler (for the .wasp file) and prisma format for Prisma model schemas, and sent those to GPT to fix them. We also wrote some simple heuristics of our own that are able to detect some of the common mistakes.

Our code (& prompt) for fixing a Page.

Our code (& prompt) for fixing Operations.

In the prompt, we would usually repeat the same guidelines we provided previously in the Generation step, while also adding a couple of additional pointers to common mistakes, and that usually helps, it fixes stuff it missed before. But, often not everything, instead something will still get through. Some things we just couldn’t get it to fix consistently, for example, Wasp-specific JS imports, no matter how much we emphasized what it needed to do with them, it would just keep messing them up. Even GPT4 wasn’t perfect in this situation. For such situations, when possible, we ended up writing our own heuristics that would fix those mistakes (fixing JS imports).

Things we tried/learned

Explanations 💬

We tried telling GPT to explain what it did while fixing mistakes: which mistakes it will fix, and which mistakes it fixed, since we read that that can help, but we didn’t see visible improvement in its performance.

Testing 🧪

Testing the performance of your code agent is hard.

In our case, it takes a couple of minutes for our code agent to generate a new app, and you need to run tests directly with the OpenAI API. Also, since results are non-deterministic, it can be pretty hard to say if output was affected by the changes you did or not.

Finally, evaluating the output itself can be hard (especially in our case when it is a whole full-stack web app).

Ideally, we would have set up a system where we can run only parts of the whole generation process, and we could automatically run a specific part a number of times for each of different sets of parameters (which would include different prompts, but also parameters like type of model (gpt4 vs gpt3.5), temperature and similar), in order to compare performance for each of those parameter sets.

Evaluation performance would also ideally be automated, e.g. we would count the mistakes during compilation and/or evaluate the quality of app design → but this is also quite hard.

We, unfortunately, didn’t have time to set up such a system, so we were mostly doing testing manually, which is quite subjective and vulnerable to randomness, and is effective only for changes that have quite a big impact, while you can’t really detect those that are minor optimizations.

Context vs smarts 🧠

When we started working on the Generator, we thought the size of GPT’s context would be the main issue. However, we didn’t have any issues with context at the end → most of what we wanted to specify would fit into 2k to max 4k tokens, while GPT3.5 has context up to 16k!

Instead, we had bigger problems with its “smarts” → meaning that GPT would not follow the rules we very explicitly told it to follow, or would do things we explicitly forbid it from doing. GPT4 proved to be better at following rules than GPT3.5, but even GPT4 would keep doing some mistakes over and over and forgetting about specific rules (even though there was more than enough context). The “fixing” step did help with this: we would repeat the rules there and GPT would pick up more of them, but often still not all of them.

Handling JSON as a response 📋

As mentioned earlier in this article, in all our interactions with GPT, we always ask it to return the response as JSON, for which we specify the schema and give some examples.

However, GPT still doesn’t always follow that rule, and will sometimes add some text around the JSON, or will make a mistake in formatting JSON.

The way we handled this is with two simple fixes:

  1. Upon receiving JSON, we would remove all the characters from the start until we hit {, and also all chars from the end until we hit }. Simple heuristic, but it works very well for removing redundant text around the JSON in practice since GPT will normally not have any { or } in that text.
  2. If we fail to parse JSON, we send it again for repairs, to GPT. We include the previous prompt and its last answer (that contains invalid JSON) and add instructions to fix it + JSON parse errors we got. We repeat this a couple of times until it gets it right (or until we give up).

In practice, these two methods took care of invalid JSON in 99% of the cases for us.

NOTE: While we were implementing our code agent, OpenAI released new functionality for GPT, “functions”, which is basically a mechanism to have GPT respond with a structured JSON, following the schema of your description. So it would likely make more sense to do this with “functions”, but we already had this working well so we just stuck with it.

Handling interruptions in the service 🚧

We were calling OpenAI API directly, so we noticed quickly that often it would return 503 - service unavailable - especially during peak hours (e.g. 3 pm CET).

Therefore, it is recommended to have some kind of retry mechanism, ideally with exponential backoff, that makes your code agent redundant to such random interruptions in the service, and also to potential rate limiting. We went with the retry mechanism with exponential backoff and it worked great.

Temperature 🌡️

Temperature determines how creative GPT is, but the more creative it gets, the less “stable” it is. It hallucinates more and also has a harder time following rules. A temperature is a number from 0 to 2, with a default value of 1.

We experimented with different values and found the following:

  • ≥ 1.5 would every so and so start giving quite silly results with random strings in it.
  • ≥ 1.0, < 1.5 was okish but was introducing a bit too many mistakes.
  • ≥ 0.7, < 1.0 was optimal → creative enough, while still not having many mistakes.
  • ≤ 0.7 seemed to perform similarly to a bit higher values, but with a bit less creativity maybe.

That said, I don’t think we tested values below 0.7 enough, and that is something we could certainly work on more.

We ended up using 0.7 as our default value, except for prompts that do fixing, for those we used a lower value of 0.5 because it seemed like GPT was changing stuff too much while fixing at 0.7 (being too creative). Our logic was: let it be creative when writing the first version of the code, then have it be a bit more conventional while fixing it. Again, we haven’t tested all this enough, so this is certainly something I would like us to explore more.

Future 🔮

While we ended up being impressed with the performance of what we managed to build in such a short time, we were also left wanting to try so many different ideas on how to improve it further. There are many avenues left to be explored in this ecosystem that is developing so rapidly, that it is hard to reach the point where you feel like you explored all the options and found the optimal solution.

Some of the ideas that would be exciting to try in the future:

  1. We put quite a few limitations regarding the code that our code agent generates, to make sure it works well enough: we don’t allow it to create helper files, to include npm dependencies, no TypeScript, no advanced Wasp features, … . We would love to lift the limitations, therefore allowing the creation of more complex and powerful apps.

  2. Instead of our code agent doing everything in one shot, we could allow the user to interact with it after the first version of the app is generated: to provide additional prompts, for example, to fix something, to add some feature to the app, to do something differently, …. The hardest thing here would be figuring out which context to provide to the GPT at which moment and designing the experience appropriately, but I am certain it is doable, and it would take the Generator to the next level of usability. Another option is to allow intervention in between initial generation steps → for example, after the plan is generated, to allow the user to adjust it by providing additional instructions to the GPT.

  3. Find an open-source LLM that fits the purpose and fine-tune / pre-train it for our purpose. If we could teach it more about Wasp and the technologies we use, so we don’t have to include it in every prompt, we could save quite some context + have the LLM be more focused on the rules and guidelines we are specifying in the prompt. We could also host it ourselves and have more control over the costs and rate limits.

  4. Take a different approach to the code agent: let it be more free. Instead of guiding it so carefully, we could teach it about all the different things it is allowed to ask for (ask for docs, ask for examples, ask to generate a certain piece of the app, ask to see a certain already generated piece of the app, …) and would let it guide itself more freely. It could constantly generate a plan, execute it, update the plan, and so on until it reaches the state of equilibrium. This approach potentially promises more flexibility and would likely be able to generate apps of greater complexity, but it also requires quite more tokens and a powerful LLM to drive it → I believe this approach will become more feasible as LLMs become more capable.

Support us! ⭐️

If you wish to express your support for what we are doing, consider giving us a star on Github! Everything we do at Wasp is open source, and your support motivates us and helps us to keep making web app development easier and with less boilerplate.

Also, if you have any ideas on how we could improve our code agent, or maybe we can help you somehow -> feel free to join our Discord server and let's chat!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/08/01/smol-ai-vs-wasp-ai.html b/blog/2023/08/01/smol-ai-vs-wasp-ai.html index 62cbe4c6ee..76075cb235 100644 --- a/blog/2023/08/01/smol-ai-vs-wasp-ai.html +++ b/blog/2023/08/01/smol-ai-vs-wasp-ai.html @@ -19,19 +19,19 @@ - - + +
-

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

· 27 min read
Vinny

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” 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” 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.

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.

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 and Wasp AI. 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 = } 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 🙏

please please please

…even Ron would star Wasp on GitHub 🤩

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 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.

Check out this tweet thread above to get a better understanding: 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:

*Curious to see what the internal system prompt looks like?
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}

Wasp’s GPT Web App Generator

In contrast to Smol-Developer, Wasp’s AI tool, GPT Web App Generator, 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!

Here’s a quick 1 minute video showcasing how GPT Web App Generator works:

So to give a bit of background, Wasp 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:

// 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 or MyPlants tend to work straight out of the box, while more complex apps need a bit of finessing to get working.

*Curious to see what the internal system prompt looks like?
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.

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

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

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. +

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

· 27 min read
Vinny

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” 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” 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.

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.

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 and Wasp AI. 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 = } 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 🙏

please please please

…even Ron would star Wasp on GitHub 🤩

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 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.

Check out this tweet thread above to get a better understanding: 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:

*Curious to see what the internal system prompt looks like?
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}

Wasp’s GPT Web App Generator

In contrast to Smol-Developer, Wasp’s AI tool, GPT Web App Generator, 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!

Here’s a quick 1 minute video showcasing how GPT Web App Generator works:

So to give a bit of background, Wasp 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:

// 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 or MyPlants tend to work straight out of the box, while more complex apps need a bit of finessing to get working.

*Curious to see what the internal system prompt looks like?
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.

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

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

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.”

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?

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!

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

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

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

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 classNames 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.

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!

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 AIWasp AI
🧑‍💻 Types of AppsVariedFull-stack Web Apps
🗯 Programming LanguagesAll TypesJavaScript/TypeScript
📈 Complexity of Generated AppSimple to MediumMedium to Complex
💰 Price per Generation — via OpenAI’s API$0.80 to $10.00$0.10 to $0.20 
💳 Payment Methodbring your own API keyfree — paid for by Wasp
🐛 DebuggingYes, if you’re willing to tinkerBuilt-in, but limited
🗣 Type of Prompt NeededComplex and detailed, 1 or more pages (e.g. an entire Product Requirement Doc)Simple, 1-3 paragraphs
😎 Intended UserEngineers, Product Designers wanting to generate a broad range of simple prototypesWeb 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.
    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, 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 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, 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 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!

Help me help you

🌟 If you haven’t yet, please star us on GitHub, 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

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 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 and the GPT Web App Generator for yourself and let me know what you think in the comments!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescript.html b/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescript.html index 7cc2077fed..f7ff91783f 100644 --- a/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescript.html +++ b/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescript.html @@ -19,13 +19,13 @@ - - + +
-

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

· 22 min read
Vinny

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.

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, 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):

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

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.

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.

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, 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

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

In this exampple, we’ll be using ExpressJS with the Socket.IO library. Although there are others out there, Socket.IO is a great library that makes working with WebSockets in NodeJS easier.

If you want to code along, first clone the start branch:

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:

cd ws-server && npm install

We also need to install the types for working with typescript:

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:

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 package and start adding WebSockets to our server!

First, let’s kill the server with ctrl + c and then run:

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 ;):

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 client to the front-end.

cd into the ws-client directory and run

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:

You may have noticed that poll does not match the PollState from our server. We need to install the 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:

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:

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):

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:

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, Authentication, Database Management, and Full-stack type-safety out-of-the box.

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

  1. Install Wasp globally by running the following command in your terminal:
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:

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.

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:

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:

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 middleware

are all being handled for you by Wasp. Noice!

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:

wasp db migrate-dev

Once that’s finished, run the app (it my take a while on first run to install all depenedencies):

wasp start

You should see a login screen this time. Go ahead and first register a user, then login:

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 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, 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:

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 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:

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. 🎩

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 WaspWith Wasp
😎 Intended UserSenior Developers, web development teamsFull-stack developers, “Indiehackers”, junior devs
📈 Complexity of CodeMedium-to-HighLow
🚤 SpeedSlower, more methodicalFaster, more integrated
🧑‍💻 LibrariesAnySocket.IO
⛑ Type safetyImplement on both server and clientImplement once on server, inferred by Wasp on client
🎮 Amount of controlHigh, as you determine the implementationOpinionated, as Wasp decides the basic implementation
🐛 Learning CurveComplex: full knowledge of front and backend technologies, including WebSocketsIntermediate: 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, 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 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

And if you know of a better, cooler, sleeker way of implementing WebSockets into your apps, let us know in the comments below

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

· 22 min read
Vinny

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.

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, 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):

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

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.

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.

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, 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

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

In this exampple, we’ll be using ExpressJS with the Socket.IO library. Although there are others out there, Socket.IO is a great library that makes working with WebSockets in NodeJS easier.

If you want to code along, first clone the start branch:

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:

cd ws-server && npm install

We also need to install the types for working with typescript:

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:

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 package and start adding WebSockets to our server!

First, let’s kill the server with ctrl + c and then run:

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 ;):

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 client to the front-end.

cd into the ws-client directory and run

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:

You may have noticed that poll does not match the PollState from our server. We need to install the 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:

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:

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):

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:

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, Authentication, Database Management, and Full-stack type-safety out-of-the box.

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

  1. Install Wasp globally by running the following command in your terminal:
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:

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.

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:

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:

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 middleware

are all being handled for you by Wasp. Noice!

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:

wasp db migrate-dev

Once that’s finished, run the app (it my take a while on first run to install all depenedencies):

wasp start

You should see a login screen this time. Go ahead and first register a user, then login:

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 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, 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:

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 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:

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. 🎩

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 WaspWith Wasp
😎 Intended UserSenior Developers, web development teamsFull-stack developers, “Indiehackers”, junior devs
📈 Complexity of CodeMedium-to-HighLow
🚤 SpeedSlower, more methodicalFaster, more integrated
🧑‍💻 LibrariesAnySocket.IO
⛑ Type safetyImplement on both server and clientImplement once on server, inferred by Wasp on client
🎮 Amount of controlHigh, as you determine the implementationOpinionated, as Wasp decides the basic implementation
🐛 Learning CurveComplex: full knowledge of front and backend technologies, including WebSocketsIntermediate: 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, 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 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

And if you know of a better, cooler, sleeker way of implementing WebSockets into your apps, let us know in the comments below

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-ai.html b/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-ai.html index 9f57d663fd..c63cbd3c0b 100644 --- a/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-ai.html +++ b/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-ai.html @@ -19,17 +19,17 @@ - - + +
-

Using Product Requirement Documents to Generate Better Web Apps with AI

· 9 min read
Vinny

I’m an indiehacker that likes creating lots of fun side-projects, like my SaaS app CoverLetterGPT with ~4,000 users. That’s why I've been on the lookout for AI-assisted coding tools to help me kickstart new full-stack web apps as quickly as possible.

I tried out a bunch, but found that most of them produced codebases that were too simple to work with, or getting a good result was just about as time consuming as coding it myself.

But through the process of trying out different tools and methods, I stumbled across a hack that helped me create comprehensive, functional codebases for full-stack apps with Auth, API routes, Tailwind CSS, DB management, and other more complex features.

The trick? Ask ChatGPT to write you a detailed Product Requirement Doc for the app you’d like to create, and then pass this to Wasp’s GPT Web App Generator.

Image description

The results are really surprising and give you a far better starter codebase than the other tools I’ve tried (mainly due to the specificity of the generator itself).

And best of all, its free to use! 🤑

Intro

I’m a self-taught, full-stack web developer and I have a lot of fun building side projects.

For example, the side project I’m most proud of is an open-source cover letter generator SaaS App, CoverLetterGPT, which has close to 4,000 users!

I also have a lot of ridiculous side-project ideas, like this app that can turn your favorite tech influencer’s YouTube videos into a drinking game. 🤣

That’s why I’ve been trying out lots of AI-assisted coding tools to generate fully-functional, full-stack web apps as quickly as possible.

There are the obvious tools at the moment, like using ChatGPT and Copilot within your IDE, but new ones are popping up all the time, especially those that act as AI assistants or “agents”.

I’ve gotten a chance to try out some of them, and I even wrote a long-form comparison piece where I put two such tools to the test, so check that out if you’re interested.

But there’s a major problem with these tools: even though they’re able to generate some good boilerplate code, they often include a lot of errors and don’t make the developer's job that much easier in the end.

Where the problem lies

On paper, AI-assisted coding tools generally save devs time and effort, especially when it comes to isolated code snippets.

On one hand, we have tools like ChatGPT and Copilot, which aid you with refactoring, fixing errors, or generating a snippet of code. It's much like assembling a jigsaw puzzle, where the tools serve you the next piece that fits the immediate gap.

But coding isn't just about filling the next available space; it’s about envisioning the entire picture, understanding the broader system and how different pieces interrelate.

https://media3.giphy.com/media/SrnCKS6s02XT2tw6kz/giphy.gif?cid=7941fdc6b01lfcj3taubztyp823itz03hhy9qx8p0mslbtij&ep=v1_gifs_search&rid=giphy.gif&ct=g

AI-assisted coding tools that behave more like agents have the potential to understand this broader context needed to generate larger codebases, but it’s easier said than done. Currently, most of the tools out there end up generating code that comes full of errors.

Worst of all, some of the code they output can be so messy it actually means more work for you.

How to fix it

AI assistants, much like novice apprentices, need a comprehensive understanding of what they should work towards. To achieve this, you need to craft a detailed outline along with a comprehensive set of instructions to give the AI as much context as possible.

You essentially want to be taking on the role of a Product Manager/Designer and be giving the AI a Product Requirement Document (PRD), i.e. an authoritative document that clearly outlines the

- - + + \ No newline at end of file diff --git a/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-call.html b/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-call.html index d31d826750..85cada398a 100644 --- a/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-call.html +++ b/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-call.html @@ -19,15 +19,15 @@ - - + +
-

Build your own AI Meme Generator & learn how to use OpenAI's function calls

· 31 min read
Vinny

Table of Contents

# TL;DR

In this two-part tutorial, we’re going to build a full-stack instant Meme Generator app using:

You check out a deployed version of the app we’re going to build here: The Memerator

If you just want to see the code for the finished app, check out the Memerator’s GitHub Repo

# Intro

Call Me, Maybe

With OpenAI’s chat completions API, developers are now able to do some really cool stuff. It basically enables ChatGPT functionality, but in the form of a callable API you can integrate into any app.

But when working with the API, a lot of devs wanted GPT to give back data in a format, like JSON, that they could use in their app’s functions.

Unfortunately, if you asked ChatGPT to return the data in a certain format, it wouldn’t always get it right. Which is why OpenAI released function calling.

As they describe it, function calling allows devs to “… describe functions to GPT, and have the model intelligently choose to output a JSON object containing arguments to call those functions.”

This is a great way to turn natural language into an API call.

So what better way to learn how to use GPT’s function calling feature than to use it to call Imgflip.com’s meme creator API!?

Image description

## Let’s Build

In this two-part tutorial, we’re going to build a full-stack React/NodeJS app with:

  • Authentication
  • Meme generation via OpenAI’s function calling and ImgFlip.com’s API
  • Daily cron job to fetch new meme templates
  • Meme editing and deleting
  • and more!

Image description

I already deployed a working version of this app that you can try out here: https://damemerator.netlify.app — so give it a go and let’s get… going.

In Part 1 of this tutorial, we will get the app set up and generating and displaying memes.

In Part 2, we will add more functionality, like recurring cron jobs to fetch more meme templates, and the ability to edit and delete memes.

BTW, two quick tips:

  1. if you need to reference the app’s finished code at any time to help you with this tutorial, you can check out the app’s GitHub Repo here.
  2. if you have any questions, feel free to hop into the Wasp Discord Server and ask us!

Part 1

Configuration

We’re going to make this a full-stack React/NodeJS web app so we need to get that set up first. But don’t worry, it won’t take long AT ALL, because we will be using Wasp as the framework.

Wasp does all the heavy lifting for us. You’ll see what I mean in a second.

Set up your Wasp project

First, install Wasp by running this in your terminal:

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

Next, let’s clone the start branch of the Memerator app that I’ve prepared for you:

git clone -b start https://github.com/vincanger/memerator.git

Then navigate into the Memerator directory and open up the project in VS Code:

cd Memerator && code .

You’ll notice Wasp sets up your full-stack app with a file structure like so:

.
├── main.wasp # The wasp config file.
└── src
   ├── client # Your React client code (JS/CSS/HTML) goes here.
   ├── server # Your server code (Node JS) goes here.
   └── shared # Your shared (runtime independent) code goes here.

Let’s check out the main.wasp file first. You can think of it as the “skeleton”, or instructions, of your app. This file configures most of your full-stack app for you 🤯:

app Memerator {
wasp: {
version: "^0.11.3"
},
title: "Memerator",
client: {
rootComponent: import { Layout } from "@client/Layout",
},
db: {
system: PostgreSQL,
prisma: {
clientPreviewFeatures: ["extendedWhereUnique"]
}
},
auth: {
userEntity: User,
methods: {
usernameAndPassword: {}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
},
dependencies: [
("openai", "4.2.0"),
("axios", "^1.4.0"),
("react-icons", "4.10.1"),
]
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
memes Meme[]
isAdmin Boolean @default(false)
credits Int @default(2)
psl=}

entity Meme {=psl
id String @id @default(uuid())
url String
text0 String
text1 String
topics String
audience String
template Template @relation(fields: [templateId], references: [id])
templateId String
user User @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
psl=}

entity Template {=psl
id String @id @unique
name String
url String
width Int
height Int
boxCount Int
memes Meme[]
psl=}

route HomePageRoute { path: "/", to: HomePage }
page HomePage {
component: import { HomePage } from "@client/pages/Home",
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/pages/auth/Login"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import Signup from "@client/pages/auth/Signup"
}

As you can see, our main.wasp config file has our:

  • dependencies,
  • authentication method,
  • database type, and
  • database models (”entities”)
  • client-side pages & routes

You might have also noticed this {=psl psl=} syntax in the entities above. This denotes that anything in between these psl brackets is actually a different language, in this case, Prisma Schema Language. Wasp uses Prisma under the hood, so if you've used Prisma before, it should be straightforward.

Also, make sure you install the Wasp VS code extension so that you get nice syntax highlighting and the best overall dev experience.

Setting up the Database

We still need to get a Postgres database setup.

Usually this can be pretty annoying, but with Wasp it’s really easy.

  1. just have Docker Deskop installed and running,
  2. open up a separate terminal tab/window,
  3. cd into the Memerator directory, and then run
wasp start db

This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 

Just leave this terminal tab, along with docker desktop, open and running in the background.

Now, in a different terminal tab, run

wasp db migrate-dev

and make sure to give your database migration a name, like init.

Environment Variables

In the root of your project, you’ll find a .env.server.example file that looks like this:

# set up your own credentials on https://imgflip.com/signup and rename this file to .env.server
# NOTE: make sure you register with Username and Password (not google)
IMGFLIP_USERNAME=
IMGFLIP_PASSWORD=

# get your api key from https://platform.openai.com/
OPENAI_API_KEY=

JWT_SECRET=asecretphraseatleastthirtytwocharacterslong

Rename this file to .env.server and follow the instructions in it to get your:

as we will need them to generate our memes 🤡

Start your App

With everything setup correctly, you should now be able to run

wasp start

When running wasp start, Wasp will install all the necessary npm packages, start our NodeJS server on port 3001, and our React client on port 3000.

Head to localhost:3000 in your browser to check it out. We should have the basis for our app that looks like this:

Image description

Generating a Meme

The boilerplate code already has the client-side form set up for generating memes based on:

  • topics
  • intended audience

This is the info we will send to the backend to call the OpenAI API using function calls. We then send this info to the imglfip.com API to generate the meme.

But the /caption_image endpoint of the imgflip API needs the meme template id. And to get that ID we first need to fetch the available meme templates from imgflip’s /get_memes endpoint

So let’s set that up now.

Server-Side Code

Create a new file in src/server/ called utils.ts:

import axios from 'axios';
import { stringify } from 'querystring';
import HttpError from '@wasp/core/HttpError.js';

type GenerateMemeArgs = {
text0: string;
text1: string;
templateId: string;
};

export const fetchMemeTemplates = async () => {
try {
const response = await axios.get('https://api.imgflip.com/get_memes');
return response.data.data.memes;
} catch (error) {
console.error(error);
throw new HttpError(500, 'Error fetching meme templates');
}
};

export const generateMemeImage = async (args: GenerateMemeArgs) => {
console.log('args: ', args);

try {
const data = stringify({
template_id: args.templateId,
username: process.env.IMGFLIP_USERNAME,
password: process.env.IMGFLIP_PASSWORD,
text0: args.text0,
text1: args.text1,
});

// Implement the generation of meme using the Imgflip API
const res = await axios.post('https://api.imgflip.com/caption_image', data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});

const url = res.data.data.url;

console.log('generated meme url: ', url);

return url as string;
} catch (error) {
console.error(error);
throw new HttpError(500, 'Error generating meme image');
}
};

This gives us some utility functions to help us fetch all the meme templates that we can possibly generate meme images with.

Notice that the POST request to the /caption_image endpoint takes the following data:

  • our imgflip username and password
  • ID of the meme template we will use
  • the text for top of the meme, i.e. text0
  • the text for the bottom of the meme, i.e. text1

Image description

The text0 and text1 arguments will generated for us by our lovely friend, ChatGPT. But in order for GPT to do that, we have to set up its API call, too.

To do that, create a new file in src/server/ called actions.ts.

Then go back to your main.wasp config file and add the following Wasp Action at the bottom of the file:

//...

action createMeme {
fn: import { createMeme } from "@server/actions.js",
entities: [Meme, Template, User]
}

An Action is a type of Wasp Operation that changes some state on the backend. It’s essentially a NodeJS function that gets called on the server, but Wasp takes care of setting it all up for you.

This means you don't have to worry about building an HTTP API for the Action, managing server-side request handling, or even dealing with client-side response handling and caching. Instead, you just write the business logic!

Image description

If you’ve got the Wasp VS Code extension installed, you’ll see an error (above). Hover over it and click Quick Fix > create function createMeme.

This will scaffold a createMeme function (below) for you in your actions.ts file if the file exists. Pretty Cool!

import { CreateMeme } from '@wasp/actions/types'

type CreateMemeInput = void
type CreateMemeOutput = void

export const createMeme: CreateMeme<CreateMemeInput, CreateMemeOutput> = async (args, context) => {
// Implementation goes here
}

You can see that it imports the Action types for you as well.

Because we will be sending the topics array and the intended audience string for the meme from our front-end form, and in the end we will return the newly created Meme entity, that’s what we should define our types as.

Remember, the Meme entity is the database model we defined in our main.wasp config file.

Knowing that, we can change the content of actions.ts to this:

import type { CreateMeme } from '@wasp/actions/types'
import type { Meme } from '@wasp/entities';

type CreateMemeArgs = { topics: string[]; audience: string };

export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
// Implementation goes here
}

Before we implement the rest of the logic, let’s run through how our createMeme function should work and how our Meme will get generated:

  1. fetch the imgflip meme template we want to use
  2. send its name, the topics, and intended audience to OpenAI’s chat completions API
  3. tell OpenAI we want the result back as arguments we can pass to our next function in JSON format, i.e. OpenAI’s function calling
  4. pass those arguments to the imgflip /caption-image endpoint and get our created meme’s url
  5. save the meme url and other info into our DB as a Meme entity

With all that in mind, go ahead and entirely replace the content in our actions.ts with the completed createMeme action:

import HttpError from '@wasp/core/HttpError.js';
import OpenAI from 'openai';
import { fetchMemeTemplates, generateMemeImage } from './utils.js';

import type { CreateMeme } from '@wasp/actions/types';
import type { Meme, Template } from '@wasp/entities';

type CreateMemeArgs = { topics: string[]; audience: string };

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});

export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
if (!context.user) {
throw new HttpError(401, 'You must be logged in');
}

if (context.user.credits === 0 && !context.user.isAdmin) {
throw new HttpError(403, 'You have no credits left');
}

const topicsStr = topics.join(', ');

let templates: Template[] = await context.entities.Template.findMany({});

if (templates.length === 0) {
const memeTemplates = await fetchMemeTemplates();
templates = await Promise.all(
memeTemplates.map(async (template: any) => {
const addedTemplate = await context.entities.Template.upsert({
where: { id: template.id },
create: {
id: template.id,
name: template.name,
url: template.url,
width: template.width,
height: template.height,
boxCount: template.box_count
},
update: {}
});

return addedTemplate;
})
);
}

// filter out templates with box_count > 2
templates = templates.filter((template) => template.boxCount <= 2);
const randomTemplate = templates[Math.floor(Math.random() * templates.length)];

console.log('random template: ', randomTemplate);

const sysPrompt = `You are a meme idea generator. You will use the imgflip api to generate a meme based on an idea you suggest. Given a random template name and topics, generate a meme idea for the intended audience. Only use the template provided`;
const userPrompt = `Topics: ${topicsStr} \n Intended Audience: ${audience} \n Template: ${randomTemplate.name} \n`;

let openAIResponse: OpenAI.Chat.Completions.ChatCompletion;
try {
openAIResponse = await openai.chat.completions.create({
messages: [
{ role: 'system', content: sysPrompt },
{ role: 'user', content: userPrompt },
],
functions: [
{
name: 'generateMemeImage',
description: 'Generate meme via the imgflip API based on the given idea',
parameters: {
type: 'object',
properties: {
text0: { type: 'string', description: 'The text for the top caption of the meme' },
text1: { type: 'string', description: 'The text for the bottom caption of the meme' },
},
required: ['templateName', 'text0', 'text1'],
},
},
],
function_call: {
name: 'generateMemeImage',
},
model: 'gpt-4-0613',
});
} catch (error: any) {
console.error('Error calling openAI: ', error);
throw new HttpError(500, 'Error calling openAI');
}

console.log(openAIResponse.choices[0]);

/**
* the Function call returned by openAI looks like this:
*/
// {
// index: 0,
// message: {
// role: 'assistant',
// content: null,
// function_call: {
// name: 'generateMeme',
// arguments: '{\n' +
// ` "text0": "CSS you've been writing all day",\n` +
// ' "text1": "This looks horrible"\n' +
// '}'
// }
// },
// finish_reason: 'stop'
// }
if (!openAIResponse.choices[0].message.function_call) throw new HttpError(500, 'No function call in openAI response');

const gptArgs = JSON.parse(openAIResponse.choices[0].message.function_call.arguments);
console.log('gptArgs: ', gptArgs);

const memeIdeaText0 = gptArgs.text0;
const memeIdeaText1 = gptArgs.text1;

console.log('meme Idea args: ', memeIdeaText0, memeIdeaText1);

const memeUrl = await generateMemeImage({
templateId: randomTemplate.id,
text0: memeIdeaText0,
text1: memeIdeaText1,
});

const newMeme = await context.entities.Meme.create({
data: {
text0: memeIdeaText0,
text1: memeIdeaText1,
topics: topicsStr,
audience: audience,
url: memeUrl,
template: { connect: { id: randomTemplate.id } },
user: { connect: { id: context.user.id } },
},
});

return newMeme;
};

At this point, the code above should be pretty self-explanatory, but I want to highlight a couple points:

  1. the context object is passed through to all Actions and Queries by Wasp. It contains the Prisma client with access to the DB entities you defined in your main.wasp config file.
  2. We first look for the imgflip meme templates in our DB. If none are found, we fetch them using our fetchTemplates utility function we defined earlier. Then we upsert them into our DB.
  3. There are some meme templates that take more than 2 text boxes, but for this tutorial we’re only using meme templates with 2 text inputs to make it easier.
  4. We choose a random template from this list to use as a basis for our meme (it’s actually a great way to serendipitously generate some interesting meme content).
  5. We give the OpenAI API info about the functions it can create arguments for via the functions and function_call properties, which tell it to always return JSON arguments for our function, generateMemeImage

Great! But once we start generating memes, we will need a way to display them on our front end.

So let’s now create a Wasp Query. A Query works just like an Action, except it’s only for reading data.

Go to src/server and create a new file called queries.ts.

Next, in your main.wasp file add the following code:

//...

query getAllMemes {
fn: import { getAllMemes } from "@server/queries.js",
entities: [Meme]
}

Then in your queries.ts file, add the getAllMemes function:

import HttpError from '@wasp/core/HttpError.js';

import type { Meme } from '@wasp/entities';
import type { GetAllMemes } from '@wasp/queries/types';

export const getAllMemes: GetAllMemes<void, Meme[]> = async (_args, context) => {
const memeIdeas = await context.entities.Meme.findMany({
orderBy: { createdAt: 'desc' },
include: { template: true },
});

return memeIdeas;
};

Client-Side Code

Now that we’ve got the createMeme and getAllMemes code implemented server-side, let’s hook it up to our client.

Wasp makes it really easy to import the Operations we just created and call them on our front end.

You can do so by going to src/client/pages/Home.tsx and adding the following code to the top of the file:

//...other imports...
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';

export function HomePage() {
const [topics, setTopics] = useState(['']);
const [audience, setAudience] = useState('');
const [isMemeGenerating, setIsMemeGenerating] = useState(false);

// 😎 😎 😎
const { data: user } = useAuth();
const { data: memes, isLoading, error } = useQuery(getAllMemes);

const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (!user) {
history.push('/login');
return;
}
if (topics.join('').trim().length === 0 || audience.length === 0) {
alert('Please provide topic and audience');
return;
}
try {
setIsMemeGenerating(true);
await createMeme({ topics, audience }); // <--- 😎 😎 😎
} catch (error: any) {
alert('Error generating meme: ' + error.message);
} finally {
setIsMemeGenerating(false);
}
};

//...

As you can see, we’ve imported createMeme and getAllMemes (😎).

For getAllMemes, we wrap it in the useQuery hook so that we can fetch and cache the data. On the other hand, our createMeme Action gets called in handleGenerateMeme which we will call when submit our form.

Rather than adding code to the Home.tsx file piece-by-piece, here is the file with all the code to generate and display the memes. Go ahead and replace all of Home.tsx with this code and I’ll explain it in more detail below:

import { useState, FormEventHandler } from 'react';
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';
import { useHistory } from 'react-router-dom';
import {
AiOutlinePlusCircle,
AiOutlineMinusCircle,
AiOutlineRobot,
} from 'react-icons/ai';

export function HomePage() {
const [topics, setTopics] = useState(['']);
const [audience, setAudience] = useState('');
const [isMemeGenerating, setIsMemeGenerating] = useState(false);

const history = useHistory();
const { data: user } = useAuth();
const { data: memes, isLoading, error } = useQuery(getAllMemes);

const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (!user) {
history.push('/login');
return;
}
if (topics.join('').trim().length === 0 || audience.length === 0) {
alert('Please provide topic and audience');
return;
}
try {
setIsMemeGenerating(true);
await createMeme({ topics, audience });
} catch (error: any) {
alert('Error generating meme: ' + error.message);
} finally {
setIsMemeGenerating(false);
}
};

const handleDeleteMeme = async (id: string) => {
//...
};

if (isLoading) return 'Loading...';
if (error) return 'Error: ' + error;

return (
<div className='p-4'>
<h1 className='text-3xl font-bold mb-4'>Welcome to Memerator!</h1>
<p className='mb-4'>Start generating meme ideas by providing topics and intended audience.</p>
<form onSubmit={handleGenerateMeme}>
<div className='mb-4 max-w-[500px]'>
<label htmlFor='topics' className='block font-bold mb-2'>
Topics:
</label>
{topics.map((topic, index) => (
<input
key={index}
type='text'
id='topics'
value={topic}
onChange={(e) => {
const updatedTopics = [...topics];
updatedTopics[index] = e.target.value;
setTopics(updatedTopics);
}}
className='p-1 mr-1 mb-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
/>
))}
<div className='flex items-center my-2 gap-1'>
<button
type='button'
onClick={() => topics.length < 3 && setTopics([...topics, ''])}
className='flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-xs py-1 px-2 rounded'
>
<AiOutlinePlusCircle /> Add Topic
</button>
{topics.length > 1 && (
<button
onClick={() => setTopics(topics.slice(0, -1))}
className='flex items-center gap-1 bg-red-500 hover:bg-red-700 border-2 text-white text-xs py-1 px-2 rounded'
>
<AiOutlineMinusCircle /> Remove Topic
</button>
)}
</div>
</div>
<div className='mb-4'>
<label htmlFor='audience' className='block font-bold mb-2'>
Intended Audience:
</label>
<input
type='text'
id='audience'
value={audience}
onChange={(e) => setAudience(e.target.value)}
className='p-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
/>
</div>
<button
type='submit'
className={`flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-sm font-bold py-1 px-2 rounded ${
isMemeGenerating ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
} $}`}
>
<AiOutlineRobot />
{!isMemeGenerating ? 'Generate Meme' : 'Generating...'}
</button>
</form>

{!!memes && memes.length > 0 ? (
memes.map((memeIdea) => (
<div key={memeIdea.id} className='mt-4 p-4 bg-gray-100 rounded-lg'>
<img src={memeIdea.url} width='500px' />
<div className='flex flex-col items-start mt-2'>
<div>
<span className='text-sm text-gray-700'>Topics: </span>
<span className='text-sm italic text-gray-500'>{memeIdea.topics}</span>
</div>
<div>
<span className='text-sm text-gray-700'>Audience: </span>
<span className='text-sm italic text-gray-500'>{memeIdea.audience}</span>
</div>
</div>
{/* TODO: implement edit and delete meme features */}
</div>
))
) : (
<div className='flex justify-center mt-5'> :( no memes found</div>
)}
</div>
);
}

There are two things I want to point out about this code:

  1. The useQuery hook calls our getAllMemes Query when the component mounts. It also caches the result for us, as well as automatically re-fetching whenever we add a new Meme to our DB via createMeme. This means our page will reload automatically whenever a new meme is generated.
  2. The useAuth hook allows us to fetch info about our logged in user. If the user isn’t logged in, we force them to do so before they can generate a meme.

These are really cool Wasp features that make your life as a developer a lot easier 🙂

So go ahead now and try and generate a meme. Here’s the one I just generated:

Image description

Haha. Pretty good!

Now wouldn’t it be cool though if we could edit and delete our memes? And what if we could expand the set of meme templates for our generator to use? Wouldn’t that be cool, too?

Yes, it would be. So let’s do that.

Part 2.

So we’ve got ourselves a really good basis for an app at this point.

We’re using OpenAI’s function calling feature to explain a function to GPT, and get it to return results for us in a format we can use to call that function.

This allows us to be certain GPT’s result will be usable in further parts of our application and opens up the door to creating AI agents.

If you think about it, we’ve basically got ourselves a really simple Meme generating “agent”. How cool is that?!

Fetching & Updating Templates with Cron Jobs

To be able to generate our meme images via ImgFlip’s API, we have to choose and send a meme template id to the API, along with the text arguments we want to fill it in with.

For example, the Grandma Finds Internet meme template has the following id:

Image description

But the only way for us to get available meme templates from ImgFlip is to send a GET request to +

Build your own AI Meme Generator & learn how to use OpenAI's function calls

· 31 min read
Vinny

Table of Contents

# TL;DR

In this two-part tutorial, we’re going to build a full-stack instant Meme Generator app using:

You check out a deployed version of the app we’re going to build here: The Memerator

If you just want to see the code for the finished app, check out the Memerator’s GitHub Repo

# Intro

Call Me, Maybe

With OpenAI’s chat completions API, developers are now able to do some really cool stuff. It basically enables ChatGPT functionality, but in the form of a callable API you can integrate into any app.

But when working with the API, a lot of devs wanted GPT to give back data in a format, like JSON, that they could use in their app’s functions.

Unfortunately, if you asked ChatGPT to return the data in a certain format, it wouldn’t always get it right. Which is why OpenAI released function calling.

As they describe it, function calling allows devs to “… describe functions to GPT, and have the model intelligently choose to output a JSON object containing arguments to call those functions.”

This is a great way to turn natural language into an API call.

So what better way to learn how to use GPT’s function calling feature than to use it to call Imgflip.com’s meme creator API!?

Image description

## Let’s Build

In this two-part tutorial, we’re going to build a full-stack React/NodeJS app with:

  • Authentication
  • Meme generation via OpenAI’s function calling and ImgFlip.com’s API
  • Daily cron job to fetch new meme templates
  • Meme editing and deleting
  • and more!

Image description

I already deployed a working version of this app that you can try out here: https://damemerator.netlify.app — so give it a go and let’s get… going.

In Part 1 of this tutorial, we will get the app set up and generating and displaying memes.

In Part 2, we will add more functionality, like recurring cron jobs to fetch more meme templates, and the ability to edit and delete memes.

BTW, two quick tips:

  1. if you need to reference the app’s finished code at any time to help you with this tutorial, you can check out the app’s GitHub Repo here.
  2. if you have any questions, feel free to hop into the Wasp Discord Server and ask us!

Part 1

Configuration

We’re going to make this a full-stack React/NodeJS web app so we need to get that set up first. But don’t worry, it won’t take long AT ALL, because we will be using Wasp as the framework.

Wasp does all the heavy lifting for us. You’ll see what I mean in a second.

Set up your Wasp project

First, install Wasp by running this in your terminal:

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

Next, let’s clone the start branch of the Memerator app that I’ve prepared for you:

git clone -b start https://github.com/vincanger/memerator.git

Then navigate into the Memerator directory and open up the project in VS Code:

cd Memerator && code .

You’ll notice Wasp sets up your full-stack app with a file structure like so:

.
├── main.wasp # The wasp config file.
└── src
   ├── client # Your React client code (JS/CSS/HTML) goes here.
   ├── server # Your server code (Node JS) goes here.
   └── shared # Your shared (runtime independent) code goes here.

Let’s check out the main.wasp file first. You can think of it as the “skeleton”, or instructions, of your app. This file configures most of your full-stack app for you 🤯:

app Memerator {
wasp: {
version: "^0.11.3"
},
title: "Memerator",
client: {
rootComponent: import { Layout } from "@client/Layout",
},
db: {
system: PostgreSQL,
prisma: {
clientPreviewFeatures: ["extendedWhereUnique"]
}
},
auth: {
userEntity: User,
methods: {
usernameAndPassword: {}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
},
dependencies: [
("openai", "4.2.0"),
("axios", "^1.4.0"),
("react-icons", "4.10.1"),
]
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
memes Meme[]
isAdmin Boolean @default(false)
credits Int @default(2)
psl=}

entity Meme {=psl
id String @id @default(uuid())
url String
text0 String
text1 String
topics String
audience String
template Template @relation(fields: [templateId], references: [id])
templateId String
user User @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
psl=}

entity Template {=psl
id String @id @unique
name String
url String
width Int
height Int
boxCount Int
memes Meme[]
psl=}

route HomePageRoute { path: "/", to: HomePage }
page HomePage {
component: import { HomePage } from "@client/pages/Home",
}

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import Login from "@client/pages/auth/Login"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import Signup from "@client/pages/auth/Signup"
}

As you can see, our main.wasp config file has our:

  • dependencies,
  • authentication method,
  • database type, and
  • database models (”entities”)
  • client-side pages & routes

You might have also noticed this {=psl psl=} syntax in the entities above. This denotes that anything in between these psl brackets is actually a different language, in this case, Prisma Schema Language. Wasp uses Prisma under the hood, so if you've used Prisma before, it should be straightforward.

Also, make sure you install the Wasp VS code extension so that you get nice syntax highlighting and the best overall dev experience.

Setting up the Database

We still need to get a Postgres database setup.

Usually this can be pretty annoying, but with Wasp it’s really easy.

  1. just have Docker Deskop installed and running,
  2. open up a separate terminal tab/window,
  3. cd into the Memerator directory, and then run
wasp start db

This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 

Just leave this terminal tab, along with docker desktop, open and running in the background.

Now, in a different terminal tab, run

wasp db migrate-dev

and make sure to give your database migration a name, like init.

Environment Variables

In the root of your project, you’ll find a .env.server.example file that looks like this:

# set up your own credentials on https://imgflip.com/signup and rename this file to .env.server
# NOTE: make sure you register with Username and Password (not google)
IMGFLIP_USERNAME=
IMGFLIP_PASSWORD=

# get your api key from https://platform.openai.com/
OPENAI_API_KEY=

JWT_SECRET=asecretphraseatleastthirtytwocharacterslong

Rename this file to .env.server and follow the instructions in it to get your:

as we will need them to generate our memes 🤡

Start your App

With everything setup correctly, you should now be able to run

wasp start

When running wasp start, Wasp will install all the necessary npm packages, start our NodeJS server on port 3001, and our React client on port 3000.

Head to localhost:3000 in your browser to check it out. We should have the basis for our app that looks like this:

Image description

Generating a Meme

The boilerplate code already has the client-side form set up for generating memes based on:

  • topics
  • intended audience

This is the info we will send to the backend to call the OpenAI API using function calls. We then send this info to the imglfip.com API to generate the meme.

But the /caption_image endpoint of the imgflip API needs the meme template id. And to get that ID we first need to fetch the available meme templates from imgflip’s /get_memes endpoint

So let’s set that up now.

Server-Side Code

Create a new file in src/server/ called utils.ts:

import axios from 'axios';
import { stringify } from 'querystring';
import HttpError from '@wasp/core/HttpError.js';

type GenerateMemeArgs = {
text0: string;
text1: string;
templateId: string;
};

export const fetchMemeTemplates = async () => {
try {
const response = await axios.get('https://api.imgflip.com/get_memes');
return response.data.data.memes;
} catch (error) {
console.error(error);
throw new HttpError(500, 'Error fetching meme templates');
}
};

export const generateMemeImage = async (args: GenerateMemeArgs) => {
console.log('args: ', args);

try {
const data = stringify({
template_id: args.templateId,
username: process.env.IMGFLIP_USERNAME,
password: process.env.IMGFLIP_PASSWORD,
text0: args.text0,
text1: args.text1,
});

// Implement the generation of meme using the Imgflip API
const res = await axios.post('https://api.imgflip.com/caption_image', data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});

const url = res.data.data.url;

console.log('generated meme url: ', url);

return url as string;
} catch (error) {
console.error(error);
throw new HttpError(500, 'Error generating meme image');
}
};

This gives us some utility functions to help us fetch all the meme templates that we can possibly generate meme images with.

Notice that the POST request to the /caption_image endpoint takes the following data:

  • our imgflip username and password
  • ID of the meme template we will use
  • the text for top of the meme, i.e. text0
  • the text for the bottom of the meme, i.e. text1

Image description

The text0 and text1 arguments will generated for us by our lovely friend, ChatGPT. But in order for GPT to do that, we have to set up its API call, too.

To do that, create a new file in src/server/ called actions.ts.

Then go back to your main.wasp config file and add the following Wasp Action at the bottom of the file:

//...

action createMeme {
fn: import { createMeme } from "@server/actions.js",
entities: [Meme, Template, User]
}

An Action is a type of Wasp Operation that changes some state on the backend. It’s essentially a NodeJS function that gets called on the server, but Wasp takes care of setting it all up for you.

This means you don't have to worry about building an HTTP API for the Action, managing server-side request handling, or even dealing with client-side response handling and caching. Instead, you just write the business logic!

Image description

If you’ve got the Wasp VS Code extension installed, you’ll see an error (above). Hover over it and click Quick Fix > create function createMeme.

This will scaffold a createMeme function (below) for you in your actions.ts file if the file exists. Pretty Cool!

import { CreateMeme } from '@wasp/actions/types'

type CreateMemeInput = void
type CreateMemeOutput = void

export const createMeme: CreateMeme<CreateMemeInput, CreateMemeOutput> = async (args, context) => {
// Implementation goes here
}

You can see that it imports the Action types for you as well.

Because we will be sending the topics array and the intended audience string for the meme from our front-end form, and in the end we will return the newly created Meme entity, that’s what we should define our types as.

Remember, the Meme entity is the database model we defined in our main.wasp config file.

Knowing that, we can change the content of actions.ts to this:

import type { CreateMeme } from '@wasp/actions/types'
import type { Meme } from '@wasp/entities';

type CreateMemeArgs = { topics: string[]; audience: string };

export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
// Implementation goes here
}

Before we implement the rest of the logic, let’s run through how our createMeme function should work and how our Meme will get generated:

  1. fetch the imgflip meme template we want to use
  2. send its name, the topics, and intended audience to OpenAI’s chat completions API
  3. tell OpenAI we want the result back as arguments we can pass to our next function in JSON format, i.e. OpenAI’s function calling
  4. pass those arguments to the imgflip /caption-image endpoint and get our created meme’s url
  5. save the meme url and other info into our DB as a Meme entity

With all that in mind, go ahead and entirely replace the content in our actions.ts with the completed createMeme action:

import HttpError from '@wasp/core/HttpError.js';
import OpenAI from 'openai';
import { fetchMemeTemplates, generateMemeImage } from './utils.js';

import type { CreateMeme } from '@wasp/actions/types';
import type { Meme, Template } from '@wasp/entities';

type CreateMemeArgs = { topics: string[]; audience: string };

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});

export const createMeme: CreateMeme<CreateMemeArgs, Meme> = async ({ topics, audience }, context) => {
if (!context.user) {
throw new HttpError(401, 'You must be logged in');
}

if (context.user.credits === 0 && !context.user.isAdmin) {
throw new HttpError(403, 'You have no credits left');
}

const topicsStr = topics.join(', ');

let templates: Template[] = await context.entities.Template.findMany({});

if (templates.length === 0) {
const memeTemplates = await fetchMemeTemplates();
templates = await Promise.all(
memeTemplates.map(async (template: any) => {
const addedTemplate = await context.entities.Template.upsert({
where: { id: template.id },
create: {
id: template.id,
name: template.name,
url: template.url,
width: template.width,
height: template.height,
boxCount: template.box_count
},
update: {}
});

return addedTemplate;
})
);
}

// filter out templates with box_count > 2
templates = templates.filter((template) => template.boxCount <= 2);
const randomTemplate = templates[Math.floor(Math.random() * templates.length)];

console.log('random template: ', randomTemplate);

const sysPrompt = `You are a meme idea generator. You will use the imgflip api to generate a meme based on an idea you suggest. Given a random template name and topics, generate a meme idea for the intended audience. Only use the template provided`;
const userPrompt = `Topics: ${topicsStr} \n Intended Audience: ${audience} \n Template: ${randomTemplate.name} \n`;

let openAIResponse: OpenAI.Chat.Completions.ChatCompletion;
try {
openAIResponse = await openai.chat.completions.create({
messages: [
{ role: 'system', content: sysPrompt },
{ role: 'user', content: userPrompt },
],
functions: [
{
name: 'generateMemeImage',
description: 'Generate meme via the imgflip API based on the given idea',
parameters: {
type: 'object',
properties: {
text0: { type: 'string', description: 'The text for the top caption of the meme' },
text1: { type: 'string', description: 'The text for the bottom caption of the meme' },
},
required: ['templateName', 'text0', 'text1'],
},
},
],
function_call: {
name: 'generateMemeImage',
},
model: 'gpt-4-0613',
});
} catch (error: any) {
console.error('Error calling openAI: ', error);
throw new HttpError(500, 'Error calling openAI');
}

console.log(openAIResponse.choices[0]);

/**
* the Function call returned by openAI looks like this:
*/
// {
// index: 0,
// message: {
// role: 'assistant',
// content: null,
// function_call: {
// name: 'generateMeme',
// arguments: '{\n' +
// ` "text0": "CSS you've been writing all day",\n` +
// ' "text1": "This looks horrible"\n' +
// '}'
// }
// },
// finish_reason: 'stop'
// }
if (!openAIResponse.choices[0].message.function_call) throw new HttpError(500, 'No function call in openAI response');

const gptArgs = JSON.parse(openAIResponse.choices[0].message.function_call.arguments);
console.log('gptArgs: ', gptArgs);

const memeIdeaText0 = gptArgs.text0;
const memeIdeaText1 = gptArgs.text1;

console.log('meme Idea args: ', memeIdeaText0, memeIdeaText1);

const memeUrl = await generateMemeImage({
templateId: randomTemplate.id,
text0: memeIdeaText0,
text1: memeIdeaText1,
});

const newMeme = await context.entities.Meme.create({
data: {
text0: memeIdeaText0,
text1: memeIdeaText1,
topics: topicsStr,
audience: audience,
url: memeUrl,
template: { connect: { id: randomTemplate.id } },
user: { connect: { id: context.user.id } },
},
});

return newMeme;
};

At this point, the code above should be pretty self-explanatory, but I want to highlight a couple points:

  1. the context object is passed through to all Actions and Queries by Wasp. It contains the Prisma client with access to the DB entities you defined in your main.wasp config file.
  2. We first look for the imgflip meme templates in our DB. If none are found, we fetch them using our fetchTemplates utility function we defined earlier. Then we upsert them into our DB.
  3. There are some meme templates that take more than 2 text boxes, but for this tutorial we’re only using meme templates with 2 text inputs to make it easier.
  4. We choose a random template from this list to use as a basis for our meme (it’s actually a great way to serendipitously generate some interesting meme content).
  5. We give the OpenAI API info about the functions it can create arguments for via the functions and function_call properties, which tell it to always return JSON arguments for our function, generateMemeImage

Great! But once we start generating memes, we will need a way to display them on our front end.

So let’s now create a Wasp Query. A Query works just like an Action, except it’s only for reading data.

Go to src/server and create a new file called queries.ts.

Next, in your main.wasp file add the following code:

//...

query getAllMemes {
fn: import { getAllMemes } from "@server/queries.js",
entities: [Meme]
}

Then in your queries.ts file, add the getAllMemes function:

import HttpError from '@wasp/core/HttpError.js';

import type { Meme } from '@wasp/entities';
import type { GetAllMemes } from '@wasp/queries/types';

export const getAllMemes: GetAllMemes<void, Meme[]> = async (_args, context) => {
const memeIdeas = await context.entities.Meme.findMany({
orderBy: { createdAt: 'desc' },
include: { template: true },
});

return memeIdeas;
};

Client-Side Code

Now that we’ve got the createMeme and getAllMemes code implemented server-side, let’s hook it up to our client.

Wasp makes it really easy to import the Operations we just created and call them on our front end.

You can do so by going to src/client/pages/Home.tsx and adding the following code to the top of the file:

//...other imports...
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';

export function HomePage() {
const [topics, setTopics] = useState(['']);
const [audience, setAudience] = useState('');
const [isMemeGenerating, setIsMemeGenerating] = useState(false);

// 😎 😎 😎
const { data: user } = useAuth();
const { data: memes, isLoading, error } = useQuery(getAllMemes);

const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (!user) {
history.push('/login');
return;
}
if (topics.join('').trim().length === 0 || audience.length === 0) {
alert('Please provide topic and audience');
return;
}
try {
setIsMemeGenerating(true);
await createMeme({ topics, audience }); // <--- 😎 😎 😎
} catch (error: any) {
alert('Error generating meme: ' + error.message);
} finally {
setIsMemeGenerating(false);
}
};

//...

As you can see, we’ve imported createMeme and getAllMemes (😎).

For getAllMemes, we wrap it in the useQuery hook so that we can fetch and cache the data. On the other hand, our createMeme Action gets called in handleGenerateMeme which we will call when submit our form.

Rather than adding code to the Home.tsx file piece-by-piece, here is the file with all the code to generate and display the memes. Go ahead and replace all of Home.tsx with this code and I’ll explain it in more detail below:

import { useState, FormEventHandler } from 'react';
import { useQuery } from '@wasp/queries';
import createMeme from '@wasp/actions/createMeme';
import getAllMemes from '@wasp/queries/getAllMemes';
import useAuth from '@wasp/auth/useAuth';
import { useHistory } from 'react-router-dom';
import {
AiOutlinePlusCircle,
AiOutlineMinusCircle,
AiOutlineRobot,
} from 'react-icons/ai';

export function HomePage() {
const [topics, setTopics] = useState(['']);
const [audience, setAudience] = useState('');
const [isMemeGenerating, setIsMemeGenerating] = useState(false);

const history = useHistory();
const { data: user } = useAuth();
const { data: memes, isLoading, error } = useQuery(getAllMemes);

const handleGenerateMeme: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (!user) {
history.push('/login');
return;
}
if (topics.join('').trim().length === 0 || audience.length === 0) {
alert('Please provide topic and audience');
return;
}
try {
setIsMemeGenerating(true);
await createMeme({ topics, audience });
} catch (error: any) {
alert('Error generating meme: ' + error.message);
} finally {
setIsMemeGenerating(false);
}
};

const handleDeleteMeme = async (id: string) => {
//...
};

if (isLoading) return 'Loading...';
if (error) return 'Error: ' + error;

return (
<div className='p-4'>
<h1 className='text-3xl font-bold mb-4'>Welcome to Memerator!</h1>
<p className='mb-4'>Start generating meme ideas by providing topics and intended audience.</p>
<form onSubmit={handleGenerateMeme}>
<div className='mb-4 max-w-[500px]'>
<label htmlFor='topics' className='block font-bold mb-2'>
Topics:
</label>
{topics.map((topic, index) => (
<input
key={index}
type='text'
id='topics'
value={topic}
onChange={(e) => {
const updatedTopics = [...topics];
updatedTopics[index] = e.target.value;
setTopics(updatedTopics);
}}
className='p-1 mr-1 mb-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
/>
))}
<div className='flex items-center my-2 gap-1'>
<button
type='button'
onClick={() => topics.length < 3 && setTopics([...topics, ''])}
className='flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-xs py-1 px-2 rounded'
>
<AiOutlinePlusCircle /> Add Topic
</button>
{topics.length > 1 && (
<button
onClick={() => setTopics(topics.slice(0, -1))}
className='flex items-center gap-1 bg-red-500 hover:bg-red-700 border-2 text-white text-xs py-1 px-2 rounded'
>
<AiOutlineMinusCircle /> Remove Topic
</button>
)}
</div>
</div>
<div className='mb-4'>
<label htmlFor='audience' className='block font-bold mb-2'>
Intended Audience:
</label>
<input
type='text'
id='audience'
value={audience}
onChange={(e) => setAudience(e.target.value)}
className='p-1 border rounded text-lg focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent'
/>
</div>
<button
type='submit'
className={`flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-sm font-bold py-1 px-2 rounded ${
isMemeGenerating ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
} $}`}
>
<AiOutlineRobot />
{!isMemeGenerating ? 'Generate Meme' : 'Generating...'}
</button>
</form>

{!!memes && memes.length > 0 ? (
memes.map((memeIdea) => (
<div key={memeIdea.id} className='mt-4 p-4 bg-gray-100 rounded-lg'>
<img src={memeIdea.url} width='500px' />
<div className='flex flex-col items-start mt-2'>
<div>
<span className='text-sm text-gray-700'>Topics: </span>
<span className='text-sm italic text-gray-500'>{memeIdea.topics}</span>
</div>
<div>
<span className='text-sm text-gray-700'>Audience: </span>
<span className='text-sm italic text-gray-500'>{memeIdea.audience}</span>
</div>
</div>
{/* TODO: implement edit and delete meme features */}
</div>
))
) : (
<div className='flex justify-center mt-5'> :( no memes found</div>
)}
</div>
);
}

There are two things I want to point out about this code:

  1. The useQuery hook calls our getAllMemes Query when the component mounts. It also caches the result for us, as well as automatically re-fetching whenever we add a new Meme to our DB via createMeme. This means our page will reload automatically whenever a new meme is generated.
  2. The useAuth hook allows us to fetch info about our logged in user. If the user isn’t logged in, we force them to do so before they can generate a meme.

These are really cool Wasp features that make your life as a developer a lot easier 🙂

So go ahead now and try and generate a meme. Here’s the one I just generated:

Image description

Haha. Pretty good!

Now wouldn’t it be cool though if we could edit and delete our memes? And what if we could expand the set of meme templates for our generator to use? Wouldn’t that be cool, too?

Yes, it would be. So let’s do that.

Part 2.

So we’ve got ourselves a really good basis for an app at this point.

We’re using OpenAI’s function calling feature to explain a function to GPT, and get it to return results for us in a format we can use to call that function.

This allows us to be certain GPT’s result will be usable in further parts of our application and opens up the door to creating AI agents.

If you think about it, we’ve basically got ourselves a really simple Meme generating “agent”. How cool is that?!

Fetching & Updating Templates with Cron Jobs

To be able to generate our meme images via ImgFlip’s API, we have to choose and send a meme template id to the API, along with the text arguments we want to fill it in with.

For example, the Grandma Finds Internet meme template has the following id:

Image description

But the only way for us to get available meme templates from ImgFlip is to send a GET request to https://api.imgflip.com/get_memes. And according to ImgFlip, the /get-memes endpoint works like this:

Gets an array of popular memes that may be captioned with this API. The size of this array and the order of memes may change at any time. When this description was written, it returned 100 memes ordered by how many times they were captioned in the last 30 days

So it returns a list of the top 100 memes from the last 30 days. And as this is always changing, we can run a daily cron job to fetch the list and update our database with any new templates that don’t already exist in it.

We know this will work because the ImgFlip docs for the /caption-image endpoint — which we use to create a meme image — says this:

key: template_id value: A template ID as returned by the get_memes response. Any ID that was ever returned from the get_memes response should work for this parameter…

Awesome!

Defining our Daily Cron Job

Now, to create an automatically recurring cron job in Wasp is really easy.

First, go to your main.wasp file and add:

job storeMemeTemplates {
executor: PgBoss,
perform: {
fn: import { fetchAndStoreMemeTemplates } from "@server/workers.js",
},
schedule: {
// daily at 7 a.m.
cron: "0 7 * * *"
},
entities: [Template],
}

This is telling Wasp to run the fetchAndStoreMemeTemplates function every day at 7 a.m.

Next, create a new file in src/server called workers.ts and add the function:

import axios from 'axios';

export const fetchAndStoreMemeTemplates = async (_args: any, context: any) => {
console.log('.... ><><>< get meme templates cron starting ><><>< ....');

try {
const response = await axios.get('https://api.imgflip.com/get_memes');

const promises = response.data.data.memes.map((meme: any) => {
return context.entities.Template.upsert({
where: { id: meme.id },
create: {
id: meme.id,
name: meme.name,
url: meme.url,
width: meme.width,
height: meme.height,
boxCount: meme.box_count,
},
update: {},
});
});

await Promise.all(promises);
} catch (error) {
console.error('error fetching meme templates: ', error);
}
};

You can see that we send a GET request to the proper endpoint, then we loop through the array of memes it returns to us add any new templates to the database.

Notice that we use Prisma’s upsert method here. This allows us to create a new entity in the database if it doesn’t already exist. If it does, we don’t do anything, which is why update is left blank.

We use [Promise.all() to call that array of promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) correctly.

Testing

Now, assuming you’ve got your app running with wasp start, you will see the cron job run in the console every day at 7 a.m.

If you want to test that the cron job is working correctly, you could run it on a faster schedule. Let’s try that now by changing it in our main.wasp file to run every minute:

//...
schedule: {
// runs every minute.
cron: "* * * * *"
},

First, your terminal where you ran wasp start to start your app should output the following:

[Server]  🔍 Validating environment variables...
[Server] 🚀 "Username and password" auth initialized
[Server] Starting pg-boss...
[Server] pg-boss started!
[Server] Server listening on port 3001

…followed shortly after by:

[Server]  .... ><><>< get meme templates cron starting ><><>< ....

Great. We’ve got an automatically recurring cron job going.

You can check your database for saved templates by opening another terminal window and running:

wasp db studio 

Image description

Editing Memes

Unfortunately, sometimes GPT’s results have some mistakes. Or sometimes the idea is really good, but we want to further modify it to make it even better.

Well, that’s pretty simple for us to do since we can just make another call to ImgFlip’s API.

So let’s set do that by setting up a dedicated page where we:

  • fetch that specific meme based on its id
  • display a form to allow the user to edit the meme text
  • send that info to a server-side Action which calls the ImgFlip API, generates a new image URL, and updates our Meme entity in the DB.

Server-Side Code

To make sure we can fetch the individual meme we want to edit, we first need to set up a Query that does this.

Go to your main.wasp file and add this Query declaration:

query getMeme {
fn: import { getMeme } from "@server/queries.js",
entities: [Meme]
}

Now go to src/server/queries.ts and add the following function:

import type { Meme, Template } from '@wasp/entities';
import type { GetAllMemes, GetMeme } from '@wasp/queries/types';

type GetMemeArgs = { id: string };
type GetMemeResult = Meme & { template: Template };

//...

export const getMeme: GetMeme<GetMemeArgs, GetMemeResult> = async ({ id }, context) => {
if (!context.user) {
throw new HttpError(401);
}

const meme = await context.entities.Meme.findUniqueOrThrow({
where: { id: id },
include: { template: true },
});

return meme;
};

We’re just fetching the single meme based on its id from the database.

We’re also including the related meme Template so that we have access to its id as well, because we need to send this to the ImgFlip API too.

Pretty simple!

Now let’s create our editMeme action by going to our main.wasp file and adding the following Action:

//...

action editMeme {
fn: import { editMeme } from "@server/actions.js",
entities: [Meme, Template, User]
}

Next, move over to the server/actions.ts file and let’s add the following server-side function:

//... other imports
import type { EditMeme } from '@wasp/actions/types';

//... other types
type EditMemeArgs = Pick<Meme, 'id' | 'text0' | 'text1'>;

export const editMeme: EditMeme<EditMemeArgs, Meme> = async ({ id, text0, text1 }, context) => {
if (!context.user) {
throw new HttpError(401, 'You must be logged in');
}

const meme = await context.entities.Meme.findUniqueOrThrow({
where: { id: id },
include: { template: true },
});

if (!context.user.isAdmin && meme.userId !== context.user.id) {
throw new HttpError(403, 'You are not the creator of this meme');
}

const memeUrl = await generateMemeImage({
templateId: meme.template.id,
text0: text0,
text1: text1,
});

const newMeme = await context.entities.Meme.update({
where: { id: id },
data: {
text0: text0,
text1: text1,
url: memeUrl,
},
});

return newMeme;
};

As you can see, this function expects the id of the already existing meme, along with the new text boxes. That’s because we’re letting the user manually input/edit the text that GPT generated, rather than making another request the the OpenAI API.

Next, we look for that specific meme in our database, and if we don’t find it we throw an error (findUniqueOrThrow).

We check to make sure that that meme belongs to the user that is currently making the request, because we don’t want a different user to edit a meme that doesn’t belong to them.

Then we send the template id of that meme along with the new text to our previously created generateMemeImage function. This function calls the ImgFlip API and returns the url of the newly created meme image.

We then update the database to save the new URL to our Meme.

Awesome!

Client-Side Code

Let’s start by adding a new route and page to our main.wasp file:

//...

route EditMemeRoute { path: "/meme/:id", to: EditMemePage }
page EditMemePage {
component: import { EditMemePage } from "@client/pages/EditMemePage",
authRequired: true
}

There are two important things to notice:

  1. the path includes the :id parameter, which means we can access page for any meme in our database by going to, e.g. memerator.com/meme/5
  2. by using the authRequired option, we tell Wasp to automatically block this page from unauthorized users. Nice!

Now, create this page by adding a new file called EditMemePage.tsx to src/client/pages. Add the following code:

import { useState, useEffect, FormEventHandler } from 'react';
import { useQuery } from '@wasp/queries';
import editMeme from '@wasp/actions/editMeme';
import getMeme from '@wasp/queries/getMeme';
import { useParams } from 'react-router-dom';
import { AiOutlineEdit } from 'react-icons/ai';

export function EditMemePage() {
// http://localhost:3000/meme/573f283c-24e2-4c45-b6b9-543d0b7cc0c7
const { id } = useParams<{ id: string }>();

const [text0, setText0] = useState('');
const [text1, setText1] = useState('');
const [isLoading, setIsLoading] = useState(false);

const { data: meme, isLoading: isMemeLoading, error: memeError } = useQuery(getMeme, { id: id });

useEffect(() => {
if (meme) {
setText0(meme.text0);
setText1(meme.text1);
}
}, [meme]);

const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
try {
setIsLoading(true);
await editMeme({ id, text0, text1 });
} catch (error: any) {
alert('Error generating meme: ' + error.message);
} finally {
setIsLoading(false);
}
};

if (isMemeLoading) return 'Loading...';
if (memeError) return 'Error: ' + memeError.message;

return (
<div className='p-4'>
<h1 className='text-3xl font-bold mb-4'>Edit Meme</h1>
<form onSubmit={handleSubmit}>
<div className='flex gap-2 items-end'>
<div className='mb-2'>
<label htmlFor='text0' className='block font-bold mb-2'>
Text 0:
</label>
<textarea
id='text0'
value={text0}
onChange={(e) => setText0(e.target.value)}
className='border rounded px-2 py-1'
/>
</div>
<div className='mb-2'>
<label htmlFor='text1' className='block font-bold mb-2'>
Text 1:
</label>

<div className='flex items-center mb-2'>
<textarea
id='text1'
value={text1}
onChange={(e) => setText1(e.target.value)}
className='border rounded px-2 py-1'
/>
</div>
</div>
</div>

<button
type='submit'
className={`flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-sm py-1 px-2 rounded ${
isLoading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
} $}`}
>
<AiOutlineEdit />
{!isLoading ? 'Save Meme' : 'Saving...'}
</button>
</form>
{!!meme && (
<div className='mt-4 mb-2 bg-gray-100 rounded-lg p-4'>
<img src={meme.url} width='500px' />
<div className='flex flex-col items-start mt-2'>
<div>
<span className='text-sm text-gray-700'>Topics: </span>
<span className='text-sm italic text-gray-500'>{meme.topics}</span>
</div>
<div>
<span className='text-sm text-gray-700'>Audience: </span>
<span className='text-sm italic text-gray-500'>{meme.audience}</span>
</div>
<div>
<span className='text-sm text-gray-700'>ImgFlip Template: </span>
<span className='text-sm italic text-gray-500'>{meme.template.name}</span>
</div>
</div>
</div>
)}
</div>
);
}

Some things to notice here are:

  1. because we’re using dynamic routes (/meme/:id), we pull the URL paramater id from the url with useParams hook.
  2. we then pass that id within the getMemes Query to fetch that specific meme to edit: useQuery(getMeme, { id: id })
    1. remember, our server-side action depends on this id in order to fetch the meme from our database

The rest of the page is just our form for calling the editMeme Action, as well as displaying the meme we want to edit.

That’s great!

Now that we have that EditMemePage, we need a way to navigate to it from the home page.

To do that, go back to the Home.tsx file, add the following imports at the top, and find the comment that says {/* TODO: implement edit and delete meme features */} and replace it with the following code:

import { Link } from '@wasp/router';
import { AiOutlineEdit } from 'react-icons/ai';

//...

{user && (user.isAdmin || user.id === memeIdea.userId) && (
<div className='flex items-center mt-2'>
<Link key={memeIdea.id} params={{ id: memeIdea.id }} to={`/meme/:id`}>
<button className='flex items-center gap-1 bg-primary-200 hover:bg-primary-300 border-2 text-black text-xs py-1 px-2 rounded'>
<AiOutlineEdit />
Edit Meme
</button>
</Link>
{/* TODO: add delete meme functionality */}
</div>
)}

What’s really cool about this, is that Wasp’s Link component will give you type-safe routes, by making sure you’re following the pattern you defined in your main.wasp file.

And with that, so long as the authenticated user was the creator of the meme (or is an admin), the Edit Meme button will show up and direct the user to the EditMemePage

Give it a try now. It should look like this:

Deleting Memes

Ok. When I initially started writing this tutorial, I thought I’d also explain how to add delete meme functionality to the app as well.

But seeing as we’ve gotten this far, and as the entire two-part tutorial is pretty long, I figured you should be able to implement yourself by this point.

So I’ll leave you guide as to how to implement it yourself. Think of it as a bit of homework:

  1. define the deleteMeme Action in your main.wasp file
  2. export the async function from the actions.ts file
  3. import the Action in your client-side code
  4. create a button which takes the meme’s id as an argument in your deleteMeme Action.

If you get stuck, you can use the editMeme section as a guide. Or you can check out the finished app’s GitHub repo for the completed code!

Conclusion

There you have it! Your own instant meme generator 🤖😆

BTW, If you found this useful, please show us your support by giving us a star on GitHub! It will help us continue to make more stuff just like it.

https://res.cloudinary.com/practicaldev/image/fetch/s--tnDxibZC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://res.cloudinary.com/practicaldev/image/fetch/s--OCpry2p9--/c_limit%252Cf_auto%252Cfl_progressive%252Cq_66%252Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bky8z46ii7ayejprrqw3.gif

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/10/04/contributing-open-source-land-a-job.html b/blog/2023/10/04/contributing-open-source-land-a-job.html index 2186da078c..010327eda4 100644 --- a/blog/2023/10/04/contributing-open-source-land-a-job.html +++ b/blog/2023/10/04/contributing-open-source-land-a-job.html @@ -19,15 +19,15 @@ - - + +
-

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

· 10 min read
Vinny

TL;DR

How to Open-Source +

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

· 10 min read
Vinny

TL;DR

How to Open-Source In this article, we’re going to see how open-source can change your career for the better and get you out of the Skill Paradox — a point in which the skills you need to land a job are generally acquired after you get a job.

Besides that, we’ll check how you can start contributing to different open-source projects and get on the hype train of Hacktoberfest while also learning some important topics on handling feedbacks and showcasing your contributions.

1. Introduction

Are you a beginner developer that lacks certain skills needed to land a job? But you feel that you could only gain those skills on the job itself? -If you answered “yes”, then you’re stuck in situation that I would call as the “skill paradox where you need skills to get a job, but those skills are the ones you would get if you had a job. It can generate a lot of stress and frustration when you start to realize that some skills cannot be obtained while working only on side hustles and therefore, you cannot learn only by yourself, but they’re generally required for job positions.

Collaboration and teamwork, learning how to code review (giving and receiving feedback), and getting started with bigger and existing codebases are things that cannot be taught while you work on some little projects. While, of course, you can learn those skills while getting a job in tech, sometimes those skills are necessary for you to get a job, making you stay in some kind of limbo where you need some skills to get a job, and those skills are precisely the ones you would get after the job.

In those cases, there’s still a way out of the limbo: you can contribute to open-source communities. Besides the value you are generating for the whole ecosystem, this can be an amazing selling point for your career and, since Hacktoberfest is already around the corner, will be a great way to win a t-shirt or plant a tree too!

Now, let’s begin by teaching you how to actually do this.

2. First steps on Open-Source Contribution

2.1. Finding a project

First of all, we need to choose a project. If you’re a beginner, you’re probably looking for projects that have a few characteristics:

  • It’s actively maintained.
  • Has an open-source license that we can modify and use freely.
  • It’s not insanely big (since these projects can have some really hard things to accomplish before submitting something).
  • It must have good documentation on how to contribute.
  • It must have well-characterized issues in order for you to search for something (in the case that you haven’t found the problem itself).

If you have matches in all of these points (or at least three of them), you’re good to go!

Throughout this article, I’m going to use our own repo, Wasp Full-stack Framework, since it gathers all the characteristics necessary for a good open-source repository.

So, let me show you how to find all these characteristics:

  • It’s actively maintained and the owners of the repo reply and care for the issues!
    • In the case of Wasp’s repo, the last commit was 13 hours ago, so, there’s definitely signs of life here!

Last commit

  • It’s not insanely big → Comparing an exaggerated example with the Linux repo (if you check it, you’ll see that all pull requests there usually take a lot of time to be merged since the project is so big)

Linux repo

  • It’s good to have a documentation on how to contribute
    • Searching for the docs, I found a file called CONTRIBUTING.md (which is a common name standard for contribution guidelines) and when we open it up:

Contributing guidelines

We have a whole documentation on how to start with things! Awesome!

  • It’s good to have well characterized issues in order for you to search for something

Issues

Searching for the issues, we can easily see that they’re all labeled and that will help us A TON!

2.2. Searching for Issues

Great! Now that we have already chosen where we are going to contribute, let’s dive into the issues and search for something we want to do!

When searching for issues, the labels do us a great favor by already explicitly identifying all issues that can be good for newcomers! If you’re a beginner, good first issues and documentation are excellent labels for you to search for!

Good labels to search for

Issues on the repo labeled

Opening the first issue, we can see that someone already manifested interest on it! So, since someone has already manifested interest in that one, let’s search for another one!

The first issue

Finding another issue — it doesn’t look like anyone is working on the one below, so we can take it ourselves!

Finding another issue

By the way, it's of absolute importance that, when you find an issue, you comment and set yourself as assignee in order to let other people know that you're going to take the task at hand!

Communicating

In this case, GitHub is a great platform for us to discuss, but sometimes authors can be hard to find. In these cases, search for a link or a way to contact them directly (in the case of Wasp, they have a Discord server, for example). Communicating your way through is really important to get things sorted out, and if you’re unsure of how to communicate well with people, you can read this other article here and start to get the hang of it!

3. Guidelines for Contributing to Open-Source Projects

3.1. Reading the guidelines and writing some code

Now that we have selected a repo, an issue to work on and communicated with the authors, it’s time to check the guidelines for making Pull Requests (if you don’t know what this means, it’s basically a request to merge your modifications to the codebase, you can check some more basic git terms here too). Sometimes, these guidelines are WAY too hard and sometimes they don’t even exist (that’s an awesome first issue actually), anyways look it up and see if you find something!

You can check Wasp’s contributing guidelines here if you want to read it yourself! After reading it, it’s time to code the solution and get along with it.

Since the intent of this article is not to actually show the solving per se, I’ll skip this part and keep talking about the process itself.

3.2 Handling Code Reviews and Feedback

It’s not rare that when we code things up (especially in open-source projects), there will be some problems. Code reviews and feedback are an amazing way for us to get the bigger picture and improve our code quality, so let’s check on how to properly read and answer code reviews and feedback.

We’re generally used to receiving criticism in a harsh way, so, when someone approaches you with feedbacks, we generally move into our defense zone. Unfortunately, these cases can teach you the wrong things as it’s generally a good way to think of feedbacks as gifts! Someone spent some time writing (or speaking) things in order for you get even better on what you’re trying to accomplish.

This does not mean that all feedback is well-made or that people will always provide great feedback. Sometimes, people can be harsh. However, as you receive more and more feedbacks, you will develop a sense of which feedbacks are genuinely meant to help you improve and which are simply baseless criticism. It is crucial to be open to receiving constructive feedbacks and not take them personally.

Let’s see an example of code review and feedback here:

Code Review Example

This is great feedback! It expresses the author’s opinion without being harsh and also suggests what to make in order to be perfect! The best way to answer this is simply:

  • Thanking for the feedback
  • Saying your opinion (agree or disagree) when it makes sense
  • Work on it!

Showcasing Contributions

After all that work, it’s time for us to showcase our contributions! Document it all. GitHub (or other git platforms), personal portfolio sites, LinkedIn, and other means of reaching people have become as important as resumes nowadays, so it’s really nice to have some statistics and data to display on:

  • What open-source projects have you worked on? Try to think of this as writing a story. First, start by giving the initial context of the project and how it’s revelatory.
  • How you contributed: Then, give the context of what you made, documentation, code, and problems you solved in general. Don’t forget to not focus a lot on the technical side since the person who could be reading this may not be technical.
  • How big was the impact? Talk about how this affected the ecosystem; it can be as big or as small as you like. Never neglect the impact that changing documentation can have (remember that for us, programmers, the documentation is our source of truth, and fixes there are greatly appreciated).

Don’t forget to utilize the opportunity to engage with other developers and communities, make it so in order to get new connections and even greater opportunities later on!

Now that the theory is set, let’s check a few examples on how I would showcase a few of my contributions:

Case 1 - A big contribution

One of the ways to describe a big contribution is like this:

I made a few big contributions to a project called Coolify, which was an open-source Heroku alternative. I refactored a lot of the UI, making it cleaner and more consistent throughout the application. Currently, more than 9000 instances are installed, and the UI affects all of them! You can check out the contributions here.

Of course, you can make this text as long or as short as you want, entering more detail about how this contribution was made and what exactly you did, but for this article, this is enough for you to get a general idea.

Case 2- A small contribution

One way to describe a small contribution is like this:

I made a small change to the new documentation for Sequelize! I was just scrolling through the documentation and found this mistake that could lead others to weird debugging sessions, so as soon as I found it, I submitted a PR for them! You can check out the contribution here!

Conclusion

So, a lot was said, let’s make a quick recap on how to do contributions and how to showcase them:

  • First of all, find a repo! If you don’t have any in mind, there loads of lists (like this one) that recommend some repos for you to take a look
  • Search for an issue that is not being made and you can work on it, if you’re beginner, check for documentation and good first issue labels
  • Comment and communicate that you’re going to fix the issue - take the opportunity to talk and get to know other developers
  • Code, get you PR reviewed and ready to merge after the feedbacks
  • Merge and showcase your contributions, showing that they are your way out of the Skill Paradox

How to Open-Source

The above steps can give you a really powerful experience in software engineering (which usually happens only when you’re already hired by a company). This is an awesome way to get some recognition while improving the open-source community — giving back to other developers and getting yourself out of the Skill Paradox!

And you? Have you contributed to open-source? Let me know in the comments below, and let’s share some experiences!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +If you answered “yes”, then you’re stuck in situation that I would call as the “skill paradox where you need skills to get a job, but those skills are the ones you would get if you had a job. It can generate a lot of stress and frustration when you start to realize that some skills cannot be obtained while working only on side hustles and therefore, you cannot learn only by yourself, but they’re generally required for job positions.

Collaboration and teamwork, learning how to code review (giving and receiving feedback), and getting started with bigger and existing codebases are things that cannot be taught while you work on some little projects. While, of course, you can learn those skills while getting a job in tech, sometimes those skills are necessary for you to get a job, making you stay in some kind of limbo where you need some skills to get a job, and those skills are precisely the ones you would get after the job.

In those cases, there’s still a way out of the limbo: you can contribute to open-source communities. Besides the value you are generating for the whole ecosystem, this can be an amazing selling point for your career and, since Hacktoberfest is already around the corner, will be a great way to win a t-shirt or plant a tree too!

Now, let’s begin by teaching you how to actually do this.

2. First steps on Open-Source Contribution

2.1. Finding a project

First of all, we need to choose a project. If you’re a beginner, you’re probably looking for projects that have a few characteristics:

  • It’s actively maintained.
  • Has an open-source license that we can modify and use freely.
  • It’s not insanely big (since these projects can have some really hard things to accomplish before submitting something).
  • It must have good documentation on how to contribute.
  • It must have well-characterized issues in order for you to search for something (in the case that you haven’t found the problem itself).

If you have matches in all of these points (or at least three of them), you’re good to go!

Throughout this article, I’m going to use our own repo, Wasp Full-stack Framework, since it gathers all the characteristics necessary for a good open-source repository.

So, let me show you how to find all these characteristics:

  • It’s actively maintained and the owners of the repo reply and care for the issues!
    • In the case of Wasp’s repo, the last commit was 13 hours ago, so, there’s definitely signs of life here!

Last commit

  • It’s not insanely big → Comparing an exaggerated example with the Linux repo (if you check it, you’ll see that all pull requests there usually take a lot of time to be merged since the project is so big)

Linux repo

  • It’s good to have a documentation on how to contribute
    • Searching for the docs, I found a file called CONTRIBUTING.md (which is a common name standard for contribution guidelines) and when we open it up:

Contributing guidelines

We have a whole documentation on how to start with things! Awesome!

  • It’s good to have well characterized issues in order for you to search for something

Issues

Searching for the issues, we can easily see that they’re all labeled and that will help us A TON!

2.2. Searching for Issues

Great! Now that we have already chosen where we are going to contribute, let’s dive into the issues and search for something we want to do!

When searching for issues, the labels do us a great favor by already explicitly identifying all issues that can be good for newcomers! If you’re a beginner, good first issues and documentation are excellent labels for you to search for!

Good labels to search for

Issues on the repo labeled

Opening the first issue, we can see that someone already manifested interest on it! So, since someone has already manifested interest in that one, let’s search for another one!

The first issue

Finding another issue — it doesn’t look like anyone is working on the one below, so we can take it ourselves!

Finding another issue

By the way, it's of absolute importance that, when you find an issue, you comment and set yourself as assignee in order to let other people know that you're going to take the task at hand!

Communicating

In this case, GitHub is a great platform for us to discuss, but sometimes authors can be hard to find. In these cases, search for a link or a way to contact them directly (in the case of Wasp, they have a Discord server, for example). Communicating your way through is really important to get things sorted out, and if you’re unsure of how to communicate well with people, you can read this other article here and start to get the hang of it!

3. Guidelines for Contributing to Open-Source Projects

3.1. Reading the guidelines and writing some code

Now that we have selected a repo, an issue to work on and communicated with the authors, it’s time to check the guidelines for making Pull Requests (if you don’t know what this means, it’s basically a request to merge your modifications to the codebase, you can check some more basic git terms here too). Sometimes, these guidelines are WAY too hard and sometimes they don’t even exist (that’s an awesome first issue actually), anyways look it up and see if you find something!

You can check Wasp’s contributing guidelines here if you want to read it yourself! After reading it, it’s time to code the solution and get along with it.

Since the intent of this article is not to actually show the solving per se, I’ll skip this part and keep talking about the process itself.

3.2 Handling Code Reviews and Feedback

It’s not rare that when we code things up (especially in open-source projects), there will be some problems. Code reviews and feedback are an amazing way for us to get the bigger picture and improve our code quality, so let’s check on how to properly read and answer code reviews and feedback.

We’re generally used to receiving criticism in a harsh way, so, when someone approaches you with feedbacks, we generally move into our defense zone. Unfortunately, these cases can teach you the wrong things as it’s generally a good way to think of feedbacks as gifts! Someone spent some time writing (or speaking) things in order for you get even better on what you’re trying to accomplish.

This does not mean that all feedback is well-made or that people will always provide great feedback. Sometimes, people can be harsh. However, as you receive more and more feedbacks, you will develop a sense of which feedbacks are genuinely meant to help you improve and which are simply baseless criticism. It is crucial to be open to receiving constructive feedbacks and not take them personally.

Let’s see an example of code review and feedback here:

Code Review Example

This is great feedback! It expresses the author’s opinion without being harsh and also suggests what to make in order to be perfect! The best way to answer this is simply:

  • Thanking for the feedback
  • Saying your opinion (agree or disagree) when it makes sense
  • Work on it!

Showcasing Contributions

After all that work, it’s time for us to showcase our contributions! Document it all. GitHub (or other git platforms), personal portfolio sites, LinkedIn, and other means of reaching people have become as important as resumes nowadays, so it’s really nice to have some statistics and data to display on:

  • What open-source projects have you worked on? Try to think of this as writing a story. First, start by giving the initial context of the project and how it’s revelatory.
  • How you contributed: Then, give the context of what you made, documentation, code, and problems you solved in general. Don’t forget to not focus a lot on the technical side since the person who could be reading this may not be technical.
  • How big was the impact? Talk about how this affected the ecosystem; it can be as big or as small as you like. Never neglect the impact that changing documentation can have (remember that for us, programmers, the documentation is our source of truth, and fixes there are greatly appreciated).

Don’t forget to utilize the opportunity to engage with other developers and communities, make it so in order to get new connections and even greater opportunities later on!

Now that the theory is set, let’s check a few examples on how I would showcase a few of my contributions:

Case 1 - A big contribution

One of the ways to describe a big contribution is like this:

I made a few big contributions to a project called Coolify, which was an open-source Heroku alternative. I refactored a lot of the UI, making it cleaner and more consistent throughout the application. Currently, more than 9000 instances are installed, and the UI affects all of them! You can check out the contributions here.

Of course, you can make this text as long or as short as you want, entering more detail about how this contribution was made and what exactly you did, but for this article, this is enough for you to get a general idea.

Case 2- A small contribution

One way to describe a small contribution is like this:

I made a small change to the new documentation for Sequelize! I was just scrolling through the documentation and found this mistake that could lead others to weird debugging sessions, so as soon as I found it, I submitted a PR for them! You can check out the contribution here!

Conclusion

So, a lot was said, let’s make a quick recap on how to do contributions and how to showcase them:

  • First of all, find a repo! If you don’t have any in mind, there loads of lists (like this one) that recommend some repos for you to take a look
  • Search for an issue that is not being made and you can work on it, if you’re beginner, check for documentation and good first issue labels
  • Comment and communicate that you’re going to fix the issue - take the opportunity to talk and get to know other developers
  • Code, get you PR reviewed and ready to merge after the feedbacks
  • Merge and showcase your contributions, showing that they are your way out of the Skill Paradox

How to Open-Source

The above steps can give you a really powerful experience in software engineering (which usually happens only when you’re already hired by a company). This is an awesome way to get some recognition while improving the open-source community — giving back to other developers and getting yourself out of the Skill Paradox!

And you? Have you contributed to open-source? Let me know in the comments below, and let’s share some experiences!

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/2023/10/12/on-importance-of-naming-in-programming.html b/blog/2023/10/12/on-importance-of-naming-in-programming.html index f8a74e07c3..42b511c5f2 100644 --- a/blog/2023/10/12/on-importance-of-naming-in-programming.html +++ b/blog/2023/10/12/on-importance-of-naming-in-programming.html @@ -3,7 +3,7 @@ -On Importance of Naming in Programming | Wasp +On the Importance of Naming in Programming | Wasp @@ -19,16 +19,16 @@ - - + +
-

On Importance of Naming in Programming

· 12 min read
Martin Sosic

In stories, you will often find the motif of a powerful demon that can be controlled only by knowing its true name. Once the hero finds out that name, through cunning dialogue or by investigating ancient tomes, they can turn things around and banish the demon!

I firmly believe writing code is not much different: through finding good names for functions, variables, and other constructs, we truly recognize the essence of the problem we are solving. The consequence of clarity gained is not just good names but also cleaner code and improved architecture.

The power of correct naming in programming

I would go as far as to say that 90% of writing clean code is “just” naming things correctly. +

On the Importance of Naming in Programming

· 12 min read
Martin Sosic

In stories, you will often find the motif of a powerful demon that can be controlled only by knowing its true name. Once the hero finds out that name, through cunning dialogue or by investigating ancient tomes, they can turn things around and banish the demon!

I firmly believe writing code is not much different: through finding good names for functions, variables, and other constructs, we truly recognize the essence of the problem we are solving. The consequence of clarity gained is not just good names but also cleaner code and improved architecture.

The power of correct naming in programming

I would go as far as to say that 90% of writing clean code is “just” naming things correctly. Sounds simple, but it is really not!

Let’s take a look at a couple of examples.

Example #1

// Given first and last name of a person, returns the
// demographic statistics for all matching people.
async function demo (a, b) {
const c = await users(a, b);
return [
avg(c.map(a => a.info[0])),
median(c.map(a => a.info[1]))
];
}

What is wrong with this code?

  1. The name of the function demo is very vague: it could stand for “demolish”, or as in “giving a demo/presentation”, … .
  2. Names a, b, and c are completely uninformative.
  3. a is reused in lambda inside the map, shadowing the a that is a function argument, confusing the reader and making it easier to make a mistake when modifying the code in the future and reference the wrong variable.
  4. The returned object doesn’t have any info about what it contains, instead, you need to be careful about the order of its elements when using it later.
  5. The name of the field .info in the result of a call to users() function gives us no information as to what it contains, which is made further worse by its elements being accessed by their position, also hiding any information about them and making our code prone to silently work wrong if their ordering changes.

Let’s fix it:

async function fetchDemographicStatsForFirstAndLastName (
firstName, lastName
) {
const users = await fetchUsersByFirstAndLastName(
firstName, lastName
);
return {
averageAge: avg(users.map(u => u.stats.age)),
medianSalary: median(users.map(u => u.stats.salary))
};
}

What did we do?

  1. The name of the function now exactly reflects what it does, no more no less. fetch in the name even indicates it does some IO (input/output, in this case fetching from the database), which can be good to know since IO is relatively slow/expensive compared to pure code.
  2. We made other names informative enough: not too much, not too little.
    • Notice how we used the name users for fetched users, and not something longer like usersWithSpecifiedFirstAndLastName or fetchedUsers: there is no need for a longer name, as this variable is very local, short-lived, and there is enough context around it to make it clear what it is about.
    • Inside lambda, we went with a single-letter name, u, which might seem like bad practice. But, here, it is perfect: this variable is extremely short-lived, and it is clear from context what it stands for. Also, we picked specifically the letter u for a reason, as it is the first letter of user, therefore making that connection obvious.
  3. We named values in the object that we return: averageAge and medianSalary. Now any code that will use our function won’t need to rely on the ordering of items in the result, and also will be easy and informative to read.

Finally, notice how there is no comment above the function anymore. The thing is, the comment is not needed anymore: it is all clear from the function name and arguments!

Example #2

// Find a free machine and use it, or create a new machine
// if needed. Then on that machine, set up the new worker
// with the given Docker image and setup cmd. Finally,
// start executing a job on that worker and return its id.
async function getJobId (
machineType, machineRegion,
workerDockerImage, workerSetupCmd,
jobDescription
) {
...
}

In this example, we are ignoring the implementation details and will focus just on getting the name and arguments right.

What is wrong with this code?

  1. The function name is hiding a lot of details about what it is doing. It doesn’t mention at all that we have to procure the machine or set up the worker, or that function will result in the creation of a job that will continue executing somewhere in the background. Instead, it gives a feeling that we are doing something simple, due to the verb get: we are just obtaining an id of an already existing job. Imagine seeing a call to this function somewhere in the code: getJobId(...)you are not expecting it to take long or do all of the stuff that it really does, which is bad.

Ok, this sounds easy to fix, let’s give it a better name!

async function procureFreeMachineAndSetUpTheDockerWorkerThenStartExecutingTheJob (
machineType, machineRegion,
workerDockerImage, workerSetupCmd,
jobDescription
) {
...
}

Uff, that is one long and complicated name. But the truth is, that we can’t really make it shorter without losing valuable information about what this function does and what we can expect from it. Therefore, we are stuck, we can’t find a better name! What now?

The thing is, you can't give a good name if you don't have clean code behind it. So a bad name is not just a naming mishap, but often also an indicator of problematic code behind it, a failure in design. Code so problematic, that you don’t even know what to name it → there is no straightforward name to give to it, because it is not a straightforward code!

Bad name is hiding bad code

In our case, the problem is that this function is trying to do too much at once. A long name and many arguments are indicators of this, although these can be okay in some situations. Stronger indicators are the usage of words “and” and “then” in the name, as well as argument names that can be grouped by prefixes (machine, worker).

The solution here is to clean up the code by breaking down the function into multiple smaller functions:

async function procureFreeMachine (type, region) { ... }
async function setUpDockerWorker (machineId, dockerImage, setupCmd) { ... }
async function startExecutingJob (workerId, jobDescription) { ... }

What is a good name?

But let’s take a step back - what is a bad name, and what is a good name? What does that mean, how do we recognize them?

Good name doesn’t misdirect, doesn’t omit, and doesn’t assume.

A good name should give you a good idea about what the variable contains or function does. A good name will tell you all there is to know or will tell you enough to know where to look next. It will not let you guess, or wonder. It will not misguide you. A good name is obvious, and expected. It is consistent. Not overly creative. It will not assume context or knowledge that the reader is not likely to have.

Also, context is king: you can’t evaluate the name without the context in which it is read. verifyOrganizationChainCredentials could be a terrible name or a great name. a could be a great name or a terrible name. It depends on the story, the surroundings, on the problem the code is solving. Names tell a story, and they need to fit together like a story.

Examples of famous bad names

  • JavaScript
    • I was the victim of this bad naming myself: my parents bought me a book about JavaScript while I wanted to learn Java.
  • HTTP Authorization header
  • Wasp-lang:
    • This one is my fault: Wasp is a full-stack JS web framework that uses a custom config language as only a small part of its codebase, but I put -lang in the name and scared a lot of people away because they thought it was a whole new general programming language!

How to come up with a good name

Don’t give a name, find it

The best advice is maybe not to give a name, but instead to find out a name. You shouldn’t be making up an original name, as if you are naming a pet or a child; you are instead looking for the essence of the thing you are naming, and the name should present itself based on it. If you don’t like the name you discovered, it means you don’t like the thing you are naming, and you should change that thing by improving the design of your code (as we did in the example #2).

You shouldn't name your variables the same way you name your pets, and vice versa

Things to look out for when figuring out a name

  1. First, make sure it is not a bad name :). Remember: don’t misdirect, don’t omit, don’t assume.
  2. Make it reflect what it represents. Find the essence of it, capture it in the name. Name is still ugly? Improve the code. You have also other things to help you here → type signature, and comments. But those come secondary.
  3. Make it play nicely with the other names around it. It should have a clear relation to them - be in the same “world”. It should be similar to similar stuff, opposite to opposite stuff. It should make a story together with other names around it. It should take into account the context it is in.
  4. Length follows the scope. In general, the shorter-lived the name is, and the smaller its scope is, the shorter the name can/should be, and vice versa. This is why it can be ok to use one-letter variables in short lambda functions. If not sure, go for the longer name.
  5. Stick to the terminology you use in the codebase. If you so far used the term server, don’t for no reason start using the term backend instead. Also, if you use server as a term, you likely shouldn't go with frontend: instead, you will likely want to use client, which is a term more closely related to the server.
  6. Stick to the conventions you use in the codebase. Examples of some of the conventions that I often use in my codebases:
    • prefix is when the variable is Bool (e.g. isAuthEnabled)
    • prefix ensure for the functions that are idempotent, that will do something (e.g allocate a resource) only if it hasn’t been set up so far (e.g. ensureServerIsRunning).

The simple technique for figuring out a name every time

If you are ever having trouble coming up with a name, do the following:

  1. Write a comment above the function/variable where you describe what it is, in human language, as if you were describing it to your colleague. It might be one sentence or multiple sentences. This is the essence of what your function/variable does, what it is.
  2. Now, you take the role of the sculptor, and you chisel at and shape that description of your function/variable until you get a name, by taking pieces of it away. You stop when you feel that one more hit of your imagined chisel at it would take too much away.
  3. Is your name still too complex/confusing? If that is so, that means that the code behind is too complex, and should be reorganized! Go refactor it.
  4. Ok, all done → you have a nice name!
  5. That comment above the function/variable? Remove everything from it that is now captured in the code (name + arguments + type signature). If you can remove the whole comment, great. Sometimes you can’t, because some stuff can’t be captured in the code (e.g. certain assumptions, explanations, examples, …), and that is also okay. But don’t repeat in the comment what you can say in the code instead. Comments are a necessary evil and are here to capture knowledge that you can’t capture in your names and/or types.

Don’t get overly stuck on always figuring out the perfect name at the get-go → it is okay to do multiple iterations of your code, with both your code and name improving with each iteration.

Reviewing code with naming in mind

Once you start thinking a lot about naming, you will see how it will change your code review process: focus shifts from looking at implementation details to looking at names first.

When I am doing a code review, there is one predominant thought I will be thinking about: “Is this name clear?”. From there, the whole review evolves and results in clean code.

Inspecting a name is a single point of pressure, that untangles the whole mess behind it. Search for bad names, and you will sooner or later uncover the bad code if there is some.

Further reading

If you haven’t yet read it, I would recommend reading the book Clean Code by Robert Martin. It has a great chapter on naming and also goes much further on how to write code that you and others will enjoy reading and maintaining.

Also, A popular joke about naming being hard.

Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - + + \ No newline at end of file diff --git a/blog/2023/10/13/wasp-launch-week-four.html b/blog/2023/10/13/wasp-launch-week-four.html index 847beb5c88..1655d060dc 100644 --- a/blog/2023/10/13/wasp-launch-week-four.html +++ b/blog/2023/10/13/wasp-launch-week-four.html @@ -19,13 +19,13 @@ - - + +
-

Wasp Launch Week #4: Waspolution

· 5 min read
Matija Sosic

Launch Week 4 is coming

We're back! Beginning of the October was both the craziest and busiest time we've ever had at Wasp - we ended up on GitHub Trending (almost at 7,000 stars - thank you🙏), MAGE (our GPT web app generator) exploded with 20,000 apps created and more people than ever used Wasp!

Crazily enough, we've even had a first startup project made in Wasp that has been acquired (GPT-powered, of course)! 💸🐝💸

To top it all off, we have a new launch week incoming that brings a ton of new exciting product updates! If this is your first rodeo, check out our previous launches:

What's this launch all about?

As you might have noticed, each of our launches comes with a specific theme. We've come a long way since our first launch week, Beta release, which moved Wasp from a prototype to a real, working framework. In the previous two launch weeks we've added plenty of new features and unlocked functionalities you couldn't have used before (e.g. email sending, async jobs, ...).

This time we kept introducing new features, but we also realised there are many opportunities to make developers' lives even easier. That's why the theme of this launch week stems from "Evolution" - Wasp is now well set on its way, lies on the solid foundations with a strong community behind it and keeps naturally evolving!

Growing up
Wasp from this launch onwards.

Enough chit-chat - let's see what will go down next week! We'll present a new feature (or more of them) every day. To stay in the loop follow us on Twitter/X (@WaspLang) and join our community on Wasp Discord!

Launch party 🚀🎉

launch event 3 - screenshot
A bit of the atmosphere from LW3 launch party!

As it is a tradition by now, we'll kick things off with a launch party on our Discord! You will be able to meet the team and be the first one to learn about the new features we'll be revealing for the rest of the week. We'll also answer community questions, discuss plans for the future, and of course, hand out some sweet swag (finally get your hands on that Da Boi plushie)!

The party starts at 11 am EDT / 5 pm CET - sign up here and make sure to mark yourself as "interested"!

launch event - how to join

Monday: I am Speed 🚄

Why waste time
We think the same, but about keystrokes.

We all know that developer productivity is a hot topic these days. At the end of the day, why waste time use many keystroke when few do trick?

That's exactly what we will feature on Monday! Wasp is already famous for its brevity and prototyping speed, which is powered by its high-level configuration language, but we found a way to make things even simpler!

When: Monday, October 16 2023

Tuesday: Safety first 👷

Realtime

In every industry they have strict safety protocols - we believe programming should be no different! Especially when it comes to types - imagine if you had a piece of data running around your application, without even knowing what it looks like!? No sir, not under my watch ⬇️⌚️.

When: Tuesday, October 17 2023

Wednesday: Wasp x AI x ...base 🤖⚡️

Power Rangers

The best things happen when you combine multiple amazing things together - and that's exactly what we did! I don't want to spoil too much, but let's just say it has become much easier to do a certain similarity search with Wasp 😉.

I don't want to overhype it, but it might be one of the coolest things you've seen so far - see you on Wednesday!

When: Wednesday, October 18 2023

Thursday: A glimpse into the future 🛸

World if everyone used Wasp for web development

Although there is a plenty of work to refine the existing features and polish the overal developer experience, we still always have our eyes on the future and take time to experiment. This is what we will present here - a really cool feature that is possible due to the Wasp's unique approach, that will illuminate a lot posibilities for the future!

When: Thursday, October 19 2023

Friday: Polish 💅

Wax on, Wax off

Sometimes, the best thing you can do is take care of what you already have! As we mentioned in the intro, Wasp is becoming all about DX, feature completeness and elegance of use. And this is what we will demonstrate today!

When: Friday, October 20 2023

Monday: SaaS-a-thon!

Hacking

As the ancient scrolls say, every launch week must end with a hackathon, and this is no exception! We'll share more details soon, but as the title says, we'll equip you as well as possible to create a SaaS of your dreams in no time!

When: Monday, October 23 2023

Recap

  • We are kicking off Launch Week #4 on Mon, Oct 16, at 11am EDT / 5pm CET - make sure to register for the event!
  • Launch Week #4 brings a ton of new exciting features - we’ll highlight one each day, starting Monday. Follow us on twitter and join our Discord to stay in the loop!
  • Following launch week, we’ll announce a SaaS-a-thon - get your keyboards warmed up and ready to roll!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

- - +

Wasp Launch Week #4: Waspolution

· 5 min read
Matija Sosic

Launch Week 4 is coming

We're back! Beginning of the October was both the craziest and busiest time we've ever had at Wasp - we ended up on GitHub Trending (almost at 7,000 stars - thank you🙏), MAGE (our GPT web app generator) exploded with 20,000 apps created and more people than ever used Wasp!

Crazily enough, we've even had a first startup project made in Wasp that has been acquired (GPT-powered, of course)! 💸🐝💸

To top it all off, we have a new launch week incoming that brings a ton of new exciting product updates! If this is your first rodeo, check out our previous launches:

What's this launch all about?

As you might have noticed, each of our launches comes with a specific theme. We've come a long way since our first launch week, Beta release, which moved Wasp from a prototype to a real, working framework. In the previous two launch weeks we've added plenty of new features and unlocked functionalities you couldn't have used before (e.g. email sending, async jobs, ...).

This time we kept introducing new features, but we also realised there are many opportunities to make developers' lives even easier. That's why the theme of this launch week stems from "Evolution" - Wasp is now well set on its way, lies on the solid foundations with a strong community behind it and keeps naturally evolving!

Growing up
Wasp from this launch onwards.

Enough chit-chat - let's see what will go down next week! We'll present a new feature (or more of them) every day. To stay in the loop follow us on Twitter/X (@WaspLang) and join our community on Wasp Discord!

Launch party 🚀🎉

launch event 3 - screenshot
A bit of the atmosphere from LW3 launch party!

As it is a tradition by now, we'll kick things off with a launch party on our Discord! You will be able to meet the team and be the first one to learn about the new features we'll be revealing for the rest of the week. We'll also answer community questions, discuss plans for the future, and of course, hand out some sweet swag (finally get your hands on that Da Boi plushie)!

The party starts at 11 am EDT / 5 pm CET - sign up here and make sure to mark yourself as "interested"!

launch event - how to join

Monday: I am Speed 🚄

Why waste time
We think the same, but about keystrokes.

We all know that developer productivity is a hot topic these days. At the end of the day, why waste time use many keystroke when few do trick?

That's exactly what we will feature on Monday! Wasp is already famous for its brevity and prototyping speed, which is powered by its high-level configuration language, but we found a way to make things even simpler!

When: Monday, October 16 2023

Tuesday: Safety first 👷

Realtime

In every industry they have strict safety protocols - we believe programming should be no different! Especially when it comes to types - imagine if you had a piece of data running around your application, without even knowing what it looks like!? No sir, not under my watch ⬇️⌚️.

When: Tuesday, October 17 2023

Wednesday: Wasp x AI x ...base 🤖⚡️

Power Rangers

The best things happen when you combine multiple amazing things together - and that's exactly what we did! I don't want to spoil too much, but let's just say it has become much easier to do a certain similarity search with Wasp 😉.

I don't want to overhype it, but it might be one of the coolest things you've seen so far - see you on Wednesday!

When: Wednesday, October 18 2023

Thursday: A glimpse into the future 🛸

World if everyone used Wasp for web development

Although there is a plenty of work to refine the existing features and polish the overal developer experience, we still always have our eyes on the future and take time to experiment. This is what we will present here - a really cool feature that is possible due to the Wasp's unique approach, that will illuminate a lot posibilities for the future!

When: Thursday, October 19 2023

Friday: Polish 💅

Wax on, Wax off

Sometimes, the best thing you can do is take care of what you already have! As we mentioned in the intro, Wasp is becoming all about DX, feature completeness and elegance of use. And this is what we will demonstrate today!

When: Friday, October 20 2023

Monday: SaaS-a-thon!

Hacking

As the ancient scrolls say, every launch week must end with a hackathon, and this is no exception! We'll share more details soon, but as the title says, we'll equip you as well as possible to create a SaaS of your dreams in no time!

When: Monday, October 23 2023

Recap

  • We are kicking off Launch Week #4 on Mon, Oct 16, at 11am EDT / 5pm CET - make sure to register for the event!
  • Launch Week #4 brings a ton of new exciting features - we’ll highlight one each day, starting Monday. Follow us on twitter and join our Discord to stay in the loop!
  • Following launch week, we’ll announce a SaaS-a-thon - get your keyboards warmed up and ready to roll!
Discord

Join our developer community

Wasp is 100% open source. Join our Discord to learn from others and get help whenever you need it!

Join our Discord 👾
📫

Subscribe to our newsletter

Once per month - receive useful blog posts and Wasp news.

+ + \ No newline at end of file diff --git a/blog/archive.html b/blog/archive.html index bf7e961d9b..ac30e9fd09 100644 --- a/blog/archive.html +++ b/blog/archive.html @@ -19,13 +19,13 @@ - - + +
-

Archive

Archive

2022

2023

- - +

Archive

Archive

2022

2023

+ + \ No newline at end of file diff --git a/blog/atom.xml b/blog/atom.xml index 5dae4ae57d..7f9916cd14 100644 --- a/blog/atom.xml +++ b/blog/atom.xml @@ -22,7 +22,7 @@ - <![CDATA[On Importance of Naming in Programming]]> + <![CDATA[On the Importance of Naming in Programming]]> https://wasp-lang.dev/blog/2023/10/12/on-importance-of-naming-in-programming 2023-10-12T00:00:00.000Z @@ -823,7 +823,7 @@ on putting "Hacktoberfest" topic on your GitHub repo won't do the trick, not wit 2022-09-05T00:00:00.000Z - We’ll build a web app to solve every developer's most common problem – finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can’t excuse ourselves from building it!

Best excuse of all time

Best excuse of all time! Taken from here.

The requirements were unclear.

We’ll use Michele Gerarduzzi’s open-source project. It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let’s define the requirements for the project:

  • The app should be able to pull excuses data from a public API.
  • Save the ones you liked (and your boss doesn't) to the database for future reference.
  • Building an app shouldn’t take more than 15 minutes.
  • Use modern web dev technologies (NodeJS + React)

As a result – we’ll get a simple and fun pet project. You can find the complete codebase here.

Final result

There’s an issue with the third party library.

Setting up a backbone for the project is the most frustrating part of building any application.

We are installing dependencies, tying up the back-end and front-end, setting up a database, managing connection strings, and so on. Avoiding this part will save us a ton of time and effort. So let’s find ourselves an excuse to skip the initial project setup.

Ideally – use a framework that will create a project infrastructure quickly with the best defaults so that we’ll focus on the business logic. A perfect candidate is Wasp. It’s an open-source, declarative DSL for building web apps in React and Node.js with no boilerplate

How it works: developer starts from a single config file that specifies the app architecture. Routes, CRUD API, auth, and so on. Then adds React/Node.js code for the specific business logic. Behind the scenes, Wasp compiler will produce the entire source code of the app - back-end, front-end, deployment template, database migrations and everything else you’ve used to have in any other full-stack app.

Wasp architecture

So let’s jump right in.

Maybe something's wrong with the environment.

Wasp intentionally works with the LTS Node.js version since it guarantees stability and active maintenance. As for now, it’s Node 16 and NPM 8. If you need another Node version for some other project – there’s a possibility to use NVM to manage multiple Node versions on your computer at the same time.

Installing Wasp on Linux (for Mac/Windows, please check the docs):

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

Now let’s create a new web app named ItWaspsOnMyMachine.

wasp new ItWaspsOnMyMachine

Changing the working directory:

cd ItWaspsOnMyMachine

Starting the app:

wasp start

Now your default browser should open up with a simple predefined text message. That’s it! 🥳 We’ve built and run a NodeJS + React application. And for now – the codebase consists of only two files! main.wasp is the config file that defines the application’s functionality. And MainPage.js is the front-end.

Initial page

That worked perfectly when I developed it.

1) Let’s add some additional configuration to our main.wasp file. So it will look like this:

main.wasp | Defining Excuse entity, queries and action

// Main declaration, defines a new web app.
app ItWaspsOnMyMachine {
// Wasp compiler configuration
wasp: {
version: "^0.6.0"
},

// Used as a browser tab title.
title: "It Wasps On My Machine",

head: [
// Adding Tailwind to make our UI prettier
"<script src='https://cdn.tailwindcss.com'></script>"
],

dependencies: [
// Adding Axios for making HTTP requests
("axios", "^0.21.1")
]
}

// Render page MainPage on url `/` (default url).
route RootRoute { path: "/", to: MainPage }

// ReactJS implementation of our page located in `src/client/MainPage.js` as a default export.
page MainPage {
component: import Main from "@client/MainPage.js"
}

// Prisma database entity
entity Excuse {=psl
id Int @id @default(autoincrement())
text String
psl=}

// Query declaration to get a new excuse
query getExcuse {
fn: import { getExcuse } from "@server/queries.js",
entities: [Excuse]
}

// Query declaration to get all excuses
query getAllSavedExcuses {
fn: import { getAllSavedExcuses } from "@server/queries.js",
entities: [Excuse]
}

// Action to save current excuse
action saveExcuse {
fn: import { saveExcuse } from "@server/actions.js",
entities: [Excuse]
}

We’ve added Tailwind to make our UI more pretty and Axios for making API requests.

Also, we’ve declared a database entity called Excuse, queries, and action. The Excuse entity consists of the entity’s ID and the text.

Queries are here when we need to fetch/read something, while actions are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info in the docs. So let’s proceed with queries/actions.

2) Create two files: “actions.js” and “queries.js” in the src/server folder.

src/server/actions.js | Defining an action
export const saveExcuse = async (excuse, context) => {
return context.entities.Excuse.create({
data: { text: excuse.text }
})
}
src/server/queries.js | Defining queries
import axios from 'axios';

export const getExcuse = async () => {
const response = await axios.get('https://api.devexcus.es/')
return response.data
}

export const getAllSavedExcuses = async (_args, context) => {
return context.entities.Excuse.findMany()
}

Let’s add saveExcuse() action to our actions.js file. This action will save the text of our excuse to the database. Then let’s create two queries in the queries.js file. First, one getExcuse will call an external API and fetch a new excuse. The second one, named getAllSavedExcuses, will pull all the excuses we’ve saved to our database.

That’s it! We finished our back-end. 🎉 Now, let’s use those queries/actions on our UI.

3) Let’s erase everything we had in the MainPage.js file and substitute it with our new UI.

src/client/MainPage.js | Updating the UI
import React, { useState } from 'react'
import { useQuery } from '@wasp/queries'
import getExcuse from '@wasp/queries/getExcuse'
import getAllSavedExcuses from '@wasp/queries/getAllSavedExcuses'
import saveExcuse from '@wasp/actions/saveExcuse'

const MainPage = () => {
const [currentExcuse, setCurrentExcuse] = useState({ text: "" })
const { data: excuses } = useQuery(getAllSavedExcuses)

const handleGetExcuse = async () => {
try {
setCurrentExcuse(await getExcuse())
} catch (err) {
window.alert('Error while getting the excuse: ' + err.message)
}
}

const handleSaveExcuse = async () => {
if (currentExcuse.text) {
try {
await saveExcuse(currentExcuse)
} catch (err) {
window.alert('Error while saving the excuse: ' + err.message)
}
}
}

return (
<div className="grid grid-cols-2 text-3xl">
<div>
<button onClick={handleGetExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Get excuse </button>
<button onClick={handleSaveExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Save excuse </button>
<Excuse excuse={currentExcuse} />
</div>
<div>
<div className="px-6 py-2 bg-blue-600 text-white"> Saved excuses: </div>
{excuses && <ExcuseList excuses={excuses} />}
</div>
</div>
)
}

const ExcuseList = (props) => {
return props.excuses?.length ? props.excuses.map((excuse, idx) => <Excuse excuse={excuse} key={idx} />) : 'No saved excuses'
}

const Excuse = ({ excuse }) => {
return (
<div className="px-6 py-2">
{excuse.text}
</div>
)
}

export default MainPage

Our page consists of three components. MainPage, ExcuseList and Excuse. It may seem at first that this file is pretty complex. It’s not, so let’s look a bit closer.

Excuse is just a div with an excuse text, ExcuseList checks if there are any excuses. If the list is empty – show a message No saved excuses. In other case – excuses will be displayed.

MainPage contains info about the current excuses and the list of already saved excuses. Two buttons click handlers handleGetExcuse and handleSaveExcuse. Plus, the markup itself with some Tailwind flavor.

4) Before starting an app – we need to execute database migration because we changed the DB schema by adding new entities. If you’ve had something running in the terminal – stop it and run:

wasp db migrate-dev

You’ll be prompted to enter a name for the migration. Something like init will be ok. Now we can start the application!

wasp start

Final empty result

Now you can click the “Get excuse” button to receive an excuse. And save the ones you like into the DB with the “Save excuse” button. Our final project should look like this:

Final result

It would have taken twice as long to build it properly.

Now we can think of some additional improvements. For example:

  • 1) Add a unique constraint to Entity’s ID so we won’t be able to save duplicated excuses.
  • 2) Add exceptions and edge cases handling.
  • 3) Make the markup prettier.
  • 4) Optimize and polish the code

So, we’ve been able to build a full-stack application with a database and external API call in a couple of minutes. And now we have a box full of excuses for all our development needs.

Box of excuses for the win!

]]>
+ We’ll build a web app to solve every developer's most common problem – finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can’t excuse ourselves from building it!

Best excuse of all time

Best excuse of all time! Taken from here.

The requirements were unclear.

We’ll use Michele Gerarduzzi’s open-source project. It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let’s define the requirements for the project:

  • The app should be able to pull excuses data from a public API.
  • Save the ones you liked (and your boss doesn't) to the database for future reference.
  • Building an app shouldn’t take more than 15 minutes.
  • Use modern web dev technologies (NodeJS + React)

As a result – we’ll get a simple and fun pet project. You can find the complete codebase here.

Final result

There’s an issue with the third party library.

Setting up a backbone for the project is the most frustrating part of building any application.

We are installing dependencies, tying up the back-end and front-end, setting up a database, managing connection strings, and so on. Avoiding this part will save us a ton of time and effort. So let’s find ourselves an excuse to skip the initial project setup.

Ideally – use a framework that will create a project infrastructure quickly with the best defaults so that we’ll focus on the business logic. A perfect candidate is Wasp. It’s an open-source, declarative DSL for building web apps in React and Node.js with no boilerplate

How it works: developer starts from a single config file that specifies the app architecture. Routes, CRUD API, auth, and so on. Then adds React/Node.js code for the specific business logic. Behind the scenes, Wasp compiler will produce the entire source code of the app - back-end, front-end, deployment template, database migrations and everything else you’ve used to have in any other full-stack app.

Wasp architecture

So let’s jump right in.

Maybe something's wrong with the environment.

Wasp intentionally works with the LTS Node.js version since it guarantees stability and active maintenance. As for now, it’s Node 16 and NPM 8. If you need another Node version for some other project – there’s a possibility to use NVM to manage multiple Node versions on your computer at the same time.

Installing Wasp on Linux (for Mac/Windows, please check the docs):

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

Now let’s create a new web app named ItWaspsOnMyMachine.

wasp new ItWaspsOnMyMachine

Changing the working directory:

cd ItWaspsOnMyMachine

Starting the app:

wasp start

Now your default browser should open up with a simple predefined text message. That’s it! 🥳 We’ve built and run a NodeJS + React application. And for now – the codebase consists of only two files! main.wasp is the config file that defines the application’s functionality. And MainPage.js is the front-end.

Initial page

That worked perfectly when I developed it.

1) Let’s add some additional configuration to our main.wasp file. So it will look like this:

main.wasp | Defining Excuse entity, queries and action

// Main declaration, defines a new web app.
app ItWaspsOnMyMachine {
// Wasp compiler configuration
wasp: {
version: "^0.6.0"
},

// Used as a browser tab title.
title: "It Wasps On My Machine",

head: [
// Adding Tailwind to make our UI prettier
"<script src='https://cdn.tailwindcss.com'></script>"
],

dependencies: [
// Adding Axios for making HTTP requests
("axios", "^0.21.1")
]
}

// Render page MainPage on url `/` (default url).
route RootRoute { path: "/", to: MainPage }

// ReactJS implementation of our page located in `src/client/MainPage.js` as a default export.
page MainPage {
component: import Main from "@client/MainPage.js"
}

// Prisma database entity
entity Excuse {=psl
id Int @id @default(autoincrement())
text String
psl=}

// Query declaration to get a new excuse
query getExcuse {
fn: import { getExcuse } from "@server/queries.js",
entities: [Excuse]
}

// Query declaration to get all excuses
query getAllSavedExcuses {
fn: import { getAllSavedExcuses } from "@server/queries.js",
entities: [Excuse]
}

// Action to save current excuse
action saveExcuse {
fn: import { saveExcuse } from "@server/actions.js",
entities: [Excuse]
}

We’ve added Tailwind to make our UI more pretty and Axios for making API requests.

Also, we’ve declared a database entity called Excuse, queries, and action. The Excuse entity consists of the entity’s ID and the text.

Queries are here when we need to fetch/read something, while actions are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info in the docs. So let’s proceed with queries/actions.

2) Create two files: “actions.js” and “queries.js” in the src/server folder.

src/server/actions.js | Defining an action
export const saveExcuse = async (excuse, context) => {
return context.entities.Excuse.create({
data: { text: excuse.text }
})
}
src/server/queries.js | Defining queries
import axios from 'axios';

export const getExcuse = async () => {
const response = await axios.get('https://api.devexcus.es/')
return response.data
}

export const getAllSavedExcuses = async (_args, context) => {
return context.entities.Excuse.findMany()
}

Let’s add saveExcuse() action to our actions.js file. This action will save the text of our excuse to the database. Then let’s create two queries in the queries.js file. First, one getExcuse will call an external API and fetch a new excuse. The second one, named getAllSavedExcuses, will pull all the excuses we’ve saved to our database.

That’s it! We finished our back-end. 🎉 Now, let’s use those queries/actions on our UI.

3) Let’s erase everything we had in the MainPage.js file and substitute it with our new UI.

src/client/MainPage.js | Updating the UI
import React, { useState } from 'react'
import { useQuery } from '@wasp/queries'
import getExcuse from '@wasp/queries/getExcuse'
import getAllSavedExcuses from '@wasp/queries/getAllSavedExcuses'
import saveExcuse from '@wasp/actions/saveExcuse'

const MainPage = () => {
const [currentExcuse, setCurrentExcuse] = useState({ text: "" })
const { data: excuses } = useQuery(getAllSavedExcuses)

const handleGetExcuse = async () => {
try {
setCurrentExcuse(await getExcuse())
} catch (err) {
window.alert('Error while getting the excuse: ' + err.message)
}
}

const handleSaveExcuse = async () => {
if (currentExcuse.text) {
try {
await saveExcuse(currentExcuse)
} catch (err) {
window.alert('Error while saving the excuse: ' + err.message)
}
}
}

return (
<div className="grid grid-cols-2 text-3xl">
<div>
<button onClick={handleGetExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Get excuse </button>
<button onClick={handleSaveExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Save excuse </button>
<Excuse excuse={currentExcuse} />
</div>
<div>
<div className="px-6 py-2 bg-blue-600 text-white"> Saved excuses: </div>
{excuses && <ExcuseList excuses={excuses} />}
</div>
</div>
)
}

const ExcuseList = (props) => {
return props.excuses?.length ? props.excuses.map((excuse, idx) => <Excuse excuse={excuse} key={idx} />) : 'No saved excuses'
}

const Excuse = ({ excuse }) => {
return (
<div className="px-6 py-2">
{excuse.text}
</div>
)
}

export default MainPage

Our page consists of three components. MainPage, ExcuseList and Excuse. It may seem at first that this file is pretty complex. It’s not, so let’s look a bit closer.

Excuse is just a div with an excuse text, ExcuseList checks if there are any excuses. If the list is empty – show a message No saved excuses. In other case – excuses will be displayed.

MainPage contains info about the current excuses and the list of already saved excuses. Two buttons click handlers handleGetExcuse and handleSaveExcuse. Plus, the markup itself with some Tailwind flavor.

4) Before starting an app – we need to execute database migration because we changed the DB schema by adding new entities. If you’ve had something running in the terminal – stop it and run:

wasp db migrate-dev

You’ll be prompted to enter a name for the migration. Something like init will be ok. Now we can start the application!

wasp start

Final empty result

Now you can click the “Get excuse” button to receive an excuse. And save the ones you like into the DB with the “Save excuse” button. Our final project should look like this:

Final result

It would have taken twice as long to build it properly.

Now we can think of some additional improvements. For example:

  • 1) Add a unique constraint to Entity’s ID so we won’t be able to save duplicated excuses.
  • 2) Add exceptions and edge cases handling.
  • 3) Make the markup prettier.
  • 4) Optimize and polish the code

So, we’ve been able to build a full-stack application with a database and external API call in a couple of minutes. And now we have a box full of excuses for all our development needs.

Box of excuses for the win!

]]>
Maksym Khamrovskyi @@ -993,7 +993,7 @@ them done.

I still occasionally need to give this advice to myself :).

2022-01-27T00:00:00.000Z - Hello, Waspleau

See Waspleau here! | See the code

We've built a dashboard powered by a job queue using Wasp!

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.

Measure all the metrics!

Hello, Waspleau!

Let’s face it - metrics are all around us. Wouldn’t it be great if there was a quick and easy way to build a nice-looking metrics dashboard from data pulled in by HTTP calls to many different sources, cache the data in-memory, and periodically update it via background jobs? Why yes, yes it would... so we made an example Wasp app called Waspleau that does just that!

Here is what it looks like live: https://waspleau.netlify.app/ There is also a screenshot at the top of this post for those who refrain from clicking on any unknown web links for fear of being Rickrolled. Respect.

“Show me the code”

So, what do we need to get started? First, we need a way to schedule and run jobs; for this, we decided to use Bull. Ok, let’s wire it up. This should be easy, right? We can add external NPM dependencies in our Wasp files like so:

main.wasp
app waspleau {
title: "Waspleau",

dependencies: [
("bull", "4.1.1"),
("axios", "^0.21.1")
]
}

But where do we declare our queue and processing callback functions in Wasp? Uh oh...

Sad

server.setupFn for queue setup

Thankfully, Waspleau can leverage a powerful and flexible hook supplied by Wasp called server.setupFn. This declares a JavaScript function that will be executed on server start. Yahoo! This means we can do things like the following:

main.wasp
app waspleau {
...

server: {
setupFn: import serverSetup from "@server/serverSetup.js"
}
}
src/server/serverSetup.js
import Queue from 'bull'

const queue = new Queue('waspleau', process.env.REDIS_URL || 'redis://127.0.0.1:6379',
{ defaultJobOptions: { removeOnComplete: true } }
)

queue.process('*', async (job) => { ... })

export default async () => {
// To initially populate the queue, we can do:
await queue.add({ ... }) // first run, one-off job
await queue.add({ ... }, { repeat: { cron: '*/10 * * * *' } }) // recurring job
}

Abstracting workers and job processing

Awesome, we can now enqueue and process background jobs, but how can we make it easy to create many different kinds of jobs and schedule them to run at different intervals? For Waspleau, we created our own type of worker object convention to help standardize and simplify adding more:

src/server/workers/template.js
const workerFunction = async (opts) => {
return [
{ name: 'Metric 1 name', value: 'foo', updatedAt: ... },
{ name: 'Metric 2 name', value: 'bar', updatedAt: ... },
]
}

export const workerTemplate = { name: 'Job Name', fn: workerFunction, schedule: '*/10 * * * *' }

With this workerFunction setup, we can return one or more metrics per worker type. Waspleau can easily use any module that exports this shape. Here is a real example from the demo that makes HTTP calls to GitHub’s API with Axios:

src/server/workers/github.js
import axios from 'axios'

const workerFunction = async (opts) => {
console.log('github.js workerFunction')

const now = Date.now()

try {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

return [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count, updatedAt: now },
{ name: 'Wasp GitHub Language', value: response.data.language, updatedAt: now },
{ name: 'Wasp GitHub Forks', value: response.data.forks, updatedAt: now },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues, updatedAt: now },
]
} catch (error) {
console.error(error)
return []
}
}

export const githubWorker = { name: 'GitHub API', fn: workerFunction, schedule: '*/10 * * * *' }

Note: Please see the actual serverSetup.js file for how we use this abstraction in practice.

Server → client

We now have jobs running and data updating at regular intervals, nice, but we still need a way to send that data down the wire. Here, we expose the in-memory data from our server.setupFn module so our queries can also use it:

main.wasp
...

query dashboard {
fn: import { refreshDashboardData } from "@server/dashboard.js"
}
src/server/dashboard.js
import { getDashboardData } from './serverSetup.js'

export const refreshDashboardData = async (_args, _context) => {
return getDashboardData()
}
src/server/serverSetup.js
...

const dashboardData = {} // This is updated in the queue process callback
export const getDashboardData = () => Object.values(dashboardData).flat()

From there, we can request it on the frontend in React components as usual and also set a one-minute client-side refetch interval just for good measure:

src/client/MainPage.js
...

const { data: dashboardData, isFetching, error } = useQuery(refreshDashboardData, null, { refetchInterval: 60 * 1000 })

...

Congratulations, let’s dance!

Whew, we did it! If you’d like to deploy your own customized version of this dashboard, please clone our repo and check out the Waspleau example README.md for tips on getting started. You can also check out our docs to dive deeper into anything.

Rickroll

Still got ya! :D

2022 is going to be exciting 🚀

While this functionality currently exists outside of Wasp, keep an eye on our roadmap as we head toward 1.0. We will be busy adding lots of great features to our Wasp DSL in the coming months that will supercharge your web development experience! Thanks for reading, and please feel free to connect with us in Discord about using Wasp on your next project.

]]>
+ Hello, Waspleau

See Waspleau here! | See the code

We've built a dashboard powered by a job queue using Wasp!

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.

Measure all the metrics!

Hello, Waspleau!

Let’s face it - metrics are all around us. Wouldn’t it be great if there was a quick and easy way to build a nice-looking metrics dashboard from data pulled in by HTTP calls to many different sources, cache the data in-memory, and periodically update it via background jobs? Why yes, yes it would... so we made an example Wasp app called Waspleau that does just that!

Here is what it looks like live: https://waspleau-app-client.fly.dev/ There is also a screenshot at the top of this post for those who refrain from clicking on any unknown web links for fear of being Rickrolled. Respect.

“Show me the code”

So, what do we need to get started? First, we need a way to schedule and run jobs; for this, we decided to use Bull. Ok, let’s wire it up. This should be easy, right? We can add external NPM dependencies in our Wasp files like so:

main.wasp
app waspleau {
title: "Waspleau",

dependencies: [
("bull", "4.1.1"),
("axios", "^0.21.1")
]
}

But where do we declare our queue and processing callback functions in Wasp? Uh oh...

Sad

server.setupFn for queue setup

Thankfully, Waspleau can leverage a powerful and flexible hook supplied by Wasp called server.setupFn. This declares a JavaScript function that will be executed on server start. Yahoo! This means we can do things like the following:

main.wasp
app waspleau {
...

server: {
setupFn: import serverSetup from "@server/serverSetup.js"
}
}
src/server/serverSetup.js
import Queue from 'bull'

const queue = new Queue('waspleau', process.env.REDIS_URL || 'redis://127.0.0.1:6379',
{ defaultJobOptions: { removeOnComplete: true } }
)

queue.process('*', async (job) => { ... })

export default async () => {
// To initially populate the queue, we can do:
await queue.add({ ... }) // first run, one-off job
await queue.add({ ... }, { repeat: { cron: '*/10 * * * *' } }) // recurring job
}

Abstracting workers and job processing

Awesome, we can now enqueue and process background jobs, but how can we make it easy to create many different kinds of jobs and schedule them to run at different intervals? For Waspleau, we created our own type of worker object convention to help standardize and simplify adding more:

src/server/workers/template.js
const workerFunction = async (opts) => {
return [
{ name: 'Metric 1 name', value: 'foo', updatedAt: ... },
{ name: 'Metric 2 name', value: 'bar', updatedAt: ... },
]
}

export const workerTemplate = { name: 'Job Name', fn: workerFunction, schedule: '*/10 * * * *' }

With this workerFunction setup, we can return one or more metrics per worker type. Waspleau can easily use any module that exports this shape. Here is a real example from the demo that makes HTTP calls to GitHub’s API with Axios:

src/server/workers/github.js
import axios from 'axios'

const workerFunction = async (opts) => {
console.log('github.js workerFunction')

const now = Date.now()

try {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

return [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count, updatedAt: now },
{ name: 'Wasp GitHub Language', value: response.data.language, updatedAt: now },
{ name: 'Wasp GitHub Forks', value: response.data.forks, updatedAt: now },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues, updatedAt: now },
]
} catch (error) {
console.error(error)
return []
}
}

export const githubWorker = { name: 'GitHub API', fn: workerFunction, schedule: '*/10 * * * *' }

Note: Please see the actual serverSetup.js file for how we use this abstraction in practice.

Server → client

We now have jobs running and data updating at regular intervals, nice, but we still need a way to send that data down the wire. Here, we expose the in-memory data from our server.setupFn module so our queries can also use it:

main.wasp
...

query dashboard {
fn: import { refreshDashboardData } from "@server/dashboard.js"
}
src/server/dashboard.js
import { getDashboardData } from './serverSetup.js'

export const refreshDashboardData = async (_args, _context) => {
return getDashboardData()
}
src/server/serverSetup.js
...

const dashboardData = {} // This is updated in the queue process callback
export const getDashboardData = () => Object.values(dashboardData).flat()

From there, we can request it on the frontend in React components as usual and also set a one-minute client-side refetch interval just for good measure:

src/client/MainPage.js
...

const { data: dashboardData, isFetching, error } = useQuery(refreshDashboardData, null, { refetchInterval: 60 * 1000 })

...

Congratulations, let’s dance!

Whew, we did it! If you’d like to deploy your own customized version of this dashboard, please clone our repo and check out the Waspleau example README.md for tips on getting started. You can also check out our docs to dive deeper into anything.

Rickroll

Still got ya! :D

2022 is going to be exciting 🚀

While this functionality currently exists outside of Wasp, keep an eye on our roadmap as we head toward 1.0. We will be busy adding lots of great features to our Wasp DSL in the coming months that will supercharge your web development experience! Thanks for reading, and please feel free to connect with us in Discord about using Wasp on your next project.

]]>
Shayne Czyzewski https://github.com/shayneczyzewski diff --git a/blog/rss.xml b/blog/rss.xml index e12e8e73bb..13cb621371 100644 --- a/blog/rss.xml +++ b/blog/rss.xml @@ -19,7 +19,7 @@ update - <![CDATA[On Importance of Naming in Programming]]> + <![CDATA[On the Importance of Naming in Programming]]> https://wasp-lang.dev/blog/2023/10/12/on-importance-of-naming-in-programming https://wasp-lang.dev/blog/2023/10/12/on-importance-of-naming-in-programming Thu, 12 Oct 2023 00:00:00 GMT @@ -637,7 +637,7 @@ on putting "Hacktoberfest" topic on your GitHub repo won't do the trick, not wit https://wasp-lang.dev/blog/2022/09/05/dev-excuses-app-tutrial Mon, 05 Sep 2022 00:00:00 GMT - We’ll build a web app to solve every developer's most common problem – finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can’t excuse ourselves from building it!

Best excuse of all time

Best excuse of all time! Taken from here.

The requirements were unclear.

We’ll use Michele Gerarduzzi’s open-source project. It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let’s define the requirements for the project:

  • The app should be able to pull excuses data from a public API.
  • Save the ones you liked (and your boss doesn't) to the database for future reference.
  • Building an app shouldn’t take more than 15 minutes.
  • Use modern web dev technologies (NodeJS + React)

As a result – we’ll get a simple and fun pet project. You can find the complete codebase here.

Final result

There’s an issue with the third party library.

Setting up a backbone for the project is the most frustrating part of building any application.

We are installing dependencies, tying up the back-end and front-end, setting up a database, managing connection strings, and so on. Avoiding this part will save us a ton of time and effort. So let’s find ourselves an excuse to skip the initial project setup.

Ideally – use a framework that will create a project infrastructure quickly with the best defaults so that we’ll focus on the business logic. A perfect candidate is Wasp. It’s an open-source, declarative DSL for building web apps in React and Node.js with no boilerplate

How it works: developer starts from a single config file that specifies the app architecture. Routes, CRUD API, auth, and so on. Then adds React/Node.js code for the specific business logic. Behind the scenes, Wasp compiler will produce the entire source code of the app - back-end, front-end, deployment template, database migrations and everything else you’ve used to have in any other full-stack app.

Wasp architecture

So let’s jump right in.

Maybe something's wrong with the environment.

Wasp intentionally works with the LTS Node.js version since it guarantees stability and active maintenance. As for now, it’s Node 16 and NPM 8. If you need another Node version for some other project – there’s a possibility to use NVM to manage multiple Node versions on your computer at the same time.

Installing Wasp on Linux (for Mac/Windows, please check the docs):

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

Now let’s create a new web app named ItWaspsOnMyMachine.

wasp new ItWaspsOnMyMachine

Changing the working directory:

cd ItWaspsOnMyMachine

Starting the app:

wasp start

Now your default browser should open up with a simple predefined text message. That’s it! 🥳 We’ve built and run a NodeJS + React application. And for now – the codebase consists of only two files! main.wasp is the config file that defines the application’s functionality. And MainPage.js is the front-end.

Initial page

That worked perfectly when I developed it.

1) Let’s add some additional configuration to our main.wasp file. So it will look like this:

main.wasp | Defining Excuse entity, queries and action

// Main declaration, defines a new web app.
app ItWaspsOnMyMachine {
// Wasp compiler configuration
wasp: {
version: "^0.6.0"
},

// Used as a browser tab title.
title: "It Wasps On My Machine",

head: [
// Adding Tailwind to make our UI prettier
"<script src='https://cdn.tailwindcss.com'></script>"
],

dependencies: [
// Adding Axios for making HTTP requests
("axios", "^0.21.1")
]
}

// Render page MainPage on url `/` (default url).
route RootRoute { path: "/", to: MainPage }

// ReactJS implementation of our page located in `src/client/MainPage.js` as a default export.
page MainPage {
component: import Main from "@client/MainPage.js"
}

// Prisma database entity
entity Excuse {=psl
id Int @id @default(autoincrement())
text String
psl=}

// Query declaration to get a new excuse
query getExcuse {
fn: import { getExcuse } from "@server/queries.js",
entities: [Excuse]
}

// Query declaration to get all excuses
query getAllSavedExcuses {
fn: import { getAllSavedExcuses } from "@server/queries.js",
entities: [Excuse]
}

// Action to save current excuse
action saveExcuse {
fn: import { saveExcuse } from "@server/actions.js",
entities: [Excuse]
}

We’ve added Tailwind to make our UI more pretty and Axios for making API requests.

Also, we’ve declared a database entity called Excuse, queries, and action. The Excuse entity consists of the entity’s ID and the text.

Queries are here when we need to fetch/read something, while actions are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info in the docs. So let’s proceed with queries/actions.

2) Create two files: “actions.js” and “queries.js” in the src/server folder.

src/server/actions.js | Defining an action
export const saveExcuse = async (excuse, context) => {
return context.entities.Excuse.create({
data: { text: excuse.text }
})
}
src/server/queries.js | Defining queries
import axios from 'axios';

export const getExcuse = async () => {
const response = await axios.get('https://api.devexcus.es/')
return response.data
}

export const getAllSavedExcuses = async (_args, context) => {
return context.entities.Excuse.findMany()
}

Let’s add saveExcuse() action to our actions.js file. This action will save the text of our excuse to the database. Then let’s create two queries in the queries.js file. First, one getExcuse will call an external API and fetch a new excuse. The second one, named getAllSavedExcuses, will pull all the excuses we’ve saved to our database.

That’s it! We finished our back-end. 🎉 Now, let’s use those queries/actions on our UI.

3) Let’s erase everything we had in the MainPage.js file and substitute it with our new UI.

src/client/MainPage.js | Updating the UI
import React, { useState } from 'react'
import { useQuery } from '@wasp/queries'
import getExcuse from '@wasp/queries/getExcuse'
import getAllSavedExcuses from '@wasp/queries/getAllSavedExcuses'
import saveExcuse from '@wasp/actions/saveExcuse'

const MainPage = () => {
const [currentExcuse, setCurrentExcuse] = useState({ text: "" })
const { data: excuses } = useQuery(getAllSavedExcuses)

const handleGetExcuse = async () => {
try {
setCurrentExcuse(await getExcuse())
} catch (err) {
window.alert('Error while getting the excuse: ' + err.message)
}
}

const handleSaveExcuse = async () => {
if (currentExcuse.text) {
try {
await saveExcuse(currentExcuse)
} catch (err) {
window.alert('Error while saving the excuse: ' + err.message)
}
}
}

return (
<div className="grid grid-cols-2 text-3xl">
<div>
<button onClick={handleGetExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Get excuse </button>
<button onClick={handleSaveExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Save excuse </button>
<Excuse excuse={currentExcuse} />
</div>
<div>
<div className="px-6 py-2 bg-blue-600 text-white"> Saved excuses: </div>
{excuses && <ExcuseList excuses={excuses} />}
</div>
</div>
)
}

const ExcuseList = (props) => {
return props.excuses?.length ? props.excuses.map((excuse, idx) => <Excuse excuse={excuse} key={idx} />) : 'No saved excuses'
}

const Excuse = ({ excuse }) => {
return (
<div className="px-6 py-2">
{excuse.text}
</div>
)
}

export default MainPage

Our page consists of three components. MainPage, ExcuseList and Excuse. It may seem at first that this file is pretty complex. It’s not, so let’s look a bit closer.

Excuse is just a div with an excuse text, ExcuseList checks if there are any excuses. If the list is empty – show a message No saved excuses. In other case – excuses will be displayed.

MainPage contains info about the current excuses and the list of already saved excuses. Two buttons click handlers handleGetExcuse and handleSaveExcuse. Plus, the markup itself with some Tailwind flavor.

4) Before starting an app – we need to execute database migration because we changed the DB schema by adding new entities. If you’ve had something running in the terminal – stop it and run:

wasp db migrate-dev

You’ll be prompted to enter a name for the migration. Something like init will be ok. Now we can start the application!

wasp start

Final empty result

Now you can click the “Get excuse” button to receive an excuse. And save the ones you like into the DB with the “Save excuse” button. Our final project should look like this:

Final result

It would have taken twice as long to build it properly.

Now we can think of some additional improvements. For example:

  • 1) Add a unique constraint to Entity’s ID so we won’t be able to save duplicated excuses.
  • 2) Add exceptions and edge cases handling.
  • 3) Make the markup prettier.
  • 4) Optimize and polish the code

So, we’ve been able to build a full-stack application with a database and external API call in a couple of minutes. And now we have a box full of excuses for all our development needs.

Box of excuses for the win!

]]>
+ We’ll build a web app to solve every developer's most common problem – finding an excuse to justify our messy work! And will do it with a single config file that covers the full-stack app architecture plus several dozen lines of code. In the quickest possible way, so we can’t excuse ourselves from building it!

Best excuse of all time

Best excuse of all time! Taken from here.

The requirements were unclear.

We’ll use Michele Gerarduzzi’s open-source project. It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let’s define the requirements for the project:

  • The app should be able to pull excuses data from a public API.
  • Save the ones you liked (and your boss doesn't) to the database for future reference.
  • Building an app shouldn’t take more than 15 minutes.
  • Use modern web dev technologies (NodeJS + React)

As a result – we’ll get a simple and fun pet project. You can find the complete codebase here.

Final result

There’s an issue with the third party library.

Setting up a backbone for the project is the most frustrating part of building any application.

We are installing dependencies, tying up the back-end and front-end, setting up a database, managing connection strings, and so on. Avoiding this part will save us a ton of time and effort. So let’s find ourselves an excuse to skip the initial project setup.

Ideally – use a framework that will create a project infrastructure quickly with the best defaults so that we’ll focus on the business logic. A perfect candidate is Wasp. It’s an open-source, declarative DSL for building web apps in React and Node.js with no boilerplate

How it works: developer starts from a single config file that specifies the app architecture. Routes, CRUD API, auth, and so on. Then adds React/Node.js code for the specific business logic. Behind the scenes, Wasp compiler will produce the entire source code of the app - back-end, front-end, deployment template, database migrations and everything else you’ve used to have in any other full-stack app.

Wasp architecture

So let’s jump right in.

Maybe something's wrong with the environment.

Wasp intentionally works with the LTS Node.js version since it guarantees stability and active maintenance. As for now, it’s Node 16 and NPM 8. If you need another Node version for some other project – there’s a possibility to use NVM to manage multiple Node versions on your computer at the same time.

Installing Wasp on Linux (for Mac/Windows, please check the docs):

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

Now let’s create a new web app named ItWaspsOnMyMachine.

wasp new ItWaspsOnMyMachine

Changing the working directory:

cd ItWaspsOnMyMachine

Starting the app:

wasp start

Now your default browser should open up with a simple predefined text message. That’s it! 🥳 We’ve built and run a NodeJS + React application. And for now – the codebase consists of only two files! main.wasp is the config file that defines the application’s functionality. And MainPage.js is the front-end.

Initial page

That worked perfectly when I developed it.

1) Let’s add some additional configuration to our main.wasp file. So it will look like this:

main.wasp | Defining Excuse entity, queries and action

// Main declaration, defines a new web app.
app ItWaspsOnMyMachine {
// Wasp compiler configuration
wasp: {
version: "^0.6.0"
},

// Used as a browser tab title.
title: "It Wasps On My Machine",

head: [
// Adding Tailwind to make our UI prettier
"<script src='https://cdn.tailwindcss.com'></script>"
],

dependencies: [
// Adding Axios for making HTTP requests
("axios", "^0.21.1")
]
}

// Render page MainPage on url `/` (default url).
route RootRoute { path: "/", to: MainPage }

// ReactJS implementation of our page located in `src/client/MainPage.js` as a default export.
page MainPage {
component: import Main from "@client/MainPage.js"
}

// Prisma database entity
entity Excuse {=psl
id Int @id @default(autoincrement())
text String
psl=}

// Query declaration to get a new excuse
query getExcuse {
fn: import { getExcuse } from "@server/queries.js",
entities: [Excuse]
}

// Query declaration to get all excuses
query getAllSavedExcuses {
fn: import { getAllSavedExcuses } from "@server/queries.js",
entities: [Excuse]
}

// Action to save current excuse
action saveExcuse {
fn: import { saveExcuse } from "@server/actions.js",
entities: [Excuse]
}

We’ve added Tailwind to make our UI more pretty and Axios for making API requests.

Also, we’ve declared a database entity called Excuse, queries, and action. The Excuse entity consists of the entity’s ID and the text.

Queries are here when we need to fetch/read something, while actions are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info in the docs. So let’s proceed with queries/actions.

2) Create two files: “actions.js” and “queries.js” in the src/server folder.

src/server/actions.js | Defining an action
export const saveExcuse = async (excuse, context) => {
return context.entities.Excuse.create({
data: { text: excuse.text }
})
}
src/server/queries.js | Defining queries
import axios from 'axios';

export const getExcuse = async () => {
const response = await axios.get('https://api.devexcus.es/')
return response.data
}

export const getAllSavedExcuses = async (_args, context) => {
return context.entities.Excuse.findMany()
}

Let’s add saveExcuse() action to our actions.js file. This action will save the text of our excuse to the database. Then let’s create two queries in the queries.js file. First, one getExcuse will call an external API and fetch a new excuse. The second one, named getAllSavedExcuses, will pull all the excuses we’ve saved to our database.

That’s it! We finished our back-end. 🎉 Now, let’s use those queries/actions on our UI.

3) Let’s erase everything we had in the MainPage.js file and substitute it with our new UI.

src/client/MainPage.js | Updating the UI
import React, { useState } from 'react'
import { useQuery } from '@wasp/queries'
import getExcuse from '@wasp/queries/getExcuse'
import getAllSavedExcuses from '@wasp/queries/getAllSavedExcuses'
import saveExcuse from '@wasp/actions/saveExcuse'

const MainPage = () => {
const [currentExcuse, setCurrentExcuse] = useState({ text: "" })
const { data: excuses } = useQuery(getAllSavedExcuses)

const handleGetExcuse = async () => {
try {
setCurrentExcuse(await getExcuse())
} catch (err) {
window.alert('Error while getting the excuse: ' + err.message)
}
}

const handleSaveExcuse = async () => {
if (currentExcuse.text) {
try {
await saveExcuse(currentExcuse)
} catch (err) {
window.alert('Error while saving the excuse: ' + err.message)
}
}
}

return (
<div className="grid grid-cols-2 text-3xl">
<div>
<button onClick={handleGetExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Get excuse </button>
<button onClick={handleSaveExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Save excuse </button>
<Excuse excuse={currentExcuse} />
</div>
<div>
<div className="px-6 py-2 bg-blue-600 text-white"> Saved excuses: </div>
{excuses && <ExcuseList excuses={excuses} />}
</div>
</div>
)
}

const ExcuseList = (props) => {
return props.excuses?.length ? props.excuses.map((excuse, idx) => <Excuse excuse={excuse} key={idx} />) : 'No saved excuses'
}

const Excuse = ({ excuse }) => {
return (
<div className="px-6 py-2">
{excuse.text}
</div>
)
}

export default MainPage

Our page consists of three components. MainPage, ExcuseList and Excuse. It may seem at first that this file is pretty complex. It’s not, so let’s look a bit closer.

Excuse is just a div with an excuse text, ExcuseList checks if there are any excuses. If the list is empty – show a message No saved excuses. In other case – excuses will be displayed.

MainPage contains info about the current excuses and the list of already saved excuses. Two buttons click handlers handleGetExcuse and handleSaveExcuse. Plus, the markup itself with some Tailwind flavor.

4) Before starting an app – we need to execute database migration because we changed the DB schema by adding new entities. If you’ve had something running in the terminal – stop it and run:

wasp db migrate-dev

You’ll be prompted to enter a name for the migration. Something like init will be ok. Now we can start the application!

wasp start

Final empty result

Now you can click the “Get excuse” button to receive an excuse. And save the ones you like into the DB with the “Save excuse” button. Our final project should look like this:

Final result

It would have taken twice as long to build it properly.

Now we can think of some additional improvements. For example:

  • 1) Add a unique constraint to Entity’s ID so we won’t be able to save duplicated excuses.
  • 2) Add exceptions and edge cases handling.
  • 3) Make the markup prettier.
  • 4) Optimize and polish the code

So, we’ve been able to build a full-stack application with a database and external API call in a couple of minutes. And now we have a box full of excuses for all our development needs.

Box of excuses for the win!

]]>
wasp
@@ -777,7 +777,7 @@ them done.

I still occasionally need to give this advice to myself :).

https://wasp-lang.dev/blog/2022/01/27/waspleau Thu, 27 Jan 2022 00:00:00 GMT - Hello, Waspleau

See Waspleau here! | See the code

We've built a dashboard powered by a job queue using Wasp!

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.

Measure all the metrics!

Hello, Waspleau!

Let’s face it - metrics are all around us. Wouldn’t it be great if there was a quick and easy way to build a nice-looking metrics dashboard from data pulled in by HTTP calls to many different sources, cache the data in-memory, and periodically update it via background jobs? Why yes, yes it would... so we made an example Wasp app called Waspleau that does just that!

Here is what it looks like live: https://waspleau.netlify.app/ There is also a screenshot at the top of this post for those who refrain from clicking on any unknown web links for fear of being Rickrolled. Respect.

“Show me the code”

So, what do we need to get started? First, we need a way to schedule and run jobs; for this, we decided to use Bull. Ok, let’s wire it up. This should be easy, right? We can add external NPM dependencies in our Wasp files like so:

main.wasp
app waspleau {
title: "Waspleau",

dependencies: [
("bull", "4.1.1"),
("axios", "^0.21.1")
]
}

But where do we declare our queue and processing callback functions in Wasp? Uh oh...

Sad

server.setupFn for queue setup

Thankfully, Waspleau can leverage a powerful and flexible hook supplied by Wasp called server.setupFn. This declares a JavaScript function that will be executed on server start. Yahoo! This means we can do things like the following:

main.wasp
app waspleau {
...

server: {
setupFn: import serverSetup from "@server/serverSetup.js"
}
}
src/server/serverSetup.js
import Queue from 'bull'

const queue = new Queue('waspleau', process.env.REDIS_URL || 'redis://127.0.0.1:6379',
{ defaultJobOptions: { removeOnComplete: true } }
)

queue.process('*', async (job) => { ... })

export default async () => {
// To initially populate the queue, we can do:
await queue.add({ ... }) // first run, one-off job
await queue.add({ ... }, { repeat: { cron: '*/10 * * * *' } }) // recurring job
}

Abstracting workers and job processing

Awesome, we can now enqueue and process background jobs, but how can we make it easy to create many different kinds of jobs and schedule them to run at different intervals? For Waspleau, we created our own type of worker object convention to help standardize and simplify adding more:

src/server/workers/template.js
const workerFunction = async (opts) => {
return [
{ name: 'Metric 1 name', value: 'foo', updatedAt: ... },
{ name: 'Metric 2 name', value: 'bar', updatedAt: ... },
]
}

export const workerTemplate = { name: 'Job Name', fn: workerFunction, schedule: '*/10 * * * *' }

With this workerFunction setup, we can return one or more metrics per worker type. Waspleau can easily use any module that exports this shape. Here is a real example from the demo that makes HTTP calls to GitHub’s API with Axios:

src/server/workers/github.js
import axios from 'axios'

const workerFunction = async (opts) => {
console.log('github.js workerFunction')

const now = Date.now()

try {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

return [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count, updatedAt: now },
{ name: 'Wasp GitHub Language', value: response.data.language, updatedAt: now },
{ name: 'Wasp GitHub Forks', value: response.data.forks, updatedAt: now },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues, updatedAt: now },
]
} catch (error) {
console.error(error)
return []
}
}

export const githubWorker = { name: 'GitHub API', fn: workerFunction, schedule: '*/10 * * * *' }

Note: Please see the actual serverSetup.js file for how we use this abstraction in practice.

Server → client

We now have jobs running and data updating at regular intervals, nice, but we still need a way to send that data down the wire. Here, we expose the in-memory data from our server.setupFn module so our queries can also use it:

main.wasp
...

query dashboard {
fn: import { refreshDashboardData } from "@server/dashboard.js"
}
src/server/dashboard.js
import { getDashboardData } from './serverSetup.js'

export const refreshDashboardData = async (_args, _context) => {
return getDashboardData()
}
src/server/serverSetup.js
...

const dashboardData = {} // This is updated in the queue process callback
export const getDashboardData = () => Object.values(dashboardData).flat()

From there, we can request it on the frontend in React components as usual and also set a one-minute client-side refetch interval just for good measure:

src/client/MainPage.js
...

const { data: dashboardData, isFetching, error } = useQuery(refreshDashboardData, null, { refetchInterval: 60 * 1000 })

...

Congratulations, let’s dance!

Whew, we did it! If you’d like to deploy your own customized version of this dashboard, please clone our repo and check out the Waspleau example README.md for tips on getting started. You can also check out our docs to dive deeper into anything.

Rickroll

Still got ya! :D

2022 is going to be exciting 🚀

While this functionality currently exists outside of Wasp, keep an eye on our roadmap as we head toward 1.0. We will be busy adding lots of great features to our Wasp DSL in the coming months that will supercharge your web development experience! Thanks for reading, and please feel free to connect with us in Discord about using Wasp on your next project.

]]>
+ Hello, Waspleau

See Waspleau here! | See the code

We've built a dashboard powered by a job queue using Wasp!

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.

Measure all the metrics!

Hello, Waspleau!

Let’s face it - metrics are all around us. Wouldn’t it be great if there was a quick and easy way to build a nice-looking metrics dashboard from data pulled in by HTTP calls to many different sources, cache the data in-memory, and periodically update it via background jobs? Why yes, yes it would... so we made an example Wasp app called Waspleau that does just that!

Here is what it looks like live: https://waspleau-app-client.fly.dev/ There is also a screenshot at the top of this post for those who refrain from clicking on any unknown web links for fear of being Rickrolled. Respect.

“Show me the code”

So, what do we need to get started? First, we need a way to schedule and run jobs; for this, we decided to use Bull. Ok, let’s wire it up. This should be easy, right? We can add external NPM dependencies in our Wasp files like so:

main.wasp
app waspleau {
title: "Waspleau",

dependencies: [
("bull", "4.1.1"),
("axios", "^0.21.1")
]
}

But where do we declare our queue and processing callback functions in Wasp? Uh oh...

Sad

server.setupFn for queue setup

Thankfully, Waspleau can leverage a powerful and flexible hook supplied by Wasp called server.setupFn. This declares a JavaScript function that will be executed on server start. Yahoo! This means we can do things like the following:

main.wasp
app waspleau {
...

server: {
setupFn: import serverSetup from "@server/serverSetup.js"
}
}
src/server/serverSetup.js
import Queue from 'bull'

const queue = new Queue('waspleau', process.env.REDIS_URL || 'redis://127.0.0.1:6379',
{ defaultJobOptions: { removeOnComplete: true } }
)

queue.process('*', async (job) => { ... })

export default async () => {
// To initially populate the queue, we can do:
await queue.add({ ... }) // first run, one-off job
await queue.add({ ... }, { repeat: { cron: '*/10 * * * *' } }) // recurring job
}

Abstracting workers and job processing

Awesome, we can now enqueue and process background jobs, but how can we make it easy to create many different kinds of jobs and schedule them to run at different intervals? For Waspleau, we created our own type of worker object convention to help standardize and simplify adding more:

src/server/workers/template.js
const workerFunction = async (opts) => {
return [
{ name: 'Metric 1 name', value: 'foo', updatedAt: ... },
{ name: 'Metric 2 name', value: 'bar', updatedAt: ... },
]
}

export const workerTemplate = { name: 'Job Name', fn: workerFunction, schedule: '*/10 * * * *' }

With this workerFunction setup, we can return one or more metrics per worker type. Waspleau can easily use any module that exports this shape. Here is a real example from the demo that makes HTTP calls to GitHub’s API with Axios:

src/server/workers/github.js
import axios from 'axios'

const workerFunction = async (opts) => {
console.log('github.js workerFunction')

const now = Date.now()

try {
const response = await axios.get('https://api.github.com/repos/wasp-lang/wasp')

return [
{ name: 'Wasp GitHub Stars', value: response.data.stargazers_count, updatedAt: now },
{ name: 'Wasp GitHub Language', value: response.data.language, updatedAt: now },
{ name: 'Wasp GitHub Forks', value: response.data.forks, updatedAt: now },
{ name: 'Wasp GitHub Open Issues', value: response.data.open_issues, updatedAt: now },
]
} catch (error) {
console.error(error)
return []
}
}

export const githubWorker = { name: 'GitHub API', fn: workerFunction, schedule: '*/10 * * * *' }

Note: Please see the actual serverSetup.js file for how we use this abstraction in practice.

Server → client

We now have jobs running and data updating at regular intervals, nice, but we still need a way to send that data down the wire. Here, we expose the in-memory data from our server.setupFn module so our queries can also use it:

main.wasp
...

query dashboard {
fn: import { refreshDashboardData } from "@server/dashboard.js"
}
src/server/dashboard.js
import { getDashboardData } from './serverSetup.js'

export const refreshDashboardData = async (_args, _context) => {
return getDashboardData()
}
src/server/serverSetup.js
...

const dashboardData = {} // This is updated in the queue process callback
export const getDashboardData = () => Object.values(dashboardData).flat()

From there, we can request it on the frontend in React components as usual and also set a one-minute client-side refetch interval just for good measure:

src/client/MainPage.js
...

const { data: dashboardData, isFetching, error } = useQuery(refreshDashboardData, null, { refetchInterval: 60 * 1000 })

...

Congratulations, let’s dance!

Whew, we did it! If you’d like to deploy your own customized version of this dashboard, please clone our repo and check out the Waspleau example README.md for tips on getting started. You can also check out our docs to dive deeper into anything.

Rickroll

Still got ya! :D

2022 is going to be exciting 🚀

While this functionality currently exists outside of Wasp, keep an eye on our roadmap as we head toward 1.0. We will be busy adding lots of great features to our Wasp DSL in the coming months that will supercharge your web development experience! Thanks for reading, and please feel free to connect with us in Discord about using Wasp on your next project.

]]>
webdev wasp
diff --git a/blog/tags.html b/blog/tags.html index ef42f7a1b3..6aca82d072 100644 --- a/blog/tags.html +++ b/blog/tags.html @@ -19,13 +19,13 @@ - - + +
-

Tags

- - +

Tags

+ + \ No newline at end of file diff --git a/blog/tags/agent.html b/blog/tags/agent.html index 789873ab07..984d5ab474 100644 --- a/blog/tags/agent.html +++ b/blog/tags/agent.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "agent"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

- - +

2 posts tagged with "agent"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/ai.html b/blog/tags/ai.html index 0192e91896..b0bb412b1b 100644 --- a/blog/tags/ai.html +++ b/blog/tags/ai.html @@ -19,13 +19,13 @@ - - + +
-

7 posts tagged with "ai"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

- - +

7 posts tagged with "ai"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/auth.html b/blog/tags/auth.html index 29975db2e5..b45993384c 100644 --- a/blog/tags/auth.html +++ b/blog/tags/auth.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "auth"

View All Tags
By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more
- - +

One post tagged with "auth"

View All Tags
By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more
+ + \ No newline at end of file diff --git a/blog/tags/career.html b/blog/tags/career.html index 12de5b2aaa..2fd55ccac5 100644 --- a/blog/tags/career.html +++ b/blog/tags/career.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "career"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
- - +

One post tagged with "career"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
+ + \ No newline at end of file diff --git a/blog/tags/chakra.html b/blog/tags/chakra.html index 181dbb8ba0..f9ac5a5a7e 100644 --- a/blog/tags/chakra.html +++ b/blog/tags/chakra.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "chakra"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

- - +

2 posts tagged with "chakra"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/chatgpt.html b/blog/tags/chatgpt.html index dc19a3914f..c829d529d2 100644 --- a/blog/tags/chatgpt.html +++ b/blog/tags/chatgpt.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "chatgpt"

View All Tags
By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more
- - +

One post tagged with "chatgpt"

View All Tags
By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more
+ + \ No newline at end of file diff --git a/blog/tags/clean-code.html b/blog/tags/clean-code.html index 73fc9c6719..a43aebac36 100644 --- a/blog/tags/clean-code.html +++ b/blog/tags/clean-code.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "clean-code"

View All Tags
By Martin Sosic
12 min read

On Importance of Naming in Programming

Read more
- - +

One post tagged with "clean-code"

View All Tags
By Martin Sosic
12 min read

On the Importance of Naming in Programming

Read more
+ + \ No newline at end of file diff --git a/blog/tags/css.html b/blog/tags/css.html index 579b433549..f03b0446d6 100644 --- a/blog/tags/css.html +++ b/blog/tags/css.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "css"

View All Tags
By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more
- - +

One post tagged with "css"

View All Tags
By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more
+ + \ No newline at end of file diff --git a/blog/tags/database.html b/blog/tags/database.html index bc0ddfadbe..9d4f6e1409 100644 --- a/blog/tags/database.html +++ b/blog/tags/database.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "database"

View All Tags
By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more
- - +

One post tagged with "database"

View All Tags
By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more
+ + \ No newline at end of file diff --git a/blog/tags/discord.html b/blog/tags/discord.html index f3a93d457f..f9b70db0a9 100644 --- a/blog/tags/discord.html +++ b/blog/tags/discord.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "discord"

View All Tags
By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more
- - +

One post tagged with "discord"

View All Tags
By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more
+ + \ No newline at end of file diff --git a/blog/tags/express.html b/blog/tags/express.html index 6f2313e13e..f55a3d0f4d 100644 --- a/blog/tags/express.html +++ b/blog/tags/express.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "express"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
- - +

One post tagged with "express"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
+ + \ No newline at end of file diff --git a/blog/tags/feature.html b/blog/tags/feature.html index 8b75edd56e..1c6306a478 100644 --- a/blog/tags/feature.html +++ b/blog/tags/feature.html @@ -19,13 +19,13 @@ - - + +
-

5 posts tagged with "feature"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

- - +

5 posts tagged with "feature"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/framework.html b/blog/tags/framework.html index aa3e10cb7f..fa5d690aaf 100644 --- a/blog/tags/framework.html +++ b/blog/tags/framework.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "framework"

View All Tags
By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more
- - +

One post tagged with "framework"

View All Tags
By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more
+ + \ No newline at end of file diff --git a/blog/tags/full-stack.html b/blog/tags/full-stack.html index a3fb5c5094..4c3bc670d7 100644 --- a/blog/tags/full-stack.html +++ b/blog/tags/full-stack.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "full-stack"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

- - +

3 posts tagged with "full-stack"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/fullstack.html b/blog/tags/fullstack.html index 8f3a3bad80..19778546bb 100644 --- a/blog/tags/fullstack.html +++ b/blog/tags/fullstack.html @@ -19,13 +19,13 @@ - - + +
-

7 posts tagged with "fullstack"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
6 min read

Hackathon #2: Results & Review

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

- - +

7 posts tagged with "fullstack"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
6 min read

Hackathon #2: Results & Review

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/function-calling.html b/blog/tags/function-calling.html index 074e363a1c..2ba17846b2 100644 --- a/blog/tags/function-calling.html +++ b/blog/tags/function-calling.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "function calling"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
- - +

One post tagged with "function calling"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
+ + \ No newline at end of file diff --git a/blog/tags/generate.html b/blog/tags/generate.html index 369da3fb27..b4d8b5d0f6 100644 --- a/blog/tags/generate.html +++ b/blog/tags/generate.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "generate"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

- - +

2 posts tagged with "generate"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/github.html b/blog/tags/github.html index 57d49febe8..586c1b2f87 100644 --- a/blog/tags/github.html +++ b/blog/tags/github.html @@ -19,13 +19,13 @@ - - + +
-

13 posts tagged with "github"

View All Tags
By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more
By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

- - +

13 posts tagged with "github"

View All Tags
By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more
By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/gitpod.html b/blog/tags/gitpod.html index fa7416918a..0c9b7f147e 100644 --- a/blog/tags/gitpod.html +++ b/blog/tags/gitpod.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "gitpod"

View All Tags
By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more
- - +

One post tagged with "gitpod"

View All Tags
By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more
+ + \ No newline at end of file diff --git a/blog/tags/gpt.html b/blog/tags/gpt.html index 5f1705cc0e..e2c3f81832 100644 --- a/blog/tags/gpt.html +++ b/blog/tags/gpt.html @@ -19,13 +19,13 @@ - - + +
-

6 posts tagged with "gpt"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more →

By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

- - +

6 posts tagged with "gpt"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more →

By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/hack.html b/blog/tags/hack.html index 18dc548256..1327ca5160 100644 --- a/blog/tags/hack.html +++ b/blog/tags/hack.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "hack"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
- - +

One post tagged with "hack"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
+ + \ No newline at end of file diff --git a/blog/tags/hackathon.html b/blog/tags/hackathon.html index 2ffeb88c8c..b70e431934 100644 --- a/blog/tags/hackathon.html +++ b/blog/tags/hackathon.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "hackathon"

View All Tags
By Vinny
6 min read

Hackathon #2: Results & Review

Read more
By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

- - +

3 posts tagged with "hackathon"

View All Tags
By Vinny
6 min read

Hackathon #2: Results & Review

Read more
By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/hacktoberfest.html b/blog/tags/hacktoberfest.html index 9ea3ca1bc5..835c14a1fe 100644 --- a/blog/tags/hacktoberfest.html +++ b/blog/tags/hacktoberfest.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "hacktoberfest"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

- - +

2 posts tagged with "hacktoberfest"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/haskell.html b/blog/tags/haskell.html index 9ccdb2171e..d2b5978a4f 100644 --- a/blog/tags/haskell.html +++ b/blog/tags/haskell.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "haskell"

View All Tags
By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more
By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more →

- - +

3 posts tagged with "haskell"

View All Tags
By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more
By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/hiring.html b/blog/tags/hiring.html index 7caa08ef58..da45f9209d 100644 --- a/blog/tags/hiring.html +++ b/blog/tags/hiring.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "hiring"

View All Tags
By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more
- - +

One post tagged with "hiring"

View All Tags
By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more
+ + \ No newline at end of file diff --git a/blog/tags/indie-hacker.html b/blog/tags/indie-hacker.html index 0cd09a0ab6..d74a89b0b2 100644 --- a/blog/tags/indie-hacker.html +++ b/blog/tags/indie-hacker.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "IndieHacker"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
- - +

One post tagged with "IndieHacker"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
+ + \ No newline at end of file diff --git a/blog/tags/interview.html b/blog/tags/interview.html index 2d0b8a45e9..78b9175929 100644 --- a/blog/tags/interview.html +++ b/blog/tags/interview.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "Interview"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
- - +

One post tagged with "Interview"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
+ + \ No newline at end of file diff --git a/blog/tags/javascript.html b/blog/tags/javascript.html index 9d1e038db5..facb9c73d3 100644 --- a/blog/tags/javascript.html +++ b/blog/tags/javascript.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "javascript"

View All Tags
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more
- - +

One post tagged with "javascript"

View All Tags
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more
+ + \ No newline at end of file diff --git a/blog/tags/jobs.html b/blog/tags/jobs.html index 35c2df9d30..80dd8e9187 100644 --- a/blog/tags/jobs.html +++ b/blog/tags/jobs.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "jobs"

View All Tags
By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more
- - +

One post tagged with "jobs"

View All Tags
By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more
+ + \ No newline at end of file diff --git a/blog/tags/junior-developers.html b/blog/tags/junior-developers.html index 2f9c36b87d..7212cd1c71 100644 --- a/blog/tags/junior-developers.html +++ b/blog/tags/junior-developers.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "Junior Developers"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

- - +

2 posts tagged with "Junior Developers"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/langchain.html b/blog/tags/langchain.html index 49ebcd594a..08c2651bc8 100644 --- a/blog/tags/langchain.html +++ b/blog/tags/langchain.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "langchain"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

- - +

2 posts tagged with "langchain"

View All Tags
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more
By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/language.html b/blog/tags/language.html index a8bb3a7fc7..a79d57bfc7 100644 --- a/blog/tags/language.html +++ b/blog/tags/language.html @@ -19,13 +19,13 @@ - - + +
-

5 posts tagged with "language"

View All Tags
By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more
By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

- - +

5 posts tagged with "language"

View All Tags
By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more
By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/launch-week.html b/blog/tags/launch-week.html index 726bbd9084..11cca78b19 100644 --- a/blog/tags/launch-week.html +++ b/blog/tags/launch-week.html @@ -19,13 +19,13 @@ - - + +
-

5 posts tagged with "launch-week"

View All Tags
By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more
By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more →

By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

By Matija Sosic
4 min read

What can you build with Wasp?

Read more →

By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

- - +

5 posts tagged with "launch-week"

View All Tags
By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more
By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more →

By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

By Matija Sosic
4 min read

What can you build with Wasp?

Read more →

By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/meme.html b/blog/tags/meme.html index e960c369fd..dcb1432083 100644 --- a/blog/tags/meme.html +++ b/blog/tags/meme.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "meme"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
- - +

One post tagged with "meme"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
+ + \ No newline at end of file diff --git a/blog/tags/ml.html b/blog/tags/ml.html index 45ba982def..9b2c33642e 100644 --- a/blog/tags/ml.html +++ b/blog/tags/ml.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "ML"

View All Tags
By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more
- - +

One post tagged with "ML"

View All Tags
By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more
+ + \ No newline at end of file diff --git a/blog/tags/new-hire.html b/blog/tags/new-hire.html index 2f4bbb2bac..f92ff544d1 100644 --- a/blog/tags/new-hire.html +++ b/blog/tags/new-hire.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "new-hire"

View All Tags
By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more
By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

- - +

2 posts tagged with "new-hire"

View All Tags
By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more
By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/node.html b/blog/tags/node.html index 3046009800..4335773357 100644 --- a/blog/tags/node.html +++ b/blog/tags/node.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "node"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

- - +

3 posts tagged with "node"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/nodejs.html b/blog/tags/nodejs.html index 127d3a0844..b220cb1927 100644 --- a/blog/tags/nodejs.html +++ b/blog/tags/nodejs.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "nodejs"

View All Tags
By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more
- - +

One post tagged with "nodejs"

View All Tags
By Martin Sosic
9 min read

How to implement a Discord bot (in NodeJS) that requires new members to introduce themselves

Read more
+ + \ No newline at end of file diff --git a/blog/tags/open-source.html b/blog/tags/open-source.html index 0f94d17e37..ff1a98c8a9 100644 --- a/blog/tags/open-source.html +++ b/blog/tags/open-source.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "open-source"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
- - +

One post tagged with "open-source"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
+ + \ No newline at end of file diff --git a/blog/tags/openai.html b/blog/tags/openai.html index 40054c87f1..dd930002e8 100644 --- a/blog/tags/openai.html +++ b/blog/tags/openai.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "openai"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
- - +

One post tagged with "openai"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
+ + \ No newline at end of file diff --git a/blog/tags/optimistic.html b/blog/tags/optimistic.html index e6488ff66c..aa624708a8 100644 --- a/blog/tags/optimistic.html +++ b/blog/tags/optimistic.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "optimistic"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
- - +

One post tagged with "optimistic"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
+ + \ No newline at end of file diff --git a/blog/tags/pern.html b/blog/tags/pern.html index 0ae14c0692..08720dcae8 100644 --- a/blog/tags/pern.html +++ b/blog/tags/pern.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "PERN"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

- - +

2 posts tagged with "PERN"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/prd.html b/blog/tags/prd.html index fe8c691dc1..f5bc9942c3 100644 --- a/blog/tags/prd.html +++ b/blog/tags/prd.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "prd"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
- - +

One post tagged with "prd"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
+ + \ No newline at end of file diff --git a/blog/tags/prisma.html b/blog/tags/prisma.html index c40098be61..1ae4099c42 100644 --- a/blog/tags/prisma.html +++ b/blog/tags/prisma.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "prisma"

View All Tags
By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more
By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

- - +

2 posts tagged with "prisma"

View All Tags
By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more
By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/product-requirement.html b/blog/tags/product-requirement.html index 46ce6d55b1..1bb9bbd0d4 100644 --- a/blog/tags/product-requirement.html +++ b/blog/tags/product-requirement.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "product requirement"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
- - +

One post tagged with "product requirement"

View All Tags
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more
+ + \ No newline at end of file diff --git a/blog/tags/product-update.html b/blog/tags/product-update.html index 81acad8edc..a55d068dcf 100644 --- a/blog/tags/product-update.html +++ b/blog/tags/product-update.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "product-update"

View All Tags
By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more
By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

- - +

2 posts tagged with "product-update"

View All Tags
By Vinny
4 min read

Tutorial Jam #1 - Teach Others & Win Prizes!

Read more
By Matija Sosic
2 min read

Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/programming.html b/blog/tags/programming.html index 018a59c61c..026495db6d 100644 --- a/blog/tags/programming.html +++ b/blog/tags/programming.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "programming"

View All Tags
By Martin Sosic
12 min read

On Importance of Naming in Programming

Read more
- - +

One post tagged with "programming"

View All Tags
By Martin Sosic
12 min read

On the Importance of Naming in Programming

Read more
+ + \ No newline at end of file diff --git a/blog/tags/react.html b/blog/tags/react.html index f8b36cd171..7b8a5f740e 100644 --- a/blog/tags/react.html +++ b/blog/tags/react.html @@ -19,13 +19,13 @@ - - + +
-

7 posts tagged with "react"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

- - +

7 posts tagged with "react"

View All Tags
By Vinny
31 min read

Build your own AI Meme Generator & learn how to use OpenAI's function calls

Read more
By Vinny
9 min read

Using Product Requirement Documents to Generate Better Web Apps with AI

Read more →

By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more →

By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/real-time.html b/blog/tags/real-time.html index 3dbd98ab17..0c6470169d 100644 --- a/blog/tags/real-time.html +++ b/blog/tags/real-time.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "real-time"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
- - +

One post tagged with "real-time"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
+ + \ No newline at end of file diff --git a/blog/tags/reddit.html b/blog/tags/reddit.html index 1ede37ef93..163fca8281 100644 --- a/blog/tags/reddit.html +++ b/blog/tags/reddit.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "Reddit"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

- - +

2 posts tagged with "Reddit"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/saa-s.html b/blog/tags/saa-s.html index f5ff3a4436..4ff9b643df 100644 --- a/blog/tags/saa-s.html +++ b/blog/tags/saa-s.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "SaaS"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
- - +

One post tagged with "SaaS"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
+ + \ No newline at end of file diff --git a/blog/tags/saas.html b/blog/tags/saas.html index 69d573ee5a..4e354db747 100644 --- a/blog/tags/saas.html +++ b/blog/tags/saas.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "saas"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

- - +

2 posts tagged with "saas"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/showcase.html b/blog/tags/showcase.html index 1065e3579f..b1ff36ce49 100644 --- a/blog/tags/showcase.html +++ b/blog/tags/showcase.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "showcase"

View All Tags
By Matija Sosic
4 min read

What can you build with Wasp?

Read more
- - +

One post tagged with "showcase"

View All Tags
By Matija Sosic
4 min read

What can you build with Wasp?

Read more
+ + \ No newline at end of file diff --git a/blog/tags/solopreneur.html b/blog/tags/solopreneur.html index 0afd20976d..2b14498a58 100644 --- a/blog/tags/solopreneur.html +++ b/blog/tags/solopreneur.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "Solopreneur"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
- - +

One post tagged with "Solopreneur"

View All Tags
By Vinny
8 min read

From Idea to Paying Customers in 1 Week: An Interview with Amicus.work

Read more
+ + \ No newline at end of file diff --git a/blog/tags/startup.html b/blog/tags/startup.html index adf520f860..8e1cbdc5c2 100644 --- a/blog/tags/startup.html +++ b/blog/tags/startup.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "startup"

View All Tags
By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more
By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

- - +

3 posts tagged with "startup"

View All Tags
By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more
By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/startups.html b/blog/tags/startups.html index d07b75d714..4c8fdac724 100644 --- a/blog/tags/startups.html +++ b/blog/tags/startups.html @@ -19,13 +19,13 @@ - - + +
-

15 posts tagged with "startups"

View All Tags
By Vinny
6 min read

Hackathon #2: Results & Review

Read more
By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

- - +

15 posts tagged with "startups"

View All Tags
By Vinny
6 min read

Hackathon #2: Results & Review

Read more
By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/state-of-js.html b/blog/tags/state-of-js.html index bdd09d4b28..83a56187b1 100644 --- a/blog/tags/state-of-js.html +++ b/blog/tags/state-of-js.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "StateOfJS"

View All Tags
By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more
- - +

One post tagged with "StateOfJS"

View All Tags
By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more
+ + \ No newline at end of file diff --git a/blog/tags/stripe.html b/blog/tags/stripe.html index 17dca88761..a693101e29 100644 --- a/blog/tags/stripe.html +++ b/blog/tags/stripe.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "stripe"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

- - +

2 posts tagged with "stripe"

View All Tags
By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more
By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/supabase.html b/blog/tags/supabase.html index e0015b1ee2..0ac5bd9113 100644 --- a/blog/tags/supabase.html +++ b/blog/tags/supabase.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "Supabase"

View All Tags
By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more
- - +

One post tagged with "Supabase"

View All Tags
By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more
+ + \ No newline at end of file diff --git a/blog/tags/tech-career.html b/blog/tags/tech-career.html index 1b7995df92..b9c8ea14f6 100644 --- a/blog/tags/tech-career.html +++ b/blog/tags/tech-career.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "Tech Career"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

- - +

2 posts tagged with "Tech Career"

View All Tags
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more
By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/tutorial.html b/blog/tags/tutorial.html index d3b71ccdc1..ee518d51cc 100644 --- a/blog/tags/tutorial.html +++ b/blog/tags/tutorial.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "tutorial"

View All Tags
By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more
- - +

One post tagged with "tutorial"

View All Tags
By Martin Sosic
9 min read

Tutorial: `forall` in Haskell

Read more
+ + \ No newline at end of file diff --git a/blog/tags/typescript.html b/blog/tags/typescript.html index 3d7c6efdb6..2e7faaee6a 100644 --- a/blog/tags/typescript.html +++ b/blog/tags/typescript.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "typescript"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

- - +

2 posts tagged with "typescript"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/update.html b/blog/tags/update.html index 2008be1c20..68334f7786 100644 --- a/blog/tags/update.html +++ b/blog/tags/update.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "update"

View All Tags
By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more
By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

By Matija Sosic
6 min read

Wasp Beta - May 2023

Read more →

- - +

3 posts tagged with "update"

View All Tags
By Matija Sosic
5 min read

Wasp Launch Week #4: Waspolution

Read more
By Matija Sosic
6 min read

Wasp Launch Week #3: Magic

Read more →

By Matija Sosic
6 min read

Wasp Beta - May 2023

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/updates.html b/blog/tags/updates.html index 3eb6c36118..b9cac2eacc 100644 --- a/blog/tags/updates.html +++ b/blog/tags/updates.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "updates"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
- - +

One post tagged with "updates"

View All Tags
By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more
+ + \ No newline at end of file diff --git a/blog/tags/wasp-ai.html b/blog/tags/wasp-ai.html index be55a6a3fa..83d77a0dce 100644 --- a/blog/tags/wasp-ai.html +++ b/blog/tags/wasp-ai.html @@ -19,13 +19,13 @@ - - + +
-

2 posts tagged with "wasp-ai"

View All Tags
By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more
By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

- - +

2 posts tagged with "wasp-ai"

View All Tags
By Martin Sosic
23 min read

How we built a GPT code agent that generates full-stack web apps in React & Node.js, explained simply

Read more
By Martin Sosic
6 min read

GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/wasp.html b/blog/tags/wasp.html index de810b6a13..6c41b1f8db 100644 --- a/blog/tags/wasp.html +++ b/blog/tags/wasp.html @@ -19,13 +19,13 @@ - - + +
-

42 posts tagged with "wasp"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Maksym Khamrovskyi
8 min read

Building an app to find an excuse for our sloppy work

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more →

By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
7 min read

Wasp - language for developing full-stack Javascript web apps with no boilerplate

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

By Martin Sosic
6 min read

Hello Wasp!

Read more →

- - +

42 posts tagged with "wasp"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
By Vinny
27 min read

Smol AI 🐣 vs. Wasp AI 🐝 - Which is the Better AI Junior Developer?

Read more →

By Vinny
46 min read

Build Your Own Personal Twitter Agent 🧠🐦⛓ with LangChain

Read more →

By Vinny
2 min read

Wasp Hackathon #2 - Let's "hack-a-ton"!

Read more →

By Vinny
3 min read

How I Built CoverLetterGPT - SaaS app with the PERN stack, GPT, Stripe, & Chakra UI

Read more →

By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
3 min read

Watch us build a *truly* full-stack app in just 9 minutes w/ Wasp & ChatGPT 🚀 🤯

Read more →

By Martin Sosic
6 min read

Wasp Beta brings major IDE improvements

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Maksym Khamrovskyi
8 min read

Building an app to find an excuse for our sloppy work

Read more →

By Vasili Shynkarenka
31 min read

How to communicate why your startup is worth joining

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Maksym Khamrovskyi
4 min read

How to win a hackathon. Brief manual.

Read more →

By Matija Sosic
6 min read

Meet the team - Filip Sodić, Founding Engineer

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
4 min read

Meet the team - Shayne Czyzewski, Founding Engineer

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

By Matija Sosic
8 min read

Our fundraising learnings - 250+ meetings in 98 days to the oversubscribed round

Read more →

By Matija Sosic
5 min read

Following YC, Wasp raised $1.5M Seed Round led by Lunar Ventures and HV Capital

Read more →

By Martin Sosic
7 min read

Wasp - language for developing full-stack Javascript web apps with no boilerplate

Read more →

By Martin Sosic
4 min read

Journey to YCombinator

Read more →

By Martin Sosic
6 min read

Hello Wasp!

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/web-dev.html b/blog/tags/web-dev.html index f3d20daae1..98b0883b62 100644 --- a/blog/tags/web-dev.html +++ b/blog/tags/web-dev.html @@ -19,13 +19,13 @@ - - + +
-

3 posts tagged with "WebDev"

View All Tags
By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more →

By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

- - +

3 posts tagged with "WebDev"

View All Tags
By Mihovil Ilakovac
14 min read

Building a full-stack app for learning Italian: Supabase vs. Wasp

Read more
By Vinny
4 min read

10 "Hard Truths" All Junior Developers Need to Hear

Read more →

By Vinny
6 min read

The Most Common Misconceptions Amongst Junior Developers

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/web-development.html b/blog/tags/web-development.html index 74597715d1..8bb17ad63c 100644 --- a/blog/tags/web-development.html +++ b/blog/tags/web-development.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "web-development"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
- - +

One post tagged with "web-development"

View All Tags
By Vinny
10 min read

Contributing to Tech Communities: How Open-Source can land you a job and get you out of the Skill Paradox

Read more
+ + \ No newline at end of file diff --git a/blog/tags/webdev.html b/blog/tags/webdev.html index d0b32e117e..d8a1824495 100644 --- a/blog/tags/webdev.html +++ b/blog/tags/webdev.html @@ -19,13 +19,13 @@ - - + +
-

30 posts tagged with "webdev"

View All Tags
By Vinny
6 min read

Hackathon #2: Results & Review

Read more
By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

- - +

30 posts tagged with "webdev"

View All Tags
By Vinny
6 min read

Hackathon #2: Results & Review

Read more
By Martin Sosic
6 min read

Wasp steps up its database game with Fully Managed Dev DB & DB Seeding

Read more →

By Matija Sosic
2 min read

Wasp Auth UI: The first full-stack auth with self-updating forms!

Read more →

By Matija Sosic
7 min read

Wasp Launch Week #2

Read more →

By Matija Sosic
5 min read

New React docs pretend SPAs don't exist anymore

Read more →

By Matija Sosic
6 min read

Wasp Beta - February 2023

Read more →

By Vinny
3 min read

The Best Web App Framework Doesn't Exist

Read more →

By Matija Sosic
7 min read

Convincing developers to try a new web framework - the effects of launching beta

Read more →

By Matija Sosic
6 min read

Wasp Beta December 2022

Read more →

By Vinny
6 min read

Hosting Our First Hackathon: Results & Review

Read more →

By Filip Sodić
7 min read

Feature Release Announcement - Wasp Optimistic Updates

Read more →

By Martin Sosic
19 min read

Permissions (access control) in web apps

Read more →

By Filip Sodić
8 min read

Feature Announcement - TypeScript Support

Read more →

By Matija Sosic
3 min read

Wasp is in Beta: Auth, TypeScript, Tailwind, LSP

Read more →

By Martin Sosic
7 min read

Why we chose Prisma as a database layer for Wasp

Read more →

By Matija Sosic
5 min read

Amicus: See how Erlis built a SaaS for legal teams with Wasp and got first paying customers!

Read more →

By Matija Sosic
5 min read

How Michael Curry chose Wasp to build Grabbit: an internal tool for managing dev resources at StudentBeans

Read more →

By Matija Sosic
5 min read

Wasp Beta Launch Week announcement

Read more →

By Maksym Khamrovskyi
6 min read

How Wasp reached all-time high PR count during Hacktoberfest: tips for OSS maintainers

Read more →

By Matija Sosic
7 min read

Alpha Testing Program: post-mortem

Read more →

By Shayne Czyzewski
3 min read

Feature Announcement - Tailwind CSS support

Read more →

By Shayne Czyzewski
4 min read

Feature Announcement - New auth method (Google)

Read more →

By Matija Sosic
4 min read

Farnance: How Julian built a SaaS for farmers with Wasp and won a hackathon!

Read more →

By Matija Sosic
12 min read

How Wasp reached 1,000 stars on GitHub (detailed stats & timeline)

Read more →

By Martin Sosic
7 min read

How to get started with Haskell in 2022 (the straightforward way)

Read more →

By Shayne Czyzewski
8 min read

How and why I got started with Haskell

Read more →

By Matija Sosic
11 min read

ML code generation vs. coding by hand - what we think programming is going to look like

Read more →

By Shayne Czyzewski
7 min read

Feature Announcement - Wasp Jobs

Read more →

By Shayne Czyzewski
5 min read

Build a metrics dashboard with background jobs in Wasp - Say hello to Waspleau!

Read more →

By Matija Sosic
10 min read

How we built a Trello clone with Wasp - Waspello!

Read more →

+ + \ No newline at end of file diff --git a/blog/tags/websockets.html b/blog/tags/websockets.html index dc030bbd6a..1ce4886956 100644 --- a/blog/tags/websockets.html +++ b/blog/tags/websockets.html @@ -19,13 +19,13 @@ - - + +
-

One post tagged with "websockets"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
- - +

One post tagged with "websockets"

View All Tags
By Vinny
22 min read

Build a real-time voting app with WebSockets, React & TypeScript 🔌⚡️

Read more
+ + \ No newline at end of file diff --git a/docs.html b/docs.html index 2f0b44b1f7..ca847e5f7d 100644 --- a/docs.html +++ b/docs.html @@ -19,18 +19,18 @@ - - + +
-

Introduction

note

If you are looking for the installation instructions, check out the Quick Start section.

We will give a brief overview of what Wasp is, how it works on a high level and when to use it.

Wasp is a tool to build modern web applications

It is an opinionated way of building full-stack web applications. It takes care of all three +

Introduction

note

If you are looking for the installation instructions, check out the Quick Start section.

We will give a brief overview of what Wasp is, how it works on a high level and when to use it.

Wasp is a tool to build modern web applications

It is an opinionated way of building full-stack web applications. It takes care of all three major parts of a web application: client (front-end), server (back-end) and database.

Works well with your existing stack

Wasp is not trying to do everything at once but rather focuses on the complexity which arises from connecting all the parts of the stack (client, server, database, deployment) together.

Wasp is using React, Node.js and Prisma under the hood and relies on them to define web components and server queries and actions.

Wasp's secret sauce

At the core is the Wasp compiler which takes the Wasp config and your Javascript code and outputs the client app, server app and deployment code.

How the magic happens 🌈

The cool thing about having a compiler that understands your code is that it can do a lot of things for you.

Define your app in the Wasp config and get:

  • login and signup with Auth UI components,
  • full-stack type safety,
  • e-mail sending,
  • async processing jobs,
  • React Query powered data fetching,
  • security best practices,
  • and more.

You don't need to write any code for these features, Wasp will take care of it for you 🤯 And what's even better, Wasp also maintains the code for you, so you don't have to worry about keeping up with the latest security best practices. As Wasp updates, so does your app.

So what does the code look like?

Let's say you want to build a web app that allows users to create and share their favorite recipes.

Let's start with the main.wasp file: it is the central file of your app, where you describe the app from the high level.

Let's give our app a title and let's immediatelly turn on the full-stack authentication via username and password:

main.wasp
app RecipeApp {
title: "My Recipes",
wasp: { version: "^0.11.0" },
auth: {
methods: { usernameAndPassword: {} },
onAuthFailedRedirectTo: "/login",
userEntity: User
}
}

Let's then add the data models for your recipes. We will want to have Users and Users can own Recipes:

main.wasp
...

entity User {=psl // Data models are defined using Prisma Schema Language.
id Int @id @default(autoincrement())
username String @unique
password String
recipes Recipe[]
psl=}

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

Next, let's define how to do something with these data models!

We do that by defining Operations, in this case a Query getRecipes and Action addRecipe, which are in their essence a Node.js functions that execute on server and can, thanks to Wasp, very easily be called from the client.

First, we define these Operations in our main.wasp file, so Wasp knows about them and can "beef them up":

main.wasp
// Queries have automatic cache invalidation and are type-safe.
query getRecipes {
fn: import { getRecipes } from "@server/recipe.js",
entities: [Recipe],
}

// Actions are type-safe and can be used to perform side-effects.
action addRecipe {
fn: import { addRecipe } from "@server/recipe.js",
entities: [Recipe],
}

... and then implement them in our Javascript (or TypeScript) code (we show just the query here, using TypeScript):

src/server/recipe.ts
// Wasp generates types for you.
import type { GetRecipes } from "@wasp/queries/types";
import type { Recipe } from "@wasp/entities";

export const getRecipes: GetRecipes<{}, Recipe[]> = async (_args, context) => {
return context.entities.Recipe.findMany( // Prisma query
{ where: { user: { id: context.user.id } } }
);
};

export const addRecipe ...

Now we can very easily use these in our React components!

For the end, let's create a home page of our app.

First we define it in main.wasp:

main.wasp
...

route HomeRoute { path: "/", to: HomePage }
page HomePage {
component: import { HomePage } from "@client/pages/HomePage",
authRequired: true // Will send user to /login if not authenticated.
}

and then implement it as a React component in JS/TS (that calls the Operations we previously defined):

src/client/pages/HomePage.tsx
import getRecipes from "@wasp/queries/getRecipes";
import { useQuery } from "@wasp/queries";
import type { User } from "@wasp/entities";

export function HomePage({ user }: { user: User }) {
// Due to full-stack type safety, `recipes` will be of type `Recipe[]` here.
const { data: recipes, isLoading } = useQuery(getRecipes); // Calling our query here!

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

return (
<div>
<h1>Recipes</h1>
<ul>
{recipes ? recipes.map((recipe) => (
<li key={recipe.id}>
<div>{recipe.title}</div>
<div>{recipe.description}</div>
</li>
)) : 'No recipes defined yet!'}
</ul>
</div>
);
}

And voila! We are listing all the recipes in our app 🎉

This was just a quick example to give you a taste of what Wasp is. For step by step tour through the most important Wasp features, check out the Todo app tutorial.

note

Above we skipped defining /login and /signup pages to keep the example a bit shorter, but those are very simple to do by using Wasp's Auth UI feature.

When to use Wasp

Wasp is addressing the same core problems that typical web app frameworks are addressing, and it in big part looks, swims and quacks like a web app framework.

Best used for

  • building full-stack web apps (like e.g. Airbnb or Asana)
  • quickly starting a web app with industry best practices
  • to be used alongside modern web dev stack (currently supported React and Node)

Avoid using Wasp for

  • building static/presentational websites
  • to be used as a no-code solution
  • to be a solve-it-all tool in a single language

Wasp is a DSL

note

You don't need to know what a DSL is to use Wasp, but if you are curious, you can read more about it below.

Wasp does not match typical expectations of a web app framework: it is not a set of libraries, it is instead a programming language that understands your code and can do a lot of things for you.

Wasp is a programming language, but a specific kind: it is specialized for a single purpose: building modern web applications. We call such languages DSLs (Domain Specific Language).

Other examples of DSLs that are often used today are e.g. SQL for databases and HTML for web page layouts. The main advantage and reason why DSLs exist is that they need to do only one task (e.g. database queries) so they can do it well and provide the best possible experience for the developer.

The same idea stands behind Wasp - a language that will allow developers to build modern web applications with 10x less code and less stack-specific knowledge.

- - + + \ No newline at end of file diff --git a/docs/advanced/apis.html b/docs/advanced/apis.html index 9bc4e8fc95..dd22a708f9 100644 --- a/docs/advanced/apis.html +++ b/docs/advanced/apis.html @@ -19,14 +19,14 @@ - - + +
-

Custom HTTP API Endpoints

In Wasp, the default client-server interaction mechanism is through Operations. However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an api. Best of all, they should look and feel very familiar.

How to Create an API

APIs are used to tie a JS function to a certain endpoint e.g. POST /something/special. They are distinct from Operations and have no client-side helpers (like useQuery).

To create a Wasp API, you must:

  1. Declare the API in Wasp using the api declaration
  2. Define the API's NodeJS implementation

After completing these two steps, you'll be able to call the API from the client code (via our Axios wrapper), or from the outside world.

Declaring the API in Wasp

First, we need to declare the API in the Wasp file and you can easily do this with the api declaration:

main.wasp
// ...

api fooBar { // APIs and their implementations don't need to (but can) have the same name.
fn: import { fooBar } from "@server/apis.js",
httpRoute: (GET, "/foo/bar")
}

Read more about the supported fields in the API Reference.

Defining the API's NodeJS Implementation

After you defined the API, it should be implemented as a NodeJS function that takes three arguments:

  1. req: Express Request object
  2. res: Express Response object
  3. context: An additional context object injected into the API by Wasp. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the section about using entities in APIs.
src/server/apis.js
export const fooBar = (req, res, context) => {
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` });
};

Using the API

Using the API externally

To use the API externally, you simply call the endpoint using the method and path you used.

For example, if your app is running at https://example.com then from the above you could issue a GET to https://example/com/foo/callback (in your browser, Postman, curl, another web service, etc.).

Using the API from the Client

To use the API from your client, including with auth support, you can import the Axios wrapper from @wasp/api and invoke a call. For example:

src/client/pages/SomePage.jsx
import React, { useEffect } from "react";
import api from "@wasp/api";

async function fetchCustomRoute() {
const res = await api.get("/foo/bar");
console.log(res.data);
}

export const Foo = () => {
useEffect(() => {
fetchCustomRoute();
}, []);

return <>// ...</>;
};

Making Sure CORS Works

APIs are designed to be as flexible as possible, hence they don't utilize the default middleware like Operations do. As a result, to use these APIs on the client side, you must ensure that CORS (Cross-Origin Resource Sharing) is enabled.

You can do this by defining custom middleware for your APIs in the Wasp file.

For example, an apiNamespace is a simple declaration used to apply some middlewareConfigFn to all APIs under some specific path:

main.wasp
apiNamespace fooBar {
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
path: "/foo"
}

And then in the implementation file:

src/server/apis.js
export const apiMiddleware = (config) => {
return config;
};

We are returning the default middleware which enables CORS for all APIs under the /foo path.

For more information about middleware configuration, please see: Middleware Configuration

Using Entities in APIs

In many cases, resources used in APIs will be Entities. +

Custom HTTP API Endpoints

In Wasp, the default client-server interaction mechanism is through Operations. However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an api. Best of all, they should look and feel very familiar.

How to Create an API

APIs are used to tie a JS function to a certain endpoint e.g. POST /something/special. They are distinct from Operations and have no client-side helpers (like useQuery).

To create a Wasp API, you must:

  1. Declare the API in Wasp using the api declaration
  2. Define the API's NodeJS implementation

After completing these two steps, you'll be able to call the API from the client code (via our Axios wrapper), or from the outside world.

Declaring the API in Wasp

First, we need to declare the API in the Wasp file and you can easily do this with the api declaration:

main.wasp
// ...

api fooBar { // APIs and their implementations don't need to (but can) have the same name.
fn: import { fooBar } from "@server/apis.js",
httpRoute: (GET, "/foo/bar")
}

Read more about the supported fields in the API Reference.

Defining the API's NodeJS Implementation

After you defined the API, it should be implemented as a NodeJS function that takes three arguments:

  1. req: Express Request object
  2. res: Express Response object
  3. context: An additional context object injected into the API by Wasp. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the section about using entities in APIs.
src/server/apis.js
export const fooBar = (req, res, context) => {
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` });
};

Using the API

Using the API externally

To use the API externally, you simply call the endpoint using the method and path you used.

For example, if your app is running at https://example.com then from the above you could issue a GET to https://example/com/foo/callback (in your browser, Postman, curl, another web service, etc.).

Using the API from the Client

To use the API from your client, including with auth support, you can import the Axios wrapper from @wasp/api and invoke a call. For example:

src/client/pages/SomePage.jsx
import React, { useEffect } from "react";
import api from "@wasp/api";

async function fetchCustomRoute() {
const res = await api.get("/foo/bar");
console.log(res.data);
}

export const Foo = () => {
useEffect(() => {
fetchCustomRoute();
}, []);

return <>// ...</>;
};

Making Sure CORS Works

APIs are designed to be as flexible as possible, hence they don't utilize the default middleware like Operations do. As a result, to use these APIs on the client side, you must ensure that CORS (Cross-Origin Resource Sharing) is enabled.

You can do this by defining custom middleware for your APIs in the Wasp file.

For example, an apiNamespace is a simple declaration used to apply some middlewareConfigFn to all APIs under some specific path:

main.wasp
apiNamespace fooBar {
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
path: "/foo"
}

And then in the implementation file:

src/server/apis.js
export const apiMiddleware = (config) => {
return config;
};

We are returning the default middleware which enables CORS for all APIs under the /foo path.

For more information about middleware configuration, please see: Middleware Configuration

Using Entities in APIs

In many cases, resources used in APIs will be Entities. To use an Entity in your API, add it to the api declaration in Wasp:

main.wasp
api fooBar {
fn: import { fooBar } from "@server/apis.js",
entities: [Task],
httpRoute: (GET, "/foo/bar")
}

Wasp will inject the specified Entity into the APIs context argument, giving you access to the Entity's Prisma API:

src/server/apis.js
export const fooBar = (req, res, context) => {
res.json({ count: await context.entities.Task.count() });
};

The object context.entities.Task exposes prisma.task from Prisma's CRUD API.

API Reference

main.wasp
api fooBar {
fn: import { fooBar } from "@server/apis.js",
httpRoute: (GET, "/foo/bar"),
entities: [Task],
auth: true,
middlewareConfigFn: import { apiMiddleware } from "@server/apis.js"
}

The api declaration has the following fields:

  • fn: ServerImport required

    The import statement of the APIs NodeJs implementation.

  • httpRoute: (HttpMethod, string) required

    The HTTP (method, path) pair, where the method can be one of:

    • ALL, GET, POST, PUT or DELETE
    • and path is an Express path string.
  • entities: [Entity]

    A list of entities you wish to use inside your API. You can read more about it here.

  • auth: bool

    If auth is enabled, this will default to true and provide a context.user object. If you do not wish to attempt to parse the JWT in the Authorization Header, you should set this to false.

  • middlewareConfigFn: ServerImport

    The import statement to an Express middleware config function for this API. See more in middleware section of the docs.

- - + + \ No newline at end of file diff --git a/docs/advanced/deployment/cli.html b/docs/advanced/deployment/cli.html index 24f7aa5d50..52b69c2aa4 100644 --- a/docs/advanced/deployment/cli.html +++ b/docs/advanced/deployment/cli.html @@ -19,15 +19,15 @@ - - + +
-

Deploying with the Wasp CLI

Wasp CLI can deploy your full-stack application with only a single command. +

Deploying with the Wasp CLI

Wasp CLI can deploy your full-stack application with only a single command. The command automates the manual deployment process and is the recommended way of deploying Wasp apps.

Supported Providers

Wasp supports automated deployment to the following providers:

  • Fly.io - they offer 5$ free credit each month
  • Railway (coming soon, track it here #1157)

Fly.io

Prerequisites

Fly provides free allowances for up to 3 VMs (so deploying a Wasp app to a new account is free), but all plans require you to add your credit card information before you can proceed. If you don't, the deployment will fail.

You can add the required credit card information on the account's billing page.

Fly.io CLI

You will need the flyctl CLI installed on your machine before you can deploy to Fly.io.

Deploying

Using the Wasp CLI, you can easily deploy a new app to Fly.io with just a single command:

wasp deploy fly launch my-wasp-app mia

Please do not CTRL-C or exit your terminal while the commands are running.

Under the covers, this runs the equivalent of the following commands:

wasp deploy fly setup my-wasp-app mia
wasp deploy fly create-db mia
wasp deploy fly deploy

The commands above use the app basename my-wasp-app and deploy it to the Miami, Florida (US) region (called mia).

The basename is used to create all three app tiers, resulting in three separate apps in your Fly dashboard:

  • my-wasp-app-client
  • my-wasp-app-server
  • my-wasp-app-db
Unique Name

Your app name must be unique across all of Fly or deployment will fail.

Read more about Fly.io regions here.

Using a Custom Domain For Your App

Setting up a custom domain is a three-step process:

  1. You need to add your domain to your Fly client app. You can do this by running:
wasp deploy fly cmd --context client certs create mycoolapp.com
Use Your 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:

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
  1. 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.

  2. You need to set your domain as the WASP_WEB_CLIENT_URL environment variable for your server app:

wasp deploy fly cmd --context server secrets set WASP_WEB_CLIENT_URL=https://mycoolapp.com

We need to do this to keep our CORS configuration up to date.

That's it, your app should be available at https://mycoolapp.com! 🎉

API Reference

launch

launch is a convenience command that runs setup, create-db, and deploy in sequence.

wasp deploy fly launch <app-name> <region>

It accepts the following arguments:

  • <app-name> - the name of your app required

  • <region> - the region where your app will be deployed required

    Read how to find the available regions here.

It gives you the same result as running the following commands:

wasp deploy fly setup <app-name> <region>
wasp deploy fly create-db <region>
wasp deploy fly deploy

Environment Variables

If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the --server-secret option:

wasp deploy fly launch my-wasp-app mia --server-secret GOOGLE_CLIENT_ID=<...> --server-secret GOOGLE_CLIENT_SECRET=<...>

setup

setup will create your client and server apps on Fly, and add some secrets, but does not deploy them.

wasp deploy fly setup <app-name> <region>

It accepts the following arguments:

  • <app-name> - the name of your app required

  • <region> - the region where your app will be deployed required

    Read how to find the available regions here.

After running setup, Wasp creates two new files in your project root directory: fly-server.toml and fly-client.toml. You should include these files in your version control.

If you want to maintain multiple apps, you can add the --fly-toml-dir <abs-path> option to point to different directories, like "dev" or "staging".

Execute Only Once

You should only run setup once per app. If you run it multiple times, it will create unnecessary apps on Fly.

create-db

create-db will create a new database for your app.

wasp deploy fly create-db <region>

It accepts the following arguments:

  • <region> - the region where your app will be deployed required

    Read how to find the available regions here.

Execute Only Once

You should only run create-db once per app. If you run it multiple times, it will create multiple databases, but your app needs only one.

deploy

wasp deploy fly deploy

deploy pushes your client and server live.

Run this command whenever you want to update your deployed app with the latest changes:

wasp deploy fly deploy

cmd

If want to run arbitrary Fly commands (e.g. flyctl secrets list for your server app), here's how to do it:

wasp deploy fly cmd secrets list --context server

Fly.io Regions

Fly.io runs applications physically close to users: in datacenters around the world, on servers we run ourselves. You can currently deploy your apps in 34 regions, connected to a global Anycast network that makes sure your users hit our nearest server, whether they’re in Tokyo, São Paolo, or Frankfurt.

Read more on Fly regions here.

You can find the list of all available Fly regions by running:

flyctl platform regions

Environment Variables

If you are deploying an app that requires any other environment variables (like social auth secrets), you can set them with the secrets set command:

wasp deploy fly cmd secrets set GOOGLE_CLIENT_ID=<...> GOOGLE_CLIENT_SECRET=<...> --context=server

Mutliple Fly Organizations

If you have multiple organizations, you can specify a --org option. For example:

wasp deploy fly launch my-wasp-app mia --org hive

Building Locally

Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, the CLI defaults to the use of a remote Fly.io builder.

If you want to build locally, supply the --build-locally option to wasp deploy fly launch or wasp deploy fly deploy.

- - + + \ No newline at end of file diff --git a/docs/advanced/deployment/manually.html b/docs/advanced/deployment/manually.html index fe1e4abae0..b24192b6b1 100644 --- a/docs/advanced/deployment/manually.html +++ b/docs/advanced/deployment/manually.html @@ -19,19 +19,19 @@ - - + +
-

Deploying Manually

We'll cover how to deploy your Wasp app manually to a variety of providers:

Deploying a Wasp App

Deploying a Wasp app comes down to the following:

  1. Generating deployable code.
  2. Deploying the API server (backend).
  3. Deploying the web client (frontend).
  4. Deploying a PostgreSQL database and keeping it running.

Let's go through each of these steps.

1. Generating Deployable Code

Running the command wasp build generates deployable code for the whole app in the .wasp/build/ directory.

wasp build
PostgreSQL in production

You won't be able to build the app if you are using SQLite as a database (which is the default database). +

Deploying Manually

We'll cover how to deploy your Wasp app manually to a variety of providers:

Deploying a Wasp App

Deploying a Wasp app comes down to the following:

  1. Generating deployable code.
  2. Deploying the API server (backend).
  3. Deploying the web client (frontend).
  4. Deploying a PostgreSQL database and keeping it running.

Let's go through each of these steps.

1. Generating Deployable Code

Running the command wasp build generates deployable code for the whole app in the .wasp/build/ directory.

wasp build
PostgreSQL in production

You won't be able to build the app if you are using SQLite as a database (which is the default database). You'll have to switch to PostgreSQL before deploying to production.

2. Deploying the API Server (backend)

There's a Dockerfile that defines an image for building the server in the .wasp/build directory.

To run the server in production, deploy this Docker image to a hosting provider and ensure it has access to correct environment variables (this varies depending on the provider). All necessary environment variables are listed in the next section.

Environment Variables

Here are the environment variables your server requires to run:

  • PORT

    The server's HTTP port number. This is where the server listens for requests (e.g., 3001).

  • DATABASE_URL

    The URL of the Postgres database you want your app to use (e.g., postgresql://mydbuser:mypass@localhost:5432/nameofmydb).

  • WASP_WEB_CLIENT_URL

    The URL where you plan to deploy your frontend app is running (e.g., https://<app-name>.netlify.app). The server needs to know about it to properly configure Same-Origin Policy (CORS) headers.

  • JWT_SECRET

    You only need this environment variable if you're using Wasp's auth features. Set it to a random string at least 32 characters long (you can use an online generator).

Using an external auth method?

If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to set the necessary environment variables.

3. Deploying the Web Client (frontend)

To build the web app, position yourself in .wasp/build/web-app directory:

cd .wasp/build/web-app

Run

npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build

where <url_to_wasp_backend> is the URL of the Wasp server that you previously deployed.

The command above will build the web client and put it in the build/ directory in the web-app directory.

Since the app's frontend is just a bunch of static files, you can deploy it to any static hosting provider.

4. Deploying the Database

Any PostgreSQL database will do, as long as you set the DATABASE_URL env var correctly and ensure that the database is accessible from the server.

Different Providers

We'll cover a few different deployment providers below:

  • Fly.io (server and database)
  • Netlify (client)
  • Railway (server, client and database)
  • Heroku (server and database)

Fly.io

We automated this process for you

If you want to do all of the work below with one command, you can use the Wasp CLI.

Wasp CLI deploys the server, deploys the client, and sets up a database. It also gives you a way to redeploy (update) your app with a single command.

Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the flyctl CLI.

note

Fly.io offers support for both locally built Docker containers and remotely built ones. However, for simplicity and reproducibility, we will default to the use of a remote Fly.io builder.

Additionally, fly is a symlink for flyctl on most systems and they can be used interchangeably.

Make sure you are logged in with flyctl CLI. You can check if you are logged in with flyctl auth whoami, and if you are not, you can log in with flyctl auth login.

Set Up a Fly.io App

info

You need to do this only once per Wasp app.

Unless you already have a Fly.io app that you want to deploy to, let's create a new Fly.io app.

After you have built the app, position yourself in .wasp/build/ directory:

cd .wasp/build

Next, run the launch command to set up a new app and create a fly.toml file:

flyctl launch --remote-only

This will ask you a series of questions, such as asking you to choose a region and whether you'd like a database.

  • Say yes to Would you like to set up a Postgresql database now? and select Development. Fly.io will set a DATABASE_URL for you.

  • Say no to Would you like to deploy now? (and to any additional questions).

    We still need to set up several environment variables.

What if the database setup fails?

If your attempts to initiate a new app fail for whatever reason, then you should run flyctl apps destroy <app-name> before trying again. Fly does not allow you to create multiple apps with the same name.

What does it look like when your DB is deployed correctly?

When your DB is deployed correctly, you'll see it in the Fly.io dashboard:

image

Next, let's copy the fly.toml file up to our Wasp project dir for safekeeping.

cp fly.toml ../../

Next, let's add a few more environment variables:

flyctl secrets set PORT=8080
flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long>
flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
note

If you do not know what your frontend URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your frontend.

Using an external auth method?

If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to set the necessary environment variables.

If you want to make sure you've added your secrets correctly, run flyctl secrets list in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data.

Deploy to a Fly.io App

While still in the .wasp/build/ directory, run:

flyctl deploy --remote-only --config ../../fly.toml

This will build and deploy the backend of your Wasp app on Fly.io to https://<app-name>.fly.dev 🤘🎸

Now, if you haven't, you can deploy your frontend and add the client url by running flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_deployed_frontend>. We suggest using Netlify for your frontend, but you can use any static hosting provider.

Additionally, some useful flyctl commands:

flyctl logs
flyctl secrets list
flyctl ssh console

Redeploying After Wasp Builds

When you rebuild your Wasp app (with wasp build), it will remove your .wasp/build/ directory. In there, you may have a fly.toml from any prior Fly.io deployments.

While we will improve this process in the future, in the meantime, you have a few options:

  1. Copy the fly.toml file to a versioned directory, like your Wasp project dir.

    From there, you can reference it in flyctl deploy --config <path> commands, like above.

  2. Backup the fly.toml file somewhere before running wasp build, and copy it into .wasp/build/ after.

    When the fly.toml file exists in .wasp/build/ dir, you do not need to specify the --config <path>.

  3. Run flyctl config save -a <app-name> to regenerate the fly.toml file from the remote state stored in Fly.io.

Netlify

Netlify is a static hosting solution that is free for many use cases. You will need a Netlify account and Netlify CLI installed to follow these instructions.

Make sure you are logged in with Netlify CLI. You can check if you are logged in with netlify status, and if you are not, you can log in with netlify login.

First, make sure you have built the Wasp app. We'll build the client web app next.

To build the web app, position yourself in .wasp/build/web-app directory:

cd .wasp/build/web-app

Run

npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build

where <url_to_wasp_backend> is the URL of the Wasp server that you previously deployed.

We can now deploy the client with:

netlify deploy

Carefully follow the instructions i.e. do you want to create a new app or use an existing one, the team under which your app will reside etc.

The final step is to run:

netlify deploy --prod`

That is it! Your client should be live at https://<app-name>.netlify.app

note

Make sure you set this URL as the WASP_WEB_CLIENT_URL environment variable in your server hosting environment (e.g., Fly.io or Heroku).

Railway

Railway is a simple and great way to host your server and database. It's also possible to deploy your entire app: database, server, and client. You can use the platform for free for a limited time, or if you meet certain eligibility requirements. See their plans page for more info.

Prerequisites

To get started, follow these steps:

  1. Make sure your Wasp app is built by running wasp build in the project dir.

  2. Create a Railway account

    Free Tier

    Sign up with your GitHub account to be eligible for the free tier

  3. Install the Railway CLI

  4. Run railway login and a browser tab will open to authenticate you.

Create New Project

Let's create our Railway project:

  1. Go to your Railway dashboard, click on New Project, and select Provision PostgreSQL from the dropdown menu.
  2. Once it initializes, right-click on the New button in the top right corner and select Empty Service.
  3. Once it initializes, click on it, go to Settings > General and change the name to server
  4. Go ahead and create another empty service and name it client

Changing the name

Deploy Your App to Railway

Setup Domains

We'll need the domains for both the server and client services:

  1. Go to the server instance's Settings tab, and click Generate Domain.
  2. Do the same under the client's Settings.

Copy the domains as we will need them later.

Deploying the Server

Let's deploy our server first:

  1. Move into your app's .wasp/build/ directory:

    cd .wasp/build
  2. Link your app build to your newly created Railway project:

    railway link
  3. Go into the Railway dashboard and set up the required env variables:

    Open the Settings and go to the Variables tab:

    • click Variable reference and select DATABASE_URL (it will populate it with the correct value)

    • add WASP_WEB_CLIENT_URL - enter the the client domain (e.g. https://client-production-XXXX.up.railway.app)

    • add JWT_SECRET - enter a random string at least 32 characters long (use an online generator)

      Using an external auth method?

      If your app is using an external authentication method(s) supported by Wasp (such as Google or GitHub), make sure to set the necessary environment variables.

  4. Push and deploy the project:

railway up

Select server when prompted with Select Service.

Railway will now locate the Dockerfile and deploy your server 👍

Deploying the Client

  1. Next, change into your app's frontend build directory .wasp/build/web-app:

    cd web-app
  2. Create the production build, using the server domain as the REACT_APP_API_URL:

    npm install && REACT_APP_API_URL=<url_to_wasp_backend> npm run build
  3. Next, we want to link this specific frontend directory to our project as well:

    railway link
  4. We need to configure Railway's static hosting for our client.

    Setting Up Static Hosting

    Copy the build folder within the web-app directory to dist:

    cp -r build dist

    We'll need to create the following files:

    • Dockerfile with:

      Dockerfile
      FROM pierrezemb/gostatic
      CMD [ "-fallback", "index.html" ]
      COPY ./dist/ /srv/http/
    • .dockerignore with:

      .dockerignore
      node_modules/

    You'll need to repeat these steps each time you run wasp build as it will remove the .wasp/build/web-app directory.

    Here's a useful shell script to do the process

    If you want to automate the process, save the following as deploy_client.sh in the root of your project:

    deploy_client.sh
    #!/usr/bin/env bash

    if [ -z "$REACT_APP_API_URL" ]
    then
    echo "REACT_APP_API_URL is not set"
    exit 1
    fi

    wasp build
    cd .wasp/build/web-app

    npm install && REACT_APP_API_URL=$REACT_APP_API_URL npm run build

    cp -r build dist

    dockerfile_contents=$(cat <<EOF
    FROM pierrezemb/gostatic
    CMD [ "-fallback", "index.html" ]
    COPY ./dist/ /srv/http/
    EOF
    )

    dockerignore_contents=$(cat <<EOF
    node_modules/
    EOF
    )

    echo "$dockerfile_contents" > Dockerfile
    echo "$dockerignore_contents" > .dockerignore

    railway up

    Make it executable with:

    chmod +x deploy_client.sh

    You can run it with:

    REACT_APP_API_URL=<url_to_wasp_backend> ./deploy_client.sh
  5. Set the PORT environment variable to 8043 under the Variables tab.

  6. Deploy the client and select client when prompted with Select Service:

railway up

Conclusion

And now your Wasp should be deployed! 🐝 🚂 🚀

Back in your Railway dashboard, click on your project and you should see your newly deployed services: Postgres, Server, and Client.

Updates & Redeploying

When you make updates and need to redeploy:

  • run wasp build to rebuild your app
  • run railway up in the .wasp/build directory (server)
  • repeat all the steps in the .wasp/build/web-app directory (client)

Heroku

note

Heroku used to offer free apps under certain limits. However, as of November 28, 2022, they ended support for their free tier. https://blog.heroku.com/next-chapter

As such, we recommend using an alternative provider like Fly.io for your first apps.

You will need Heroku account, heroku CLI and docker CLI installed to follow these instructions.

Make sure you are logged in with heroku CLI. You can check if you are logged in with heroku whoami, and if you are not, you can log in with heroku login.

Set Up a Heroku App

info

You need to do this only once per Wasp app.

Unless you want to deploy to an existing Heroku app, let's create a new Heroku app:

heroku create <app-name>

Unless you have an external Postgres database that you want to use, let's create a new database on Heroku and attach it to our app:

heroku addons:create --app <app-name> heroku-postgresql:mini
caution

Heroku does not offer a free plan anymore and mini is their cheapest database instance - it costs $5/mo.

Heroku will also set DATABASE_URL env var for us at this point. If you are using an external database, you will have to set it up yourself.

The PORT env var will also be provided by Heroku, so the only two left to set are the JWT_SECRET and WASP_WEB_CLIENT_URL env vars:

heroku config:set --app <app-name> JWT_SECRET=<random_string_at_least_32_characters_long>
heroku config:set --app <app-name> WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
note

If you do not know what your frontend URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your frontend.

Deploy to a Heroku App

After you have built the app, position yourself in .wasp/build/ directory:

cd .wasp/build

assuming you were at the root of your Wasp project at that moment.

Log in to Heroku Container Registry:

heroku container:login

Build the docker image and push it to Heroku:

heroku container:push --app <app-name> web

App is still not deployed at this point. This step might take some time, especially the very first time, since there are no cached docker layers.

Note for Apple Silicon Users

Apple Silicon users need to build a non-Arm image, so the above step will not work at this time. Instead of heroku container:push, users instead should:

docker buildx build --platform linux/amd64 -t <app-name> .
docker tag <app-name> registry.heroku.com/<app-name>/web
docker push registry.heroku.com/<app-name>/web

You are now ready to proceed to the next step.

Deploy the pushed image and restart the app:

heroku container:release --app <app-name> web

This is it, the backend is deployed at https://<app-name>-XXXX.herokuapp.com 🎉

Find out the exact app URL with:

heroku info --app <app-name>

Additionally, you can check out the logs with:

heroku logs --tail --app <app-name>
Using pg-boss with Heroku

If you wish to deploy an app leveraging Jobs that use pg-boss as the executor to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.

Read more: https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js

- - + + \ No newline at end of file diff --git a/docs/advanced/deployment/overview.html b/docs/advanced/deployment/overview.html index 6d98e3c0fd..04cc7d18cd 100644 --- a/docs/advanced/deployment/overview.html +++ b/docs/advanced/deployment/overview.html @@ -19,17 +19,17 @@ - - + +
-

Overview

Wasp apps are full-stack apps that consist of:

  • A Node.js server.
  • A static client.
  • A PostgreSQL database.

You can deploy each part anywhere where you can usually deploy Node.js apps or static apps. For example, you can deploy your client on Netlify, the server on Fly.io, and the database on Neon.

To make deploying as smooth as possible, Wasp also offers a single-command deployment through the Wasp CLI. Read more about deploying through the CLI here.

Click on each deployment method for more details.

Regardless of how you choose to deploy your app (i.e., manually or using the Wasp CLI), you'll need to know about some common patterns covered below.

Customizing the Dockerfile

By default, Wasp generates a multi-stage Dockerfile. +

Overview

Wasp apps are full-stack apps that consist of:

  • A Node.js server.
  • A static client.
  • A PostgreSQL database.

You can deploy each part anywhere where you can usually deploy Node.js apps or static apps. For example, you can deploy your client on Netlify, the server on Fly.io, and the database on Neon.

To make deploying as smooth as possible, Wasp also offers a single-command deployment through the Wasp CLI. Read more about deploying through the CLI here.

Click on each deployment method for more details.

Regardless of how you choose to deploy your app (i.e., manually or using the Wasp CLI), you'll need to know about some common patterns covered below.

Customizing the Dockerfile

By default, Wasp generates a multi-stage Dockerfile. This file is used to build and run a Docker image with the Wasp-generated server code. It also runs any pending migrations.

You can add extra steps to this multi-stage Dockerfile by creating your own Dockerfile in the project's root directory. If Wasp finds a Dockerfile in the project's root, it appends its contents at the bottom of the default multi-stage Dockerfile.

Since the last definition in a Dockerfile wins, you can override or continue from any existing build stages. -You can also choose not to use any of our build stages and have your own custom Dockerfile used as-is.

A few things to keep in mind:

  • If you override an intermediate build stage, no later build stages will be used unless you reproduce them below.
  • The generated Dockerfile's content is dynamic and depends on which features your app uses. The content can also change in future releases, so please verify it from time to time.
  • Make sure to supply ENTRYPOINT in your final build stage. Your changes won't have any effect if you don't.

Read more in the official Docker docs on multi-stage builds.

To see what your project's (potentially combined) Dockerfile will look like, run:

wasp dockerfile

Join our Discord if you have any questions, or if you need more customization than this hook provides.

- - +You can also choose not to use any of our build stages and have your own custom Dockerfile used as-is.

A few things to keep in mind:

  • If you override an intermediate build stage, no later build stages will be used unless you reproduce them below.
  • The generated Dockerfile's content is dynamic and depends on which features your app uses. The content can also change in future releases, so please verify it from time to time.
  • Make sure to supply ENTRYPOINT in your final build stage. Your changes won't have any effect if you don't.

Read more in the official Docker docs on multi-stage builds.

To see what your project's (potentially combined) Dockerfile will look like, run:

wasp dockerfile

Join our Discord if you have any questions, or if you need more customization than this hook provides.

+ + \ No newline at end of file diff --git a/docs/advanced/email.html b/docs/advanced/email.html index 6409aa3c4d..bd564d67f1 100644 --- a/docs/advanced/email.html +++ b/docs/advanced/email.html @@ -19,13 +19,13 @@ - - + +
-

Sending Emails

With Wasp's email sending feature, you can easily integrate email functionality into your web application.

main.wasp
app Example {
...
emailSender: {
provider: <provider>,
defaultFrom: {
name: "Example",
email: "hello@itsme.com"
},
}
}

Choose from one of the providers:

  • Mailgun,
  • SendGrid
  • or the good old SMTP.

Optionally, define the defaultFrom field, so you don't need to provide it whenever sending an email.

Sending Emails

Sending emails while developing

When you run your app in development mode, the emails are not sent. Instead, they are logged to the console.

To enable sending emails in development mode, you need to set the SEND_EMAILS_IN_DEVELOPMENT env variable to true in your .env.server file.

Before jumping into details about setting up various providers, let's see how easy it is to send emails.

You import the emailSender that is provided by the @wasp/email module and call the send method on it.

src/actions/sendEmail.js
import { emailSender } from "@wasp/email/index.js";

// In some action handler...
const info = await emailSender.send({
from: {
name: "John Doe",
email: "john@doe.com",
},
to: "user@domain.com",
subject: "Saying hello",
text: "Hello world",
html: "Hello <strong>world</strong>",
});

Read more about the send method in the API Reference.

The send method returns an object with the status of the sent email. It varies depending on the provider you use.

Providers

For each provider, you'll need to set up env variables in the .env.server file at the root of your project.

Using the SMTP Provider

First, set the provider to SMTP in your main.wasp file.

main.wasp
app Example {
...
emailSender: {
provider: SMTP,
}
}

Then, add the following env variables to your .env.server file.

.env.server
SMTP_HOST=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_PORT=

Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well.

Using the Mailgun Provider

Set the provider to Mailgun in the main.wasp file.

main.wasp
app Example {
...
emailSender: {
provider: Mailgun,
}
}

Then, get the Mailgun API key and domain and add them to your .env.server file.

Getting the API Key and Domain

  1. Go to Mailgun and create an account.
  2. Go to API Keys and create a new API key.
  3. Copy the API key and add it to your .env.server file.
  4. Go to Domains and create a new domain.
  5. Copy the domain and add it to your .env.server file.
.env.server
MAILGUN_API_KEY=
MAILGUN_DOMAIN=

Using the SendGrid Provider

Set the provider field to SendGrid in your main.wasp file.

main.wasp
app Example {
...
emailSender: {
provider: SendGrid,
}
}

Then, get the SendGrid API key and add it to your .env.server file.

Getting the API Key

  1. Go to SendGrid and create an account.
  2. Go to API Keys and create a new API key.
  3. Copy the API key and add it to your .env.server file.
.env.server
SENDGRID_API_KEY=

API Reference

emailSender dict

main.wasp
app Example {
...
emailSender: {
provider: <provider>,
defaultFrom: {
name: "Example",
email: "hello@itsme.com"
},
}
}

The emailSender dict has the following fields:

  • provider: Provider required

    The provider you want to use. Choose from SMTP, Mailgun or SendGrid.

  • defaultFrom: dict

    The default sender's details. If you set this field, you don't need to provide the from field when sending an email.

JavaScript API

Using the emailSender in :

src/actions/sendEmail.js
import { emailSender } from "@wasp/email/index.js";

// In some action handler...
const info = await emailSender.send({
from: {
name: "John Doe",
email: "john@doe.com",
},
to: "user@domain.com",
subject: "Saying hello",
text: "Hello world",
html: "Hello <strong>world</strong>",
});

The send method accepts an object with the following fields:

  • from: object

    The sender's details. If you set up defaultFrom field in the emailSender dict in Wasp file, this field is optional.

    • name: string

      The name of the sender.

    • email: string

      The email address of the sender.

  • to: string required

    The recipient's email address.

  • subject: string required

    The subject of the email.

  • text: string required

    The text version of the email.

  • html: string required

    The HTML version of the email

- - +

Sending Emails

With Wasp's email sending feature, you can easily integrate email functionality into your web application.

main.wasp
app Example {
...
emailSender: {
provider: <provider>,
defaultFrom: {
name: "Example",
email: "hello@itsme.com"
},
}
}

Choose from one of the providers:

  • Mailgun,
  • SendGrid
  • or the good old SMTP.

Optionally, define the defaultFrom field, so you don't need to provide it whenever sending an email.

Sending Emails

Sending emails while developing

When you run your app in development mode, the emails are not sent. Instead, they are logged to the console.

To enable sending emails in development mode, you need to set the SEND_EMAILS_IN_DEVELOPMENT env variable to true in your .env.server file.

Before jumping into details about setting up various providers, let's see how easy it is to send emails.

You import the emailSender that is provided by the @wasp/email module and call the send method on it.

src/actions/sendEmail.js
import { emailSender } from "@wasp/email/index.js";

// In some action handler...
const info = await emailSender.send({
from: {
name: "John Doe",
email: "john@doe.com",
},
to: "user@domain.com",
subject: "Saying hello",
text: "Hello world",
html: "Hello <strong>world</strong>",
});

Read more about the send method in the API Reference.

The send method returns an object with the status of the sent email. It varies depending on the provider you use.

Providers

For each provider, you'll need to set up env variables in the .env.server file at the root of your project.

Using the SMTP Provider

First, set the provider to SMTP in your main.wasp file.

main.wasp
app Example {
...
emailSender: {
provider: SMTP,
}
}

Then, add the following env variables to your .env.server file.

.env.server
SMTP_HOST=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_PORT=

Many transactional email providers (e.g. Mailgun, SendGrid but also others) can also use SMTP, so you can use them as well.

Using the Mailgun Provider

Set the provider to Mailgun in the main.wasp file.

main.wasp
app Example {
...
emailSender: {
provider: Mailgun,
}
}

Then, get the Mailgun API key and domain and add them to your .env.server file.

Getting the API Key and Domain

  1. Go to Mailgun and create an account.
  2. Go to API Keys and create a new API key.
  3. Copy the API key and add it to your .env.server file.
  4. Go to Domains and create a new domain.
  5. Copy the domain and add it to your .env.server file.
.env.server
MAILGUN_API_KEY=
MAILGUN_DOMAIN=

Using the SendGrid Provider

Set the provider field to SendGrid in your main.wasp file.

main.wasp
app Example {
...
emailSender: {
provider: SendGrid,
}
}

Then, get the SendGrid API key and add it to your .env.server file.

Getting the API Key

  1. Go to SendGrid and create an account.
  2. Go to API Keys and create a new API key.
  3. Copy the API key and add it to your .env.server file.
.env.server
SENDGRID_API_KEY=

API Reference

emailSender dict

main.wasp
app Example {
...
emailSender: {
provider: <provider>,
defaultFrom: {
name: "Example",
email: "hello@itsme.com"
},
}
}

The emailSender dict has the following fields:

  • provider: Provider required

    The provider you want to use. Choose from SMTP, Mailgun or SendGrid.

  • defaultFrom: dict

    The default sender's details. If you set this field, you don't need to provide the from field when sending an email.

JavaScript API

Using the emailSender in :

src/actions/sendEmail.js
import { emailSender } from "@wasp/email/index.js";

// In some action handler...
const info = await emailSender.send({
from: {
name: "John Doe",
email: "john@doe.com",
},
to: "user@domain.com",
subject: "Saying hello",
text: "Hello world",
html: "Hello <strong>world</strong>",
});

The send method accepts an object with the following fields:

  • from: object

    The sender's details. If you set up defaultFrom field in the emailSender dict in Wasp file, this field is optional.

    • name: string

      The name of the sender.

    • email: string

      The email address of the sender.

  • to: string required

    The recipient's email address.

  • subject: string required

    The subject of the email.

  • text: string required

    The text version of the email.

  • html: string required

    The HTML version of the email

+ + \ No newline at end of file diff --git a/docs/advanced/jobs.html b/docs/advanced/jobs.html index a7ff505817..1a28e87d38 100644 --- a/docs/advanced/jobs.html +++ b/docs/advanced/jobs.html @@ -19,13 +19,13 @@ - - + +
-

Recurring Jobs

In most web apps, users send requests to the server and receive responses with some data. When the server responds quickly, the app feels responsive and smooth.

What if the server needs extra time to fully process the request? This might mean sending an email or making a slow HTTP request to an external API. In that case, it's a good idea to respond to the user as soon as possible and do the remaining work in the background.

Wasp supports background jobs that can help you with this:

  • Jobs persist between server restarts,
  • Jobs can be retried if they fail,
  • Jobs can be delayed until a future time,
  • Jobs can have a recurring schedule.

Using Jobs

Job Definition and Usage

Let's write an example Job that will print a message to the console and return a list of tasks from the database.

  1. Start by creating a Job declaration in your .wasp file:

    main.wasp
    job mySpecialJob {
    executor: PgBoss,
    perform: {
    fn: import { foo } from "@server/workers/bar.js"
    },
    entities: [Task],
    }
  2. After declaring the Job, implement its worker function:

    bar.js
    export const foo = async ({ name }, context) => {
    console.log(`Hello ${name}!`)
    const tasks = await context.entities.Task.findMany({})
    return { tasks }
    }
    The worker function

    The worker function must be an async function. The function's return value represents the Job's result.

    The worker function accepts two arguments:

    • args: The data passed into the job when it's submitted.
    • context: { entities }: The context object containing entities you put in the Job declaration.
  3. After successfully defining the job, you can submit work to be done in your Operations or setupFn (or any other NodeJS code):

    someAction.js
    import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'

    const submittedJob = await mySpecialJob.submit({ job: "Johnny" })

    // Or, if you'd prefer it to execute in the future, just add a .delay().
    // It takes a number of seconds, Date, or ISO date string.
    await mySpecialJob
    .delay(10)
    .submit({ name: "Johnny" })

And that'is it. Your job will be executed by PgBoss as if you called foo({ name: "Johnny" }).

In our example, foo takes an argument, but passing arguments to jobs is not a requirement. It depends on how you've implemented your worker function.

Recurring Jobs

If you have work that needs to be done on some recurring basis, you can add a schedule to your job declaration:

main.wasp
job mySpecialJob {
executor: PgBoss,
perform: {
fn: import { foo } from "@server/workers/bar.js"
},
schedule: {
cron: "0 * * * *",
args: {=json { "job": "args" } json=} // optional
}
}

In this example, you don't need to invoke anything in . You can imagine foo({ job: "args" }) getting automatically scheduled and invoked for you every hour.

API Reference

Declaring Jobs

main.wasp
job mySpecialJob {
executor: PgBoss,
perform: {
fn: import { foo } from "@server/workers/bar.js",
executorOptions: {
pgBoss: {=json { "retryLimit": 1 } json=}
}
},
schedule: {
cron: "*/5 * * * *",
args: {=json { "foo": "bar" } json=},
executorOptions: {
pgBoss: {=json { "retryLimit": 0 } json=}
}
},
entities: [Task],
}

The Job declaration has the following fields:

  • executor: JobExecutor required

    Job executors

    Our jobs need job executors to handle the scheduling, monitoring, and execution.

    PgBoss is currently our only job executor, and is recommended for low-volume production use cases. It requires your app.db.system to be PostgreSQL.

    We have selected pg-boss as our first job executor to handle the low-volume, basic job queue workloads many web applications have. By using PostgreSQL (and SKIP LOCKED) as its storage and synchronization mechanism, it allows us to provide many job queue pros without any additional infrastructure or complex management.

    info

    Keep in mind that pg-boss jobs run alongside your other server-side code, so they are not appropriate for CPU-heavy workloads. Additionally, some care is required if you modify scheduled jobs. Please see pg-boss details below for more information.

    pg-boss details

    pg-boss provides many useful features, which can be found here.

    When you add pg-boss to a Wasp project, it will automatically add a new schema to your database called pgboss with some internal tracking tables, including job and schedule. pg-boss tables have a name column in most tables that will correspond to your Job identifier. Additionally, these tables maintain arguments, states, return values, retry information, start and expiration times, and other metadata required by pg-boss.

    If you need to customize the creation of the pg-boss instance, you can set an environment variable called PG_BOSS_NEW_OPTIONS to a stringified JSON object containing these initialization parameters. NOTE: Setting this overwrites all Wasp defaults, so you must include database connection information as well.

    pg-boss considerations

    • Wasp starts pg-boss alongside your web server's application, where both are simultaneously operational. This means that jobs running via pg-boss and the rest of the server logic (like Operations) share the CPU, therefore you should avoid running CPU-intensive tasks via jobs.
      • Wasp does not (yet) support independent, horizontal scaling of pg-boss-only applications, nor starting them as separate workers/processes/threads.
    • The job name/identifier in your .wasp file is the same name that will be used in the name column of pg-boss tables. If you change a name that had a schedule associated with it, pg-boss will continue scheduling those jobs but they will have no handlers associated, and will thus become stale and expire. To resolve this, you can remove the applicable row from the schedule table in the pgboss schema of your database.
      • If you remove a schedule from a job, you will need to do the above as well.
    • If you wish to deploy to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.
    • https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
  • perform: dict required

    • fn: ServerImport required

      • An async function that performs the work. Since Wasp executes Jobs on the server, you must import it from @server.
      • It receives the following arguments:
        • args: Input: The data passed to the job when it's submitted.
        • context: { entities: Entities }: The context object containing any declared entities.

      Here's an example of a perform.fn function:

      bar.js
      export const foo = async ({ name }, context) => {
      console.log(`Hello ${name}!`)
      const tasks = await context.entities.Task.findMany({})
      return { tasks }
      }
    • executorOptions: dict

      Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with submit() or in a schedule.

      • pgBoss: JSON

        See the docs for pg-boss.

  • schedule: dict

    • cron: string required

      A 5-placeholder format cron expression string. See rationale for minute-level precision here.

      If you need help building cron expressions, Check out Crontab guru.

    • args: JSON

      The arguments to pass to the perform.fn function when invoked.

    • executorOptions: dict

      Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The perform.executorOptions are the default options, and schedule.executorOptions can override/extend those.

      • pgBoss: JSON

        See the docs for pg-boss.

  • entities: [Entity]

    A list of entities you wish to use inside your Job (similar to Queries and Actions).

JavaScript API

  • Importing a Job:

    someAction.js
    import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'
  • submit(jobArgs, executorOptions)

    • jobArgs: Input

    • executorOptions: object

      Submits a Job to be executed by an executor, optionally passing in a JSON job argument your job handler function receives, and executor-specific submit options.

    someAction.js
    const submittedJob = await mySpecialJob.submit({ job: "args" })
  • delay(startAfter)

    • startAfter: int | string | Date required

      Delaying the invocation of the job handler. The delay can be one of:

      • Integer: number of seconds to delay. [Default 0]
      • String: ISO date string to run at.
      • Date: Date to run at.
    someAction.js
    const submittedJob = await mySpecialJob
    .delay(10)
    .submit({ job: "args" }, { "retryLimit": 2 })

Tracking

The return value of submit() is an instance of SubmittedJob, which has the following fields:

  • jobId: The ID for the job in that executor.
  • jobName: The name of the job you used in your .wasp file.
  • executorName: The Symbol of the name of the job executor.
    • For pg-boss, you can import a Symbol from: import { PG_BOSS_EXECUTOR_NAME } from '@wasp/jobs/core/pgBoss/pgBossJob.js' if you wish to compare against executorName.

There are also some namespaced, job executor-specific objects.

  • For pg-boss, you may access: pgBoss
    • details(): pg-boss specific job detail information. Reference
    • cancel(): attempts to cancel a job. Reference
    • resume(): attempts to resume a canceled job. Reference
- - +

Recurring Jobs

In most web apps, users send requests to the server and receive responses with some data. When the server responds quickly, the app feels responsive and smooth.

What if the server needs extra time to fully process the request? This might mean sending an email or making a slow HTTP request to an external API. In that case, it's a good idea to respond to the user as soon as possible and do the remaining work in the background.

Wasp supports background jobs that can help you with this:

  • Jobs persist between server restarts,
  • Jobs can be retried if they fail,
  • Jobs can be delayed until a future time,
  • Jobs can have a recurring schedule.

Using Jobs

Job Definition and Usage

Let's write an example Job that will print a message to the console and return a list of tasks from the database.

  1. Start by creating a Job declaration in your .wasp file:

    main.wasp
    job mySpecialJob {
    executor: PgBoss,
    perform: {
    fn: import { foo } from "@server/workers/bar.js"
    },
    entities: [Task],
    }
  2. After declaring the Job, implement its worker function:

    bar.js
    export const foo = async ({ name }, context) => {
    console.log(`Hello ${name}!`)
    const tasks = await context.entities.Task.findMany({})
    return { tasks }
    }
    The worker function

    The worker function must be an async function. The function's return value represents the Job's result.

    The worker function accepts two arguments:

    • args: The data passed into the job when it's submitted.
    • context: { entities }: The context object containing entities you put in the Job declaration.
  3. After successfully defining the job, you can submit work to be done in your Operations or setupFn (or any other NodeJS code):

    someAction.js
    import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'

    const submittedJob = await mySpecialJob.submit({ job: "Johnny" })

    // Or, if you'd prefer it to execute in the future, just add a .delay().
    // It takes a number of seconds, Date, or ISO date string.
    await mySpecialJob
    .delay(10)
    .submit({ name: "Johnny" })

And that'is it. Your job will be executed by PgBoss as if you called foo({ name: "Johnny" }).

In our example, foo takes an argument, but passing arguments to jobs is not a requirement. It depends on how you've implemented your worker function.

Recurring Jobs

If you have work that needs to be done on some recurring basis, you can add a schedule to your job declaration:

main.wasp
job mySpecialJob {
executor: PgBoss,
perform: {
fn: import { foo } from "@server/workers/bar.js"
},
schedule: {
cron: "0 * * * *",
args: {=json { "job": "args" } json=} // optional
}
}

In this example, you don't need to invoke anything in . You can imagine foo({ job: "args" }) getting automatically scheduled and invoked for you every hour.

API Reference

Declaring Jobs

main.wasp
job mySpecialJob {
executor: PgBoss,
perform: {
fn: import { foo } from "@server/workers/bar.js",
executorOptions: {
pgBoss: {=json { "retryLimit": 1 } json=}
}
},
schedule: {
cron: "*/5 * * * *",
args: {=json { "foo": "bar" } json=},
executorOptions: {
pgBoss: {=json { "retryLimit": 0 } json=}
}
},
entities: [Task],
}

The Job declaration has the following fields:

  • executor: JobExecutor required

    Job executors

    Our jobs need job executors to handle the scheduling, monitoring, and execution.

    PgBoss is currently our only job executor, and is recommended for low-volume production use cases. It requires your app.db.system to be PostgreSQL.

    We have selected pg-boss as our first job executor to handle the low-volume, basic job queue workloads many web applications have. By using PostgreSQL (and SKIP LOCKED) as its storage and synchronization mechanism, it allows us to provide many job queue pros without any additional infrastructure or complex management.

    info

    Keep in mind that pg-boss jobs run alongside your other server-side code, so they are not appropriate for CPU-heavy workloads. Additionally, some care is required if you modify scheduled jobs. Please see pg-boss details below for more information.

    pg-boss details

    pg-boss provides many useful features, which can be found here.

    When you add pg-boss to a Wasp project, it will automatically add a new schema to your database called pgboss with some internal tracking tables, including job and schedule. pg-boss tables have a name column in most tables that will correspond to your Job identifier. Additionally, these tables maintain arguments, states, return values, retry information, start and expiration times, and other metadata required by pg-boss.

    If you need to customize the creation of the pg-boss instance, you can set an environment variable called PG_BOSS_NEW_OPTIONS to a stringified JSON object containing these initialization parameters. NOTE: Setting this overwrites all Wasp defaults, so you must include database connection information as well.

    pg-boss considerations

    • Wasp starts pg-boss alongside your web server's application, where both are simultaneously operational. This means that jobs running via pg-boss and the rest of the server logic (like Operations) share the CPU, therefore you should avoid running CPU-intensive tasks via jobs.
      • Wasp does not (yet) support independent, horizontal scaling of pg-boss-only applications, nor starting them as separate workers/processes/threads.
    • The job name/identifier in your .wasp file is the same name that will be used in the name column of pg-boss tables. If you change a name that had a schedule associated with it, pg-boss will continue scheduling those jobs but they will have no handlers associated, and will thus become stale and expire. To resolve this, you can remove the applicable row from the schedule table in the pgboss schema of your database.
      • If you remove a schedule from a job, you will need to do the above as well.
    • If you wish to deploy to Heroku, you need to set an additional environment variable called PG_BOSS_NEW_OPTIONS to {"connectionString":"<REGULAR_HEROKU_DATABASE_URL>","ssl":{"rejectUnauthorized":false}}. This is because pg-boss uses the pg extension, which does not seem to connect to Heroku over SSL by default, which Heroku requires. Additionally, Heroku uses a self-signed cert, so we must handle that as well.
    • https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
  • perform: dict required

    • fn: ServerImport required

      • An async function that performs the work. Since Wasp executes Jobs on the server, you must import it from @server.
      • It receives the following arguments:
        • args: Input: The data passed to the job when it's submitted.
        • context: { entities: Entities }: The context object containing any declared entities.

      Here's an example of a perform.fn function:

      bar.js
      export const foo = async ({ name }, context) => {
      console.log(`Hello ${name}!`)
      const tasks = await context.entities.Task.findMany({})
      return { tasks }
      }
    • executorOptions: dict

      Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with submit() or in a schedule.

      • pgBoss: JSON

        See the docs for pg-boss.

  • schedule: dict

    • cron: string required

      A 5-placeholder format cron expression string. See rationale for minute-level precision here.

      If you need help building cron expressions, Check out Crontab guru.

    • args: JSON

      The arguments to pass to the perform.fn function when invoked.

    • executorOptions: dict

      Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The perform.executorOptions are the default options, and schedule.executorOptions can override/extend those.

      • pgBoss: JSON

        See the docs for pg-boss.

  • entities: [Entity]

    A list of entities you wish to use inside your Job (similar to Queries and Actions).

JavaScript API

  • Importing a Job:

    someAction.js
    import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'
  • submit(jobArgs, executorOptions)

    • jobArgs: Input

    • executorOptions: object

      Submits a Job to be executed by an executor, optionally passing in a JSON job argument your job handler function receives, and executor-specific submit options.

    someAction.js
    const submittedJob = await mySpecialJob.submit({ job: "args" })
  • delay(startAfter)

    • startAfter: int | string | Date required

      Delaying the invocation of the job handler. The delay can be one of:

      • Integer: number of seconds to delay. [Default 0]
      • String: ISO date string to run at.
      • Date: Date to run at.
    someAction.js
    const submittedJob = await mySpecialJob
    .delay(10)
    .submit({ job: "args" }, { "retryLimit": 2 })

Tracking

The return value of submit() is an instance of SubmittedJob, which has the following fields:

  • jobId: The ID for the job in that executor.
  • jobName: The name of the job you used in your .wasp file.
  • executorName: The Symbol of the name of the job executor.
    • For pg-boss, you can import a Symbol from: import { PG_BOSS_EXECUTOR_NAME } from '@wasp/jobs/core/pgBoss/pgBossJob.js' if you wish to compare against executorName.

There are also some namespaced, job executor-specific objects.

  • For pg-boss, you may access: pgBoss
    • details(): pg-boss specific job detail information. Reference
    • cancel(): attempts to cancel a job. Reference
    • resume(): attempts to resume a canceled job. Reference
+ + \ No newline at end of file diff --git a/docs/advanced/links.html b/docs/advanced/links.html index c12673da52..5a4b676199 100644 --- a/docs/advanced/links.html +++ b/docs/advanced/links.html @@ -19,13 +19,13 @@ - - + +
-

Type-Safe Links

If you are using Typescript, you can use Wasp's custom Link component to create type-safe links to other pages on your site.

After you defined a route:

main.wasp
route TaskRoute { path: "/task/:id", to: TaskPage }
page TaskPage { ... }

You can get the benefits of type-safe links by using the Link component from @wasp/router:

TaskList.tsx
import { Link } from '@wasp/router'

export const TaskList = () => {
// ...

return (
<div>
{tasks.map((task) => (
<Link
key={task.id}
to="/task/:id"
{/* 👆 You must provide a valid path here */}
params={{ id: task.id }}>
{/* 👆 All the params must be correctly passed in */}
{task.description}
</Link>
))}
</div>
)
}

Using Search Query & Hash

You can also pass search and hash props to the Link component:

TaskList.tsx
<Link
to="/task/:id"
params={{ id: task.id }}
search={{ sortBy: 'date' }}
hash="comments"
>
{task.description}
</Link>

This will result in a link like this: /task/1?sortBy=date#comments. Check out the API Reference for more details.

The routes Object

You can also get all the pages in your app with the routes object:

TaskList.tsx
import { routes } from '@wasp/router'

const linkToTask = routes.TaskRoute.build({ params: { id: 1 } })

This will result in a link like this: /task/1.

You can also pass search and hash props to the build function. Check out the API Reference for more details.

API Reference

The Link component accepts the following props:

  • to required

    • A valid Wasp Route path from your main.wasp file.
  • params: { [name: string]: string | number } required (if the path contains params)

    • An object with keys and values for each param in the path.
    • For example, if the path is /task/:id, then the params prop must be { id: 1 }. Wasp supports required and optional params.
  • search: string[][] | Record<string, string> | string | URLSearchParams

    • Any valid input for URLSearchParams constructor.
    • For example, the object { sortBy: 'date' } becomes ?sortBy=date.
  • hash: string

  • all other props that the react-router-dom's Link component accepts

routes Object

The routes object contains a function for each route in your app.

router.tsx
export const routes = {
// RootRoute has a path like "/"
RootRoute: {
build: (options?: {
search?: string[][] | Record<string, string> | string | URLSearchParams
hash?: string
}) => // ...
},

// DetailRoute has a path like "/task/:id/:something?"
DetailRoute: {
build: (
options: {
params: { id: ParamValue; something?: ParamValue; },
search?: string[][] | Record<string, string> | string | URLSearchParams
hash?: string
}
) => // ...
}
}

The params object is required if the route contains params. The search and hash parameters are optional.

You can use the routes object like this:

import { routes } from '@wasp/router'

const linkToRoot = routes.RootRoute.build()
const linkToTask = routes.DetailRoute.build({ params: { id: 1 } })
- - +

Type-Safe Links

If you are using Typescript, you can use Wasp's custom Link component to create type-safe links to other pages on your site.

After you defined a route:

main.wasp
route TaskRoute { path: "/task/:id", to: TaskPage }
page TaskPage { ... }

You can get the benefits of type-safe links by using the Link component from @wasp/router:

TaskList.tsx
import { Link } from '@wasp/router'

export const TaskList = () => {
// ...

return (
<div>
{tasks.map((task) => (
<Link
key={task.id}
to="/task/:id"
{/* 👆 You must provide a valid path here */}
params={{ id: task.id }}>
{/* 👆 All the params must be correctly passed in */}
{task.description}
</Link>
))}
</div>
)
}

Using Search Query & Hash

You can also pass search and hash props to the Link component:

TaskList.tsx
<Link
to="/task/:id"
params={{ id: task.id }}
search={{ sortBy: 'date' }}
hash="comments"
>
{task.description}
</Link>

This will result in a link like this: /task/1?sortBy=date#comments. Check out the API Reference for more details.

The routes Object

You can also get all the pages in your app with the routes object:

TaskList.tsx
import { routes } from '@wasp/router'

const linkToTask = routes.TaskRoute.build({ params: { id: 1 } })

This will result in a link like this: /task/1.

You can also pass search and hash props to the build function. Check out the API Reference for more details.

API Reference

The Link component accepts the following props:

  • to required

    • A valid Wasp Route path from your main.wasp file.
  • params: { [name: string]: string | number } required (if the path contains params)

    • An object with keys and values for each param in the path.
    • For example, if the path is /task/:id, then the params prop must be { id: 1 }. Wasp supports required and optional params.
  • search: string[][] | Record<string, string> | string | URLSearchParams

    • Any valid input for URLSearchParams constructor.
    • For example, the object { sortBy: 'date' } becomes ?sortBy=date.
  • hash: string

  • all other props that the react-router-dom's Link component accepts

routes Object

The routes object contains a function for each route in your app.

router.tsx
export const routes = {
// RootRoute has a path like "/"
RootRoute: {
build: (options?: {
search?: string[][] | Record<string, string> | string | URLSearchParams
hash?: string
}) => // ...
},

// DetailRoute has a path like "/task/:id/:something?"
DetailRoute: {
build: (
options: {
params: { id: ParamValue; something?: ParamValue; },
search?: string[][] | Record<string, string> | string | URLSearchParams
hash?: string
}
) => // ...
}
}

The params object is required if the route contains params. The search and hash parameters are optional.

You can use the routes object like this:

import { routes } from '@wasp/router'

const linkToRoot = routes.RootRoute.build()
const linkToTask = routes.DetailRoute.build({ params: { id: 1 } })
+ + \ No newline at end of file diff --git a/docs/advanced/middleware-config.html b/docs/advanced/middleware-config.html index 91816bbba3..53e6fe91f5 100644 --- a/docs/advanced/middleware-config.html +++ b/docs/advanced/middleware-config.html @@ -19,13 +19,13 @@ - - + +
-

Configuring Middleware

Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-api/path basis.

Default Global Middleware 🌍

Wasp's Express server has the following middleware by default:

  • Helmet: Helmet helps you secure your Express apps by setting various HTTP headers. It's not a silver bullet, but it's a good start.

  • CORS: CORS is a package for providing a middleware that can be used to enable CORS with various options.

    note

    CORS middleware is required for the frontend to communicate with the backend.

  • Morgan: HTTP request logger middleware.

  • express.json (which uses body-parser): parses incoming request bodies in a middleware before your handlers, making the result available under the req.body property.

    note

    JSON middlware is required for Operations to function properly.

  • express.urlencoded (which uses body-parser): returns middleware that only parses urlencoded bodies and only looks at requests where the Content-Type header matches the type option.

  • cookieParser: parses Cookie header and populates req.cookies with an object keyed by the cookie names.

Customization

You have three places where you can customize middleware:

  1. global: here, any changes will apply by default to all operations (query and action) and api. This is helpful if you wanted to add support for multiple domains to CORS, for example.

    Modifying global middleware

    Please treat modifications to global middleware with extreme care as they will affect all operations and APIs. If you are unsure, use one of the other two options.

  2. per-api: you can override middleware for a specific api route (e.g. POST /webhook/callback). This is helpful if you want to disable JSON parsing for some callback, for example.

  3. per-path: this is helpful if you need to customize middleware for all methods under a given path.

    • It's helpful for things like "complex CORS requests" which may need to apply to both OPTIONS and GET, or to apply some middleware to a set of api routes.

Default Middleware Definitions

Below is the actual definitions of default middleware which you can override.

const defaultGlobalMiddleware = new Map([
['helmet', helmet()],
['cors', cors({ origin: config.allowedCORSOrigins })],
['logger', logger('dev')],
['express.json', express.json()],
['express.urlencoded', express.urlencoded({ extended: false })],
['cookieParser', cookieParser()]
])

1. Customize Global Middleware

If you would like to modify the middleware for all operations and APIs, you can do something like:

main.wasp
app todoApp {
// ...

server: {
setupFn: import setup from "@server/serverSetup.js",
middlewareConfigFn: import { serverMiddlewareFn } from "@server/serverSetup.js"
},
}
src/server/serverSetup.js
import cors from 'cors'
import config from '@wasp/config.js'

export const serverMiddlewareFn = (middlewareConfig) => {
// Example of adding extra domains to CORS.
middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))
return middlewareConfig
}

2. Customize api-specific Middleware

If you would like to modify the middleware for a single API, you can do something like:

main.wasp
// ...

api webhookCallback {
fn: import { webhookCallback } from "@server/apis.js",
middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js",
httpRoute: (POST, "/webhook/callback"),
auth: false
}
src/server/apis.js
import express from 'express'

export const webhookCallback = (req, res, _context) => {
res.json({ msg: req.body.length })
}

export const webhookCallbackMiddlewareFn = (middlewareConfig) => {
console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')

middlewareConfig.delete('express.json')
middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))

return middlewareConfig
}

note

This gets installed on a per-method basis. Behind the scenes, this results in code like:

router.post('/webhook/callback', webhookCallbackMiddleware, ...)

3. Customize Per-Path Middleware

If you would like to modify the middleware for all API routes under some common path, you can define a middlewareConfigFn on an apiNamespace:

main.wasp
// ...

apiNamespace fooBar {
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
path: "/foo/bar"
}
src/server/apis.js
export const fooBarNamespaceMiddlewareFn = (middlewareConfig) => {
const customMiddleware = (_req, _res, next) => {
console.log('fooBarNamespaceMiddlewareFn: custom middleware')
next()
}

middlewareConfig.set('custom.middleware', customMiddleware)

return middlewareConfig
}
note

This gets installed at the router level for the path. Behind the scenes, this results in something like:

router.use('/foo/bar', fooBarNamespaceMiddleware)
- - +

Configuring Middleware

Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-api/path basis.

Default Global Middleware 🌍

Wasp's Express server has the following middleware by default:

  • Helmet: Helmet helps you secure your Express apps by setting various HTTP headers. It's not a silver bullet, but it's a good start.

  • CORS: CORS is a package for providing a middleware that can be used to enable CORS with various options.

    note

    CORS middleware is required for the frontend to communicate with the backend.

  • Morgan: HTTP request logger middleware.

  • express.json (which uses body-parser): parses incoming request bodies in a middleware before your handlers, making the result available under the req.body property.

    note

    JSON middlware is required for Operations to function properly.

  • express.urlencoded (which uses body-parser): returns middleware that only parses urlencoded bodies and only looks at requests where the Content-Type header matches the type option.

  • cookieParser: parses Cookie header and populates req.cookies with an object keyed by the cookie names.

Customization

You have three places where you can customize middleware:

  1. global: here, any changes will apply by default to all operations (query and action) and api. This is helpful if you wanted to add support for multiple domains to CORS, for example.

    Modifying global middleware

    Please treat modifications to global middleware with extreme care as they will affect all operations and APIs. If you are unsure, use one of the other two options.

  2. per-api: you can override middleware for a specific api route (e.g. POST /webhook/callback). This is helpful if you want to disable JSON parsing for some callback, for example.

  3. per-path: this is helpful if you need to customize middleware for all methods under a given path.

    • It's helpful for things like "complex CORS requests" which may need to apply to both OPTIONS and GET, or to apply some middleware to a set of api routes.

Default Middleware Definitions

Below is the actual definitions of default middleware which you can override.

const defaultGlobalMiddleware = new Map([
['helmet', helmet()],
['cors', cors({ origin: config.allowedCORSOrigins })],
['logger', logger('dev')],
['express.json', express.json()],
['express.urlencoded', express.urlencoded({ extended: false })],
['cookieParser', cookieParser()]
])

1. Customize Global Middleware

If you would like to modify the middleware for all operations and APIs, you can do something like:

main.wasp
app todoApp {
// ...

server: {
setupFn: import setup from "@server/serverSetup.js",
middlewareConfigFn: import { serverMiddlewareFn } from "@server/serverSetup.js"
},
}
src/server/serverSetup.js
import cors from 'cors'
import config from '@wasp/config.js'

export const serverMiddlewareFn = (middlewareConfig) => {
// Example of adding extra domains to CORS.
middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))
return middlewareConfig
}

2. Customize api-specific Middleware

If you would like to modify the middleware for a single API, you can do something like:

main.wasp
// ...

api webhookCallback {
fn: import { webhookCallback } from "@server/apis.js",
middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js",
httpRoute: (POST, "/webhook/callback"),
auth: false
}
src/server/apis.js
import express from 'express'

export const webhookCallback = (req, res, _context) => {
res.json({ msg: req.body.length })
}

export const webhookCallbackMiddlewareFn = (middlewareConfig) => {
console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')

middlewareConfig.delete('express.json')
middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))

return middlewareConfig
}

note

This gets installed on a per-method basis. Behind the scenes, this results in code like:

router.post('/webhook/callback', webhookCallbackMiddleware, ...)

3. Customize Per-Path Middleware

If you would like to modify the middleware for all API routes under some common path, you can define a middlewareConfigFn on an apiNamespace:

main.wasp
// ...

apiNamespace fooBar {
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
path: "/foo/bar"
}
src/server/apis.js
export const fooBarNamespaceMiddlewareFn = (middlewareConfig) => {
const customMiddleware = (_req, _res, next) => {
console.log('fooBarNamespaceMiddlewareFn: custom middleware')
next()
}

middlewareConfig.set('custom.middleware', customMiddleware)

return middlewareConfig
}
note

This gets installed at the router level for the path. Behind the scenes, this results in something like:

router.use('/foo/bar', fooBarNamespaceMiddleware)
+ + \ No newline at end of file diff --git a/docs/advanced/web-sockets.html b/docs/advanced/web-sockets.html index 6fa5cbc1e3..36387d628a 100644 --- a/docs/advanced/web-sockets.html +++ b/docs/advanced/web-sockets.html @@ -19,13 +19,13 @@ - - + +
-

Web Sockets

Wasp provides a fully integrated WebSocket experience by utilizing Socket.IO on the client and server.

We handle making sure your URLs are correctly setup, CORS is enabled, and provide a useful useSocket and useSocketListener abstractions for use in React components.

To get started, you need to:

  1. Define your WebSocket logic on the server.
  2. Enable WebSockets in your Wasp file, and connect it with your server logic.
  3. Use WebSockets on the client, in React, via useSocket and useSocketListener.
  4. Optionally, type the WebSocket events and payloads for full-stack type safety.

Let's go through setting up WebSockets step by step, starting with enabling WebSockets in your Wasp file.

Turn On WebSockets in Your Wasp File

We specify that we are using WebSockets by adding webSocket to our app and providing the required fn. You can optionally change the auto-connect behavior.

todoApp.wasp
app todoApp {
// ...

webSocket: {
fn: import { webSocketFn } from "@server/webSocket.js",
autoConnect: true, // optional, default: true
},
}

Defining the Events Handler

Let's define the WebSockets server with all of the events and handler functions.

webSocketFn Function

On the server, you will get Socket.IO io: Server argument and context for your WebSocket function. The context object give you access to all of the entities from your Wasp app.

You can use this io object to register callbacks for all the regular Socket.IO events. Also, if a user is logged in, you will have a socket.data.user on the server.

This is how we can define our webSocketFn function:

src/server/webSocket.js
import { v4 as uuidv4 } from 'uuid'

export const webSocketFn = (io, context) => {
io.on('connection', (socket) => {
const username = socket.data.user?.email || socket.data.user?.username || 'unknown'
console.log('a user connected: ', username)

socket.on('chatMessage', async (msg) => {
console.log('message: ', msg)
io.emit('chatMessage', { id: uuidv4(), username, text: msg })
// You can also use your entities here:
// await context.entities.SomeEntity.create({ someField: msg })
})
})
}

Using the WebSocket On The Client

useSocket Hook

Client access to WebSockets is provided by the useSocket hook. It returns:

  • socket: Socket for sending and receiving events.
  • isConnected: boolean for showing a display of the Socket.IO connection status.
    • Note: Wasp automatically connects and establishes a WebSocket connection from the client to the server by default, so you do not need to explicitly socket.connect() or socket.disconnect().
    • If you set autoConnect: false in your Wasp file, then you should call these as needed.

All components using useSocket share the same underlying socket.

useSocketListener Hook

Additionally, there is a useSocketListener: (event, callback) => void hook which is used for registering event handlers. It takes care of unregistering the handler on unmount.

src/client/ChatPage.jsx
import React, { useState } from 'react'
import {
useSocket,
useSocketListener,
} from '@wasp/webSocket'

export const ChatPage = () => {
const [messageText, setMessageText] = useState('')
const [messages, setMessages] = useState([])
const { socket, isConnected } = useSocket()

useSocketListener('chatMessage', logMessage)

function logMessage(msg) {
setMessages((priorMessages) => [msg, ...priorMessages])
}

function handleSubmit(e) {
e.preventDefault()
socket.emit('chatMessage', messageText)
setMessageText('')
}

const messageList = messages.map((msg) => (
<li key={msg.id}>
<em>{msg.username}</em>: {msg.text}
</li>
))
const connectionIcon = isConnected ? '🟢' : '🔴'

return (
<>
<h2>Chat {connectionIcon}</h2>
<div>
<form onSubmit={handleSubmit}>
<div>
<div>
<input
type="text"
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
/>
</div>
<div>
<button type="submit">Submit</button>
</div>
</div>
</form>
<ul>{messageList}</ul>
</div>
</>
)
}

API Reference

todoApp.wasp
app todoApp {
// ...

webSocket: {
fn: import { webSocketFn } from "@server/webSocket.js",
autoConnect: true, // optional, default: true
},
}

The webSocket dict has the following fields:

  • fn: WebSocketFn required

    The function that defines the WebSocket events and handlers.

  • autoConnect: bool

    Whether to automatically connect to the WebSocket server. Default: true.

- - +

Web Sockets

Wasp provides a fully integrated WebSocket experience by utilizing Socket.IO on the client and server.

We handle making sure your URLs are correctly setup, CORS is enabled, and provide a useful useSocket and useSocketListener abstractions for use in React components.

To get started, you need to:

  1. Define your WebSocket logic on the server.
  2. Enable WebSockets in your Wasp file, and connect it with your server logic.
  3. Use WebSockets on the client, in React, via useSocket and useSocketListener.
  4. Optionally, type the WebSocket events and payloads for full-stack type safety.

Let's go through setting up WebSockets step by step, starting with enabling WebSockets in your Wasp file.

Turn On WebSockets in Your Wasp File

We specify that we are using WebSockets by adding webSocket to our app and providing the required fn. You can optionally change the auto-connect behavior.

todoApp.wasp
app todoApp {
// ...

webSocket: {
fn: import { webSocketFn } from "@server/webSocket.js",
autoConnect: true, // optional, default: true
},
}

Defining the Events Handler

Let's define the WebSockets server with all of the events and handler functions.

webSocketFn Function

On the server, you will get Socket.IO io: Server argument and context for your WebSocket function. The context object give you access to all of the entities from your Wasp app.

You can use this io object to register callbacks for all the regular Socket.IO events. Also, if a user is logged in, you will have a socket.data.user on the server.

This is how we can define our webSocketFn function:

src/server/webSocket.js
import { v4 as uuidv4 } from 'uuid'

export const webSocketFn = (io, context) => {
io.on('connection', (socket) => {
const username = socket.data.user?.email || socket.data.user?.username || 'unknown'
console.log('a user connected: ', username)

socket.on('chatMessage', async (msg) => {
console.log('message: ', msg)
io.emit('chatMessage', { id: uuidv4(), username, text: msg })
// You can also use your entities here:
// await context.entities.SomeEntity.create({ someField: msg })
})
})
}

Using the WebSocket On The Client

useSocket Hook

Client access to WebSockets is provided by the useSocket hook. It returns:

  • socket: Socket for sending and receiving events.
  • isConnected: boolean for showing a display of the Socket.IO connection status.
    • Note: Wasp automatically connects and establishes a WebSocket connection from the client to the server by default, so you do not need to explicitly socket.connect() or socket.disconnect().
    • If you set autoConnect: false in your Wasp file, then you should call these as needed.

All components using useSocket share the same underlying socket.

useSocketListener Hook

Additionally, there is a useSocketListener: (event, callback) => void hook which is used for registering event handlers. It takes care of unregistering the handler on unmount.

src/client/ChatPage.jsx
import React, { useState } from 'react'
import {
useSocket,
useSocketListener,
} from '@wasp/webSocket'

export const ChatPage = () => {
const [messageText, setMessageText] = useState('')
const [messages, setMessages] = useState([])
const { socket, isConnected } = useSocket()

useSocketListener('chatMessage', logMessage)

function logMessage(msg) {
setMessages((priorMessages) => [msg, ...priorMessages])
}

function handleSubmit(e) {
e.preventDefault()
socket.emit('chatMessage', messageText)
setMessageText('')
}

const messageList = messages.map((msg) => (
<li key={msg.id}>
<em>{msg.username}</em>: {msg.text}
</li>
))
const connectionIcon = isConnected ? '🟢' : '🔴'

return (
<>
<h2>Chat {connectionIcon}</h2>
<div>
<form onSubmit={handleSubmit}>
<div>
<div>
<input
type="text"
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
/>
</div>
<div>
<button type="submit">Submit</button>
</div>
</div>
</form>
<ul>{messageList}</ul>
</div>
</>
)
}

API Reference

todoApp.wasp
app todoApp {
// ...

webSocket: {
fn: import { webSocketFn } from "@server/webSocket.js",
autoConnect: true, // optional, default: true
},
}

The webSocket dict has the following fields:

  • fn: WebSocketFn required

    The function that defines the WebSocket events and handlers.

  • autoConnect: bool

    Whether to automatically connect to the WebSocket server. Default: true.

+ + \ No newline at end of file diff --git a/docs/auth/email.html b/docs/auth/email.html index 6e89a5bf87..7d8aaf2b50 100644 --- a/docs/auth/email.html +++ b/docs/auth/email.html @@ -19,13 +19,13 @@ - - + +
-

Email

Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows.

Auth UI

Using email auth and social auth together

If a user signs up with Google or Github (and you set it up to save their social provider e-mail info on the User entity), they'll be able to reset their password and login with e-mail and password ✅

If a user signs up with the e-mail and password and then tries to login with a social provider (Google or Github), they won't be able to do that ❌

In the future, we will lift this limitation and enable smarter merging of accounts.

Setting Up Email Authentication

We'll need to take the following steps to set up email authentication:

  1. Enable email authentication in the Wasp file
  2. Add the user entity
  3. Add the routes and pages
  4. Use Auth UI components in our pages
  5. Set up the email sender

Structure of the main.wasp file we will end up with:

main.wasp
// Configuring e-mail authentication
app myApp {
auth: { ... }
}

// Defining User entity
entity User { ... }

// Defining routes and pages
route SignupRoute { ... }
page SignupPage { ... }
// ...

1. Enable Email Authentication in main.wasp

Let's start with adding the following to our main.wasp file:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
// 1. Specify the user entity (we'll define it next)
userEntity: User,
methods: {
// 2. Enable email authentication
email: {
// 3. Specify the email from field
fromField: {
name: "My App Postman",
email: "hello@itsme.com"
},
// 4. Specify the email verification and password reset options (we'll talk about them later)
emailVerification: {
clientRoute: EmailVerificationRoute,
},
passwordReset: {
clientRoute: PasswordResetRoute,
},
allowUnverifiedLogin: false,
},
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
},
}

Read more about the email auth method options here.

2. Add the User Entity

When email authentication is enabled, Wasp expects certain fields in your userEntity. Let's add these fields to our main.wasp file:

main.wasp
// 5. Define the user entity
entity User {=psl
id Int @id @default(autoincrement())
email String? @unique
password String?
isEmailVerified Boolean @default(false)
emailVerificationSentAt DateTime?
passwordResetSentAt DateTime?
// Add your own fields below
// ...
psl=}

Read more about the userEntity fields here.

3. Add the Routes and Pages

Next, we need to define the routes and pages for the authentication pages.

Add the following to the main.wasp file:

main.wasp
// ...

// 6. Define the routes
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@client/pages/auth.jsx"
}

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@client/pages/auth.jsx"
}

route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
page RequestPasswordResetPage {
component: import { RequestPasswordReset } from "@client/pages/auth.jsx",
}

route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
page PasswordResetPage {
component: import { PasswordReset } from "@client/pages/auth.jsx",
}

route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
page EmailVerificationPage {
component: import { EmailVerification } from "@client/pages/auth.jsx",
}

We'll define the React components for these pages in the client/pages/auth.tsx file below.

4. Create the Client Pages

info

We are using Tailwind CSS to style the pages. Read more about how to add it here.

Let's create a auth.tsx file in the client/pages folder and add the following to it:

client/pages/auth.jsx
import { LoginForm } from "@wasp/auth/forms/Login";
import { SignupForm } from "@wasp/auth/forms/Signup";
import { VerifyEmailForm } from "@wasp/auth/forms/VerifyEmail";
import { ForgotPasswordForm } from "@wasp/auth/forms/ForgotPassword";
import { ResetPasswordForm } from "@wasp/auth/forms/ResetPassword";
import { Link } from "react-router-dom";

export function Login() {
return (
<Layout>
<LoginForm />
<br />
<span className="text-sm font-medium text-gray-900">
Don't have an account yet? <Link to="/signup">go to signup</Link>.
</span>
<br />
<span className="text-sm font-medium text-gray-900">
Forgot your password? <Link to="/request-password-reset">reset it</Link>
.
</span>
</Layout>
);
}

export function Signup() {
return (
<Layout>
<SignupForm />
<br />
<span className="text-sm font-medium text-gray-900">
I already have an account (<Link to="/login">go to login</Link>).
</span>
</Layout>
);
}

export function EmailVerification() {
return (
<Layout>
<VerifyEmailForm />
<br />
<span className="text-sm font-medium text-gray-900">
If everything is okay, <Link to="/login">go to login</Link>
</span>
</Layout>
);
}

export function RequestPasswordReset() {
return (
<Layout>
<ForgotPasswordForm />
</Layout>
);
}

export function PasswordReset() {
return (
<Layout>
<ResetPasswordForm />
<br />
<span className="text-sm font-medium text-gray-900">
If everything is okay, <Link to="/login">go to login</Link>
</span>
</Layout>
);
}

// A layout component to center the content
export function Layout({ children }) {
return (
<div className="w-full h-full bg-white">
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
<div className="w-full h-full max-w-sm p-5 bg-white">
<div>{children}</div>
</div>
</div>
</div>
);
}

We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

5. Set up an Email Sender

To support e-mail verification and password reset flows, we need an e-mail sender. Luckily, Wasp supports several email providers out of the box.

We'll use SendGrid in this guide to send our e-mails. You can use any of the supported email providers.

To set up SendGrid to send emails, we will add the following to our main.wasp file:

main.wasp
app myApp {
// ...
// 7. Set up the email sender
emailSender: {
provider: SendGrid,
}
}

... and add the following to our .env.server file:

.env.server
SENDGRID_API_KEY=<your key>

If you are not sure how to get a SendGrid API key, read more here.

Read more about setting up email senders in the sending emails docs.

Conclusion

That's it! We have set up email authentication in our app. 🎉

Running wasp db migrate-dev and then wasp start should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the using auth docs.

Login and Signup Flows

Login

Auth UI

If logging in with an unverified email is allowed, the user will be able to login with an unverified email address. If logging in with an unverified email is not allowed, the user will be shown an error message.

Read more about the allowUnverifiedLogin option here.

Signup

Auth UI

Some of the behavior you get out of the box:

  1. Rate limiting

    We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

  2. Preventing user email leaks

    If somebody tries to signup with an email that already exists and it's verified, we pretend that the account was created instead of saying it's an existing account. This is done to prevent leaking the user's email address.

  3. Allowing registration for unverified emails

    If a user tries to register with an existing but unverified email, we'll allow them to do that. This is done to prevent bad actors from locking out other users from registering with their email address.

  4. Password validation

    Read more about the default password validation rules and how to override them in using auth docs.

Email Verification Flow

By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address.

Our setup looks like this:

main.wasp
// ...

emailVerification: {
clientRoute: EmailVerificationRoute,
}

When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the EmailVerificationRoute route we defined in the main.wasp file.

The content of the e-mail can be customized, read more about it here.

Email Verification Page

We defined our email verification page in the auth.tsx file.

Auth UI

Password Reset Flow

Users can request a password and then they'll receive an e-mail with a link to reset their password.

Some of the behavior you get out of the box:

  1. Rate limiting

    We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

  2. Preventing user email leaks

    If somebody requests a password reset with an unknown email address, we'll give back the same response as if the user requested a password reset successfully. This is done to prevent leaking information.

Our setup in main.wasp looks like this:

main.wasp
// ...

passwordReset: {
clientRoute: PasswordResetRoute,
}

Request Password Reset Page

Users request their password to be reset by going to the /request-password-reset route. We defined our request password reset page in the auth.tsx file.

Request password reset page

Password Reset Page

When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the PasswordResetRoute route we defined in the main.wasp file.

Request password reset page

Users can enter their new password there.

The content of the e-mail can be customized, read more about it here.

Using The Auth

To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the using auth docs.

API Reference

Let's go over the options we can specify when using email authentication.

userEntity fields

main.wasp
app myApp {
title: "My app",
// ...

auth: {
userEntity: User,
methods: {
email: {
// We'll explain these options below
},
},
onAuthFailedRedirectTo: "/someRoute"
},
// ...
}

// Using email auth requires the `userEntity` to have at least the following fields
entity User {=psl
id Int @id @default(autoincrement())
email String? @unique
password String?
isEmailVerified Boolean @default(false)
emailVerificationSentAt DateTime?
passwordResetSentAt DateTime?
psl=}

Email auth requires that userEntity specified in auth contains:

  • optional email field of type String
  • optional password field of type String
  • isEmailVerified field of type Boolean with a default value of false
  • optional emailVerificationSentAt field of type DateTime
  • optional passwordResetSentAt field of type DateTime

Fields in the email dict

main.wasp
app myApp {
title: "My app",
// ...

auth: {
userEntity: User,
methods: {
email: {
fromField: {
name: "My App",
email: "hello@itsme.com"
},
emailVerification: {
clientRoute: EmailVerificationRoute,
getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
},
passwordReset: {
clientRoute: PasswordResetRoute,
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
},
allowUnverifiedLogin: false,
},
},
onAuthFailedRedirectTo: "/someRoute"
},
// ...
}

fromField: EmailFromField required

fromField is a dict that specifies the name and e-mail address of the sender of the e-mails sent by your app.

It has the following fields:

  • name: name of the sender
  • email: e-mail address of the sender required

emailVerification: EmailVerificationConfig required

emailVerification is a dict that specifies the details of the e-mail verification process.

It has the following fields:

  • clientRoute: Route: a route that is used for the user to verify their e-mail address. required

    Client route should handle the process of taking a token from the URL and sending it to the server to verify the e-mail address. You can use our verifyEmail action for that.

    src/pages/EmailVerificationPage.jsx
    import { verifyEmail } from '@wasp/auth/email/actions';
    ...
    await verifyEmail({ token });
    note

    We used Auth UI above to avoid doing this work of sending the token to the server manually.

  • getEmailContentFn: ServerImport: a function that returns the content of the e-mail that is sent to the user.

    Defining getEmailContentFn can be done by defining a file in the server directory.

    server/email.js
    export const getVerificationEmailContent = ({ verificationLink }) => ({
    subject: 'Verify your email',
    text: `Click the link below to verify your email: ${verificationLink}`,
    html: `
    <p>Click the link below to verify your email</p>
    <a href="${verificationLink}">Verify email</a>
    `,
    })
    This is the default content of the e-mail, you can customize it to your liking.

passwordReset: PasswordResetConfig required

passwordReset is a dict that specifies the password reset process.

It has the following fields:

  • clientRoute: Route: a route that is used for the user to reset their password. required

    Client route should handle the process of taking a token from the URL and a new password from the user and sending it to the server. You can use our requestPasswordReset and resetPassword actions to do that.

    src/pages/ForgotPasswordPage.jsx
    import { requestPasswordReset } from '@wasp/auth/email/actions';
    ...
    await requestPasswordReset({ email });
    src/pages/PasswordResetPage.jsx
    import { resetPassword } from '@wasp/auth/email/actions';
    ...
    await resetPassword({ password, token })
    note

    We used Auth UI above to avoid doing this work of sending the password request and the new password to the server manually.

  • getEmailContentFn: ServerImport: a function that returns the content of the e-mail that is sent to the user.

    Defining getEmailContentFn is done by defining a function that looks like this:

    server/email.js
    export const getPasswordResetEmailContent = ({ passwordResetLink }) => ({
    subject: 'Password reset',
    text: `Click the link below to reset your password: ${passwordResetLink}`,
    html: `
    <p>Click the link below to reset your password</p>
    <a href="${passwordResetLink}">Reset password</a>
    `,
    })
    This is the default content of the e-mail, you can customize it to your liking.

allowUnverifiedLogin: bool: specifies whether the user can login without verifying their e-mail address

It defaults to false. If allowUnverifiedLogin is set to true, the user can login without verifying their e-mail address, otherwise users will receive a 401 error when trying to login without verifying their e-mail address.

Sometimes you want to allow unverified users to login to provide them a different onboarding experience. Some of the pages can be viewed without verifying the e-mail address, but some of them can't. You can use the isEmailVerified field on the user entity to check if the user has verified their e-mail address.

If you have any questions, feel free to ask them on our Discord server.

- - +

Email

Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows.

Auth UI

Using email auth and social auth together

If a user signs up with Google or Github (and you set it up to save their social provider e-mail info on the User entity), they'll be able to reset their password and login with e-mail and password ✅

If a user signs up with the e-mail and password and then tries to login with a social provider (Google or Github), they won't be able to do that ❌

In the future, we will lift this limitation and enable smarter merging of accounts.

Setting Up Email Authentication

We'll need to take the following steps to set up email authentication:

  1. Enable email authentication in the Wasp file
  2. Add the user entity
  3. Add the routes and pages
  4. Use Auth UI components in our pages
  5. Set up the email sender

Structure of the main.wasp file we will end up with:

main.wasp
// Configuring e-mail authentication
app myApp {
auth: { ... }
}

// Defining User entity
entity User { ... }

// Defining routes and pages
route SignupRoute { ... }
page SignupPage { ... }
// ...

1. Enable Email Authentication in main.wasp

Let's start with adding the following to our main.wasp file:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
// 1. Specify the user entity (we'll define it next)
userEntity: User,
methods: {
// 2. Enable email authentication
email: {
// 3. Specify the email from field
fromField: {
name: "My App Postman",
email: "hello@itsme.com"
},
// 4. Specify the email verification and password reset options (we'll talk about them later)
emailVerification: {
clientRoute: EmailVerificationRoute,
},
passwordReset: {
clientRoute: PasswordResetRoute,
},
allowUnverifiedLogin: false,
},
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
},
}

Read more about the email auth method options here.

2. Add the User Entity

When email authentication is enabled, Wasp expects certain fields in your userEntity. Let's add these fields to our main.wasp file:

main.wasp
// 5. Define the user entity
entity User {=psl
id Int @id @default(autoincrement())
email String? @unique
password String?
isEmailVerified Boolean @default(false)
emailVerificationSentAt DateTime?
passwordResetSentAt DateTime?
// Add your own fields below
// ...
psl=}

Read more about the userEntity fields here.

3. Add the Routes and Pages

Next, we need to define the routes and pages for the authentication pages.

Add the following to the main.wasp file:

main.wasp
// ...

// 6. Define the routes
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@client/pages/auth.jsx"
}

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@client/pages/auth.jsx"
}

route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
page RequestPasswordResetPage {
component: import { RequestPasswordReset } from "@client/pages/auth.jsx",
}

route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
page PasswordResetPage {
component: import { PasswordReset } from "@client/pages/auth.jsx",
}

route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
page EmailVerificationPage {
component: import { EmailVerification } from "@client/pages/auth.jsx",
}

We'll define the React components for these pages in the client/pages/auth.tsx file below.

4. Create the Client Pages

info

We are using Tailwind CSS to style the pages. Read more about how to add it here.

Let's create a auth.tsx file in the client/pages folder and add the following to it:

client/pages/auth.jsx
import { LoginForm } from "@wasp/auth/forms/Login";
import { SignupForm } from "@wasp/auth/forms/Signup";
import { VerifyEmailForm } from "@wasp/auth/forms/VerifyEmail";
import { ForgotPasswordForm } from "@wasp/auth/forms/ForgotPassword";
import { ResetPasswordForm } from "@wasp/auth/forms/ResetPassword";
import { Link } from "react-router-dom";

export function Login() {
return (
<Layout>
<LoginForm />
<br />
<span className="text-sm font-medium text-gray-900">
Don't have an account yet? <Link to="/signup">go to signup</Link>.
</span>
<br />
<span className="text-sm font-medium text-gray-900">
Forgot your password? <Link to="/request-password-reset">reset it</Link>
.
</span>
</Layout>
);
}

export function Signup() {
return (
<Layout>
<SignupForm />
<br />
<span className="text-sm font-medium text-gray-900">
I already have an account (<Link to="/login">go to login</Link>).
</span>
</Layout>
);
}

export function EmailVerification() {
return (
<Layout>
<VerifyEmailForm />
<br />
<span className="text-sm font-medium text-gray-900">
If everything is okay, <Link to="/login">go to login</Link>
</span>
</Layout>
);
}

export function RequestPasswordReset() {
return (
<Layout>
<ForgotPasswordForm />
</Layout>
);
}

export function PasswordReset() {
return (
<Layout>
<ResetPasswordForm />
<br />
<span className="text-sm font-medium text-gray-900">
If everything is okay, <Link to="/login">go to login</Link>
</span>
</Layout>
);
}

// A layout component to center the content
export function Layout({ children }) {
return (
<div className="w-full h-full bg-white">
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
<div className="w-full h-full max-w-sm p-5 bg-white">
<div>{children}</div>
</div>
</div>
</div>
);
}

We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

5. Set up an Email Sender

To support e-mail verification and password reset flows, we need an e-mail sender. Luckily, Wasp supports several email providers out of the box.

We'll use SendGrid in this guide to send our e-mails. You can use any of the supported email providers.

To set up SendGrid to send emails, we will add the following to our main.wasp file:

main.wasp
app myApp {
// ...
// 7. Set up the email sender
emailSender: {
provider: SendGrid,
}
}

... and add the following to our .env.server file:

.env.server
SENDGRID_API_KEY=<your key>

If you are not sure how to get a SendGrid API key, read more here.

Read more about setting up email senders in the sending emails docs.

Conclusion

That's it! We have set up email authentication in our app. 🎉

Running wasp db migrate-dev and then wasp start should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the using auth docs.

Login and Signup Flows

Login

Auth UI

If logging in with an unverified email is allowed, the user will be able to login with an unverified email address. If logging in with an unverified email is not allowed, the user will be shown an error message.

Read more about the allowUnverifiedLogin option here.

Signup

Auth UI

Some of the behavior you get out of the box:

  1. Rate limiting

    We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

  2. Preventing user email leaks

    If somebody tries to signup with an email that already exists and it's verified, we pretend that the account was created instead of saying it's an existing account. This is done to prevent leaking the user's email address.

  3. Allowing registration for unverified emails

    If a user tries to register with an existing but unverified email, we'll allow them to do that. This is done to prevent bad actors from locking out other users from registering with their email address.

  4. Password validation

    Read more about the default password validation rules and how to override them in using auth docs.

Email Verification Flow

By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address.

Our setup looks like this:

main.wasp
// ...

emailVerification: {
clientRoute: EmailVerificationRoute,
}

When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the EmailVerificationRoute route we defined in the main.wasp file.

The content of the e-mail can be customized, read more about it here.

Email Verification Page

We defined our email verification page in the auth.tsx file.

Auth UI

Password Reset Flow

Users can request a password and then they'll receive an e-mail with a link to reset their password.

Some of the behavior you get out of the box:

  1. Rate limiting

    We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.

  2. Preventing user email leaks

    If somebody requests a password reset with an unknown email address, we'll give back the same response as if the user requested a password reset successfully. This is done to prevent leaking information.

Our setup in main.wasp looks like this:

main.wasp
// ...

passwordReset: {
clientRoute: PasswordResetRoute,
}

Request Password Reset Page

Users request their password to be reset by going to the /request-password-reset route. We defined our request password reset page in the auth.tsx file.

Request password reset page

Password Reset Page

When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute field. In our case, this is the PasswordResetRoute route we defined in the main.wasp file.

Request password reset page

Users can enter their new password there.

The content of the e-mail can be customized, read more about it here.

Using The Auth

To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the using auth docs.

API Reference

Let's go over the options we can specify when using email authentication.

userEntity fields

main.wasp
app myApp {
title: "My app",
// ...

auth: {
userEntity: User,
methods: {
email: {
// We'll explain these options below
},
},
onAuthFailedRedirectTo: "/someRoute"
},
// ...
}

// Using email auth requires the `userEntity` to have at least the following fields
entity User {=psl
id Int @id @default(autoincrement())
email String? @unique
password String?
isEmailVerified Boolean @default(false)
emailVerificationSentAt DateTime?
passwordResetSentAt DateTime?
psl=}

Email auth requires that userEntity specified in auth contains:

  • optional email field of type String
  • optional password field of type String
  • isEmailVerified field of type Boolean with a default value of false
  • optional emailVerificationSentAt field of type DateTime
  • optional passwordResetSentAt field of type DateTime

Fields in the email dict

main.wasp
app myApp {
title: "My app",
// ...

auth: {
userEntity: User,
methods: {
email: {
fromField: {
name: "My App",
email: "hello@itsme.com"
},
emailVerification: {
clientRoute: EmailVerificationRoute,
getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
},
passwordReset: {
clientRoute: PasswordResetRoute,
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
},
allowUnverifiedLogin: false,
},
},
onAuthFailedRedirectTo: "/someRoute"
},
// ...
}

fromField: EmailFromField required

fromField is a dict that specifies the name and e-mail address of the sender of the e-mails sent by your app.

It has the following fields:

  • name: name of the sender
  • email: e-mail address of the sender required

emailVerification: EmailVerificationConfig required

emailVerification is a dict that specifies the details of the e-mail verification process.

It has the following fields:

  • clientRoute: Route: a route that is used for the user to verify their e-mail address. required

    Client route should handle the process of taking a token from the URL and sending it to the server to verify the e-mail address. You can use our verifyEmail action for that.

    src/pages/EmailVerificationPage.jsx
    import { verifyEmail } from '@wasp/auth/email/actions';
    ...
    await verifyEmail({ token });
    note

    We used Auth UI above to avoid doing this work of sending the token to the server manually.

  • getEmailContentFn: ServerImport: a function that returns the content of the e-mail that is sent to the user.

    Defining getEmailContentFn can be done by defining a file in the server directory.

    server/email.js
    export const getVerificationEmailContent = ({ verificationLink }) => ({
    subject: 'Verify your email',
    text: `Click the link below to verify your email: ${verificationLink}`,
    html: `
    <p>Click the link below to verify your email</p>
    <a href="${verificationLink}">Verify email</a>
    `,
    })
    This is the default content of the e-mail, you can customize it to your liking.

passwordReset: PasswordResetConfig required

passwordReset is a dict that specifies the password reset process.

It has the following fields:

  • clientRoute: Route: a route that is used for the user to reset their password. required

    Client route should handle the process of taking a token from the URL and a new password from the user and sending it to the server. You can use our requestPasswordReset and resetPassword actions to do that.

    src/pages/ForgotPasswordPage.jsx
    import { requestPasswordReset } from '@wasp/auth/email/actions';
    ...
    await requestPasswordReset({ email });
    src/pages/PasswordResetPage.jsx
    import { resetPassword } from '@wasp/auth/email/actions';
    ...
    await resetPassword({ password, token })
    note

    We used Auth UI above to avoid doing this work of sending the password request and the new password to the server manually.

  • getEmailContentFn: ServerImport: a function that returns the content of the e-mail that is sent to the user.

    Defining getEmailContentFn is done by defining a function that looks like this:

    server/email.js
    export const getPasswordResetEmailContent = ({ passwordResetLink }) => ({
    subject: 'Password reset',
    text: `Click the link below to reset your password: ${passwordResetLink}`,
    html: `
    <p>Click the link below to reset your password</p>
    <a href="${passwordResetLink}">Reset password</a>
    `,
    })
    This is the default content of the e-mail, you can customize it to your liking.

allowUnverifiedLogin: bool: specifies whether the user can login without verifying their e-mail address

It defaults to false. If allowUnverifiedLogin is set to true, the user can login without verifying their e-mail address, otherwise users will receive a 401 error when trying to login without verifying their e-mail address.

Sometimes you want to allow unverified users to login to provide them a different onboarding experience. Some of the pages can be viewed without verifying the e-mail address, but some of them can't. You can use the isEmailVerified field on the user entity to check if the user has verified their e-mail address.

If you have any questions, feel free to ask them on our Discord server.

+ + \ No newline at end of file diff --git a/docs/auth/overview.html b/docs/auth/overview.html index a74bce3eb8..e3163ed3ef 100644 --- a/docs/auth/overview.html +++ b/docs/auth/overview.html @@ -19,17 +19,17 @@ - - + +
-

Using Auth

Auth is an essential piece of any serious application. Coincidentally, Wasp provides authentication and authorization support out of the box 🙃.

Enabling auth for your app is optional and can be done by configuring the auth field of the app declaration.

main.wasp
app MyApp {
title: "My app",
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {}, // use this or email, not both
email: {}, // use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute"
}
}

//...

Read more about the auth field options in the API Reference section.

We will provide a quick overview of auth in Wasp and link to more detailed documentation for each auth method.

Available auth methods

Wasp supports the following auth methods:

Click on each auth method for more details.

Let's say we enabled the Username & password authentication.

We get an auth backend with signup and login endpoints. We also get the user object in our Operations and we can decide what to do based on whether the user is logged in or not.

We would also get the Auth UI generated for us. We can set up our login and signup pages where our users can create their account and login. We can then protect certain pages by setting authRequired: true for them. This will make sure that only logged-in users can access them.

We will also have access to the user object in our frontend code, so we can show different UI to logged-in and logged-out users. For example, we can show the user's name in the header alongside a logout button or a login button if the user is not logged in.

Protecting a page with authRequired

When declaring a page, you can set the authRequired property.

If you set it to true, only authenticated users can access the page. Unauthenticated users are redirected to a route defined by the app.auth.onAuthFailedRedirectTo field.

main.wasp
page MainPage {
component: import Main from "@client/pages/Main.jsx",
authRequired: true
}
Requires auth method

You can only use authRequired if your app uses one of the available auth methods.

If authRequired is set to true, the page's React component (specified by the component property) receives the user object as a prop. Read more about the user object in the Accessing the logged-in user section.

Logout action

We provide an action for logging out the user. Here's how you can use it:

client/components/LogoutButton.jsx
import logout from '@wasp/auth/logout'

const LogoutButton = () => {
return <button onClick={logout}>Logout</button>
}

Accessing the logged-in user

You can get access to the user object both in the backend and on the frontend.

On the client

There are two ways to access the user object on the client:

  • the user prop
  • the useAuth hook

Using the user prop

If the page's declaration sets authRequired to true, the page's React component receives the user object as a prop:

main.wasp
// ...

page AccountPage {
component: import Account from "@client/pages/Account.jsx",
authRequired: true
}
client/pages/Account.jsx
import Button from './Button'
import logout from '@wasp/auth/logout'

const AccountPage = ({ user }) => {
return (
<div>
<Button onClick={logout}>Logout</Button>
{JSON.stringify(user, null, 2)}
</div>
)
}

export default AccountPage

Using the useAuth hook

Wasp provides a React hook you can use in the client components - useAuth.

This hook is a thin wrapper over Wasp's useQuery hook and returns data in the same format.

src/client/pages/MainPage.jsx
import useAuth from '@wasp/auth/useAuth'
import { Link } from 'react-router-dom'
import logout from '@wasp/auth/logout'
import Todo from '../Todo'

export function Main() {
const { data: user } = useAuth()

if (!user) {
return (
<span>
Please <Link to="/login">login</Link> or{' '}
<Link to="/signup">sign up</Link>.
</span>
)
} else {
return (
<>
<button onClick={logout}>Logout</button>
<Todo />
</>
)
}
}
tip

Since the user prop is only available in a page's React component: use the user prop in the page's React component and the useAuth hook in any other React component.

On the server

Using the context.user object

When authentication is enabled, all queries and actions have access to the user object through the context argument. context.user contains all User entity's fields, except for the password.

src/server/actions.js
import HttpError from '@wasp/core/HttpError.js'

export const createTask = async (task, context) => {
if (!context.user) {
throw new HttpError(403)
}

const Task = context.entities.Task
return Task.create({
data: {
description: task.description,
user: {
connect: { id: context.user.id },
},
},
})
}

To implement access control in your app, each operation must check context.user and decide what to do. For example, if context.user is undefined inside a private operation, the user's access should be denied.

When using WebSockets, the user object is also available on the socket.data object. Read more in the WebSockets section.

User entity

Password hashing

You don't need to worry about hashing the password yourself. Even when directly using the Prisma client and calling create() with a plain-text password, Wasp's middleware makes sure to hash the password before storing it in the database. +

Using Auth

Auth is an essential piece of any serious application. Coincidentally, Wasp provides authentication and authorization support out of the box 🙃.

Enabling auth for your app is optional and can be done by configuring the auth field of the app declaration.

main.wasp
app MyApp {
title: "My app",
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {}, // use this or email, not both
email: {}, // use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute"
}
}

//...

Read more about the auth field options in the API Reference section.

We will provide a quick overview of auth in Wasp and link to more detailed documentation for each auth method.

Available auth methods

Wasp supports the following auth methods:

Click on each auth method for more details.

Let's say we enabled the Username & password authentication.

We get an auth backend with signup and login endpoints. We also get the user object in our Operations and we can decide what to do based on whether the user is logged in or not.

We would also get the Auth UI generated for us. We can set up our login and signup pages where our users can create their account and login. We can then protect certain pages by setting authRequired: true for them. This will make sure that only logged-in users can access them.

We will also have access to the user object in our frontend code, so we can show different UI to logged-in and logged-out users. For example, we can show the user's name in the header alongside a logout button or a login button if the user is not logged in.

Protecting a page with authRequired

When declaring a page, you can set the authRequired property.

If you set it to true, only authenticated users can access the page. Unauthenticated users are redirected to a route defined by the app.auth.onAuthFailedRedirectTo field.

main.wasp
page MainPage {
component: import Main from "@client/pages/Main.jsx",
authRequired: true
}
Requires auth method

You can only use authRequired if your app uses one of the available auth methods.

If authRequired is set to true, the page's React component (specified by the component property) receives the user object as a prop. Read more about the user object in the Accessing the logged-in user section.

Logout action

We provide an action for logging out the user. Here's how you can use it:

client/components/LogoutButton.jsx
import logout from '@wasp/auth/logout'

const LogoutButton = () => {
return <button onClick={logout}>Logout</button>
}

Accessing the logged-in user

You can get access to the user object both in the backend and on the frontend.

On the client

There are two ways to access the user object on the client:

  • the user prop
  • the useAuth hook

Using the user prop

If the page's declaration sets authRequired to true, the page's React component receives the user object as a prop:

main.wasp
// ...

page AccountPage {
component: import Account from "@client/pages/Account.jsx",
authRequired: true
}
client/pages/Account.jsx
import Button from './Button'
import logout from '@wasp/auth/logout'

const AccountPage = ({ user }) => {
return (
<div>
<Button onClick={logout}>Logout</Button>
{JSON.stringify(user, null, 2)}
</div>
)
}

export default AccountPage

Using the useAuth hook

Wasp provides a React hook you can use in the client components - useAuth.

This hook is a thin wrapper over Wasp's useQuery hook and returns data in the same format.

src/client/pages/MainPage.jsx
import useAuth from '@wasp/auth/useAuth'
import { Link } from 'react-router-dom'
import logout from '@wasp/auth/logout'
import Todo from '../Todo'

export function Main() {
const { data: user } = useAuth()

if (!user) {
return (
<span>
Please <Link to="/login">login</Link> or{' '}
<Link to="/signup">sign up</Link>.
</span>
)
} else {
return (
<>
<button onClick={logout}>Logout</button>
<Todo />
</>
)
}
}
tip

Since the user prop is only available in a page's React component: use the user prop in the page's React component and the useAuth hook in any other React component.

On the server

Using the context.user object

When authentication is enabled, all queries and actions have access to the user object through the context argument. context.user contains all User entity's fields, except for the password.

src/server/actions.js
import HttpError from '@wasp/core/HttpError.js'

export const createTask = async (task, context) => {
if (!context.user) {
throw new HttpError(403)
}

const Task = context.entities.Task
return Task.create({
data: {
description: task.description,
user: {
connect: { id: context.user.id },
},
},
})
}

To implement access control in your app, each operation must check context.user and decide what to do. For example, if context.user is undefined inside a private operation, the user's access should be denied.

When using WebSockets, the user object is also available on the socket.data object. Read more in the WebSockets section.

User entity

Password hashing

You don't need to worry about hashing the password yourself. Even when directly using the Prisma client and calling create() with a plain-text password, Wasp's middleware makes sure to hash the password before storing it in the database. For example, if you need to update a user's password, you can safely use the Prisma client to do so, e.g., inside an Action:

src/server/actions.js
export const updatePassword = async (args, context) => {
return context.entities.User.update({
where: { id: args.userId },
data: {
password: 'New pwd which will be hashed automatically!',
},
})
}

Default validations

Wasp includes several basic validation mechanisms. If you need something extra, the next section shows how to customize them.

Default validations depend on the auth method you use.

Username & password

If you use Username & password authentication, the default validations are:

  • The username must not be empty
  • The password must not be empty, have at least 8 characters, and contain a number

Note that usernames are stored in a case-sensitive manner.

Email

If you use Email authentication, the default validations are:

  • The email must not be empty and a valid email address
  • The password must not be empty, have at least 8 characters, and contain a number

Note that emails are stored in a case-insensitive manner.

Customizing validations

note

You can only disable the default validation for Username & password authentication, but you can add custom validations can to both Username & password and Email auth methods.

This is a bug in Wasp that is being tracked here

To disable/enable default validations, or add your own, modify your custom signup function:

const newUser = context.entities.User.create({
data: {
username: args.username,
password: args.password, // password hashed automatically by Wasp! 🐝
},
_waspSkipDefaultValidations: false, // can be omitted if false (default), or explicitly set to true
_waspCustomValidations: [
{
validates: 'password',
message: 'password must contain an uppercase letter',
validator: (password) => /[A-Z]/.test(password),
},
],
})
info

Validations always run on create(). For update(), they only run when the field mentioned in validates is present.

The validation process stops on the first validator to return false. If enabled, default validations run first and then custom validations.

Validation Error Handling

When creating, updating, or deleting entities, you may wish to handle validation errors. Wasp exposes a class called AuthError for this purpose.

src/server/actions.js
try {
await context.entities.User.update(...)
} catch (e) {
if (e instanceof AuthError) {
throw new HttpError(422, 'Validation failed', { message: e.message })
} else {
throw e
}
}

Customizing the Signup Process

Sometimes you want to include extra fields in your signup process, like first name and last name.

In Wasp, in this case:

  • you need to define the fields that you want saved in the database,
  • you need to customize the SignupForm.

Other times, you might need to just add some extra UI elements to the form, like a checkbox for terms of service. In this case, customizing only the UI components is enough.

Let's see how to do both.

1. Defining Extra Fields

If we want to save some extra fields in our signup process, we need to tell our app they exist.

We do that by defining an object where the keys represent the field name, and the values are functions that receive the data sent from the client* and return the value of the field.

* We exclude the password field from this object to prevent it from being saved as plain-text in the database. The password field is handled by Wasp's auth backend.

First, we add the auth.signup.additionalFields field in our main.wasp file:

main.wasp
app crudTesting {
// ...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login",
signup: {
additionalFields: import { fields } from "@server/auth/signup.js",
},
},
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
address String?
psl=}

Then we'll define and export the fields object from the server/auth/signup.js file:

server/auth/signup.js
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'

export const fields = defineAdditionalSignupFields({
address: async (data) => {
const address = data.address
if (typeof address !== 'string') {
throw new Error('Address is required')
}
if (address.length < 5) {
throw new Error('Address must be at least 5 characters long')
}
return address
},
})

Read more about the fields object in the API Reference.

Keep in mind, that these field names need to exist on the userEntity you defined in your main.wasp file e.g. address needs to be a field on the User entity.

The field function will receive the data sent from the client and it needs to return the value that will be saved into the database. If the field is invalid, the function should throw an error.

Using Validation Libraries

You can use any validation library you want to validate the fields. For example, you can use zod like this:

Click to see the code
server/auth/signup.js
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'
import * as z from 'zod'

export const fields = defineAdditionalSignupFields({
address: (data) => {
const AddressSchema = z
.string({
required_error: 'Address is required',
invalid_type_error: 'Address must be a string',
})
.min(10, 'Address must be at least 10 characters long')
const result = AddressSchema.safeParse(data.address)
if (result.success === false) {
throw new Error(result.error.issues[0].message)
}
return result.data
},
})

Now that we defined the fields, Wasp knows how to:

  1. Validate the data sent from the client
  2. Save the data to the database

Next, let's see how to customize Auth UI to include those fields.

2. Customizing the Signup Component

Using Custom Signup Component

If you are not using Wasp's Auth UI, you can skip this section. Just make sure to include the extra fields in your custom signup form.

Read more about using the signup actions for:

  • email auth here
  • username & password auth here

If you are using Wasp's Auth UI, you can customize the SignupForm component by passing the additionalFields prop to it. It can be either a list of extra fields or a render function.

Using a List of Extra Fields

When you pass in a list of extra fields to the SignupForm, they are added to the form one by one, in the order you pass them in.

Inside the list, there can be either objects or render functions (you can combine them):

  1. Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.
  2. Render functions can be used to render any UI you want, but they require a bit more code. The render functions receive the react-hook-form object and the form state object as arguments.
client/SignupPage.jsx
import { SignupForm } from '@wasp/auth/forms/Signup'
import {
FormError,
FormInput,
FormItemGroup,
FormLabel,
} from '@wasp/auth/forms/internal/Form'

export const SignupPage = () => {
return (
<SignupForm
additionalFields={[
/* The address field is defined using an object */
{
name: 'address',
label: 'Address',
type: 'input',
validations: {
required: 'Address is required',
},
},
/* The phone number is defined using a render function */
(form, state) => {
return (
<FormItemGroup>
<FormLabel>Phone Number</FormLabel>
<FormInput
{...form.register('phoneNumber', {
required: 'Phone number is required',
})}
disabled={state.isLoading}
/>
{form.formState.errors.phoneNumber && (
<FormError>
{form.formState.errors.phoneNumber.message}
</FormError>
)}
</FormItemGroup>
)
},
]}
/>
)
}

Read more about the extra fields in the API Reference.

Using a Single Render Function

Instead of passing in a list of extra fields, you can pass in a render function which will receive the react-hook-form object and the form state object as arguments. What ever the render function returns, will be rendered below the default fields.

client/SignupPage.jsx
import { SignupForm } from '@wasp/auth/forms/Signup'
import { FormItemGroup } from '@wasp/auth/forms/internal/Form'

export const SignupPage = () => {
return (
<SignupForm
additionalFields={(form, state) => {
const username = form.watch('username')
return (
username && (
<FormItemGroup>
Hello there <strong>{username}</strong> 👋
</FormItemGroup>
)
)
}}
/>
)
}

Read more about the render function in the API Reference.

API Reference

Auth Fields

main.wasp
  title: "My app",
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {}, // use this or email, not both
email: {}, // use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute",
signup: { ... }
}
}

//...

app.auth is a dictionary with the following fields:

userEntity: entity required

The entity representing the user. Its mandatory fields depend on your chosen auth method.

externalAuthEntity: entity

Wasp requires you to set the field auth.externalAuthEntity for all authentication methods relying on an external authorizatino provider (e.g., Google). You also need to tweak the Entity referenced by auth.userEntity, as shown below.

main.wasp
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
//...

entity User {=psl
id Int @id @default(autoincrement())
//...
externalAuthAssociations SocialLogin[]
psl=}

entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}
note

The same externalAuthEntity can be used across different social login providers (e.g., both GitHub and Google can use the same entity).

See Google docs and GitHub docs for more details.

methods: dict required

A dictionary of auth methods enabled for the app.

Click on each auth method for more details.

onAuthFailedRedirectTo: String required

The route to which Wasp should redirect unauthenticated user when they try to access a private page (i.e., a page that has authRequired: true). Check out these essentials docs on auth to see an example of usage.

onAuthSucceededRedirectTo: String

The route to which Wasp will send a successfully authenticated after a successful login/signup. The default value is "/".

note

Automatic redirect on successful login only works when using the Wasp-provided Auth UI.

signup: SignupOptions

Read more about the signup process customization API in the Signup Fields Customization section.

Signup Fields Customization

If you want to add extra fields to the signup process, the server needs to know how to save them to the database. You do that by defining the auth.signup.additionalFields field in your main.wasp file.

main.wasp
app crudTesting {
// ...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login",
signup: {
additionalFields: import { fields } from "@server/auth/signup.js",
},
},
}

Then we'll export the fields object from the server/auth/signup.js file:

server/auth/signup.js
import { defineAdditionalSignupFields } from '@wasp/auth/index.js'

export const fields = defineAdditionalSignupFields({
address: async (data) => {
const address = data.address
if (typeof address !== 'string') {
throw new Error('Address is required')
}
if (address.length < 5) {
throw new Error('Address must be at least 5 characters long')
}
return address
},
})

The fields object is an object where the keys represent the field name, and the values are functions which receive the data sent from the client* and return the value of the field.

If the field value is invalid, the function should throw an error.

* We exclude the password field from this object to prevent it from being saved as plain-text in the database. The password field is handled by Wasp's auth backend.

SignupForm Customization

To customize the SignupForm component, you need to pass in the additionalFields prop. It can be either a list of extra fields or a render function.

client/SignupPage.jsx
import { SignupForm } from '@wasp/auth/forms/Signup'
import {
FormError,
FormInput,
FormItemGroup,
FormLabel,
} from '@wasp/auth/forms/internal/Form'

export const SignupPage = () => {
return (
<SignupForm
additionalFields={[
{
name: 'address',
label: 'Address',
type: 'input',
validations: {
required: 'Address is required',
},
},
(form, state) => {
return (
<FormItemGroup>
<FormLabel>Phone Number</FormLabel>
<FormInput
{...form.register('phoneNumber', {
required: 'Phone number is required',
})}
disabled={state.isLoading}
/>
{form.formState.errors.phoneNumber && (
<FormError>
{form.formState.errors.phoneNumber.message}
</FormError>
)}
</FormItemGroup>
)
},
]}
/>
)
}

The extra fields can be either objects or render functions (you can combine them):

  1. Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.

    The objects have the following properties:

    • name required

      • the name of the field
    • label required

      • the label of the field (used in the UI)
    • type required

      • the type of the field, which can be input or textarea
    • validations

      • an object with the validation rules for the field. The keys are the validation names, and the values are the validation error messages. Read more about the available validation rules in the react-hook-form docs.
  2. Render functions receive the react-hook-form object and the form state as arguments, and they can use them to render arbitrary UI elements.

    The render function has the following signature:

    ;(form: UseFormReturn, state: FormState) => React.ReactNode
    • form required

      • the react-hook-form object, read more about it in the react-hook-form docs
      • you need to use the form.register function to register your fields
    • state required

      • the form state object which has the following properties:
        • isLoading: boolean
          • whether the form is currently submitting
- - + + \ No newline at end of file diff --git a/docs/auth/social-auth/github.html b/docs/auth/social-auth/github.html index d3ae1e0097..fb75e1ad12 100644 --- a/docs/auth/social-auth/github.html +++ b/docs/auth/social-auth/github.html @@ -19,17 +19,17 @@ - - + +
-

GitHub

Wasp supports Github Authentication out of the box. +

GitHub

Wasp supports Github Authentication out of the box. GitHub is a great external auth choice when you're building apps for developers, as most of them already have a GitHub account.

Letting your users log in using their GitHub accounts turns the signup process into a breeze.

Let's walk through enabling Github Authentication, explain some of the default settings, and show how to override them.

Setting up Github Auth

Enabling GitHub Authentication comes down to a series of steps:

  1. Enabling GitHub authentication in the Wasp file.
  2. Adding the necessary Entities.
  3. Creating a GitHub OAuth app.
  4. Adding the neccessary Routes and Pages
  5. Using Auth UI components in our Pages.

Here's a skeleton of how our main.wasp should look like after we're done:

main.wasp
// Configuring the social authentication
app myApp {
auth: { ... }
}

// Defining entities
entity User { ... }
entity SocialLogin { ... }

// Defining routes and pages
route LoginRoute { ... }
page LoginPage { ... }

1. Adding Github Auth to Your Wasp File

Let's start by properly configuring the Auth object:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
// 1. Specify the User entity (we'll define it next)
userEntity: User,
// 2. Specify the SocialLogin entity (we'll define it next)
externalAuthEntity: SocialLogin,
methods: {
// 3. Enable Github Auth
gitHub: {}
},
onAuthFailedRedirectTo: "/login"
},
}

2. Add the Entities

Let's now define the entities acting as app.auth.userEntity and app.auth.externalAuthEntity:

main.wasp
// ...
// 4. Define the User entity
entity User {=psl
id Int @id @default(autoincrement())
// ...
externalAuthAssociations SocialLogin[]
psl=}

// 5. Define the SocialLogin entity
entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

externalAuthEntity and userEntity are explained in the social auth overview.

3. Creating a GitHub OAuth App

To use GitHub as an authentication method, you'll first need to create a GitHub OAuth App and provide Wasp with your client key and secret. Here's how you do it:

  1. Log into your GitHub account and navigate to: https://github.com/settings/developers.
  2. Select New OAuth App.
  3. Supply required information.
GitHub Applications Screenshot
  • For Authorization callback URL:
    • For development, put: http://localhost:3000/auth/login/github.
    • Once you know on which URL your app will be deployed, you can create a new app with that URL instead e.g. https://someotherhost.com/auth/login/github.
  1. Hit Register application.
  2. Hit Generate a new client secret on the next page.
  3. Copy your Client ID and Client secret as you'll need them in the next step.

4. Adding Environment Variables

Add these environment variables to the .env.server file at the root of your project (take their values from the previous step):

.env.server
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

5. Adding the Necessary Routes and Pages

Let's define the necessary authentication Routes and Pages.

Add the following code to your main.wasp file:

main.wasp
// ...

// 6. Define the routes
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@client/pages/auth.jsx"
}

We'll define the React components for these pages in the client/pages/auth.tsx file below.

6. Creating the Client Pages

info

We are using Tailwind CSS to style the pages. Read more about how to add it here.

Let's create a auth.tsx file in the client/pages folder and add the following to it:

client/pages/auth.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

export function Login() {
return (
<Layout>
<LoginForm />
</Layout>
)
}

// A layout component to center the content
export function Layout({ children }) {
return (
<div className="w-full h-full bg-white">
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
<div className="w-full h-full max-w-sm p-5 bg-white">
<div>{children}</div>
</div>
</div>
</div>
)
}

We imported the generated Auth UI component and used them in our pages. Read more about the Auth UI components here.

Conclusion

Yay, we've successfully set up Github Auth! 🎉

Github Auth

Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

Default Behaviour

Add gitHub: {} to the auth.methods dictionary to use it with default settings.

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
gitHub: {}
},
onAuthFailedRedirectTo: "/login"
},
}

When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

Also, if the userEntity has:

  • A username field: Wasp sets it to a random username (e.g. nice-blue-horse-14357).
  • A password field: Wasp sets it to a random string.

This is a historical coupling between auth methods we plan to remove in the future.

Overrides

Wasp lets you override the default behavior. You can create custom setups, such as allowing users to define a custom username rather instead of getting a randomly generated one.

There are two mechanisms (functions) used for overriding the default behavior:

  • getUserFieldsFn
  • configFn

Let's explore them in more detail.

Using the User's Provider Account Details

When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the getUserFieldsFn function.

For example, the User entity can include a displayName field which you can set based on the details received from the provider.

Wasp also lets you customize the configuration of the providers' settings using the configFn function.

Let's use this example to show both functions in action:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
gitHub: {
configFn: import { getConfig } from "@server/auth/github.js",
getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
}
},
onAuthFailedRedirectTo: "/login"
},
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
displayName String
externalAuthAssociations SocialLogin[]
psl=}

// ...
src/server/auth/github.js
import { generateAvailableDictionaryUsername } from "@wasp/core/auth.js";

export const getUserFields = async (_context, args) => {
const username = await generateAvailableDictionaryUsername();
const displayName = args.profile.displayName;
return { username, displayName };
};

export function getConfig() {
return {
clientID // look up from env or elsewhere
clientSecret // look up from env or elsewhere
scope: [],
};
}

Using Auth

To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

API Reference

Provider-specific behavior comes down to implementing two functions.

  • configFn
  • getUserFieldsFn

The reference shows how to define both.

For behavior common to all providers, check the general API Reference.

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
gitHub: {
configFn: import { getConfig } from "@server/auth/github.js",
getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
}
},
onAuthFailedRedirectTo: "/login"
},
}

The gitHub dict has the following properties:

  • configFn: ServerImport

    This function should return an object with the Client ID, Client Secret, and scope for the OAuth provider.

    src/server/auth/github.js
    export function getConfig() {
    return {
    clientID, // look up from env or elsewhere
    clientSecret, // look up from env or elsewhere
    scope: [],
    }
    }
  • getUserFieldsFn: ServerImport

    This function should return the user fields to use when creating a new user.

    The context contains the User entity, and the args object contains GitHub profile information. You can do whatever you want with this information (e.g., generate a username).

    Here is how you could generate a username based on the Github display name:

    src/server/auth/github.js
    import { generateAvailableUsername } from '@wasp/core/auth.js'

    export const getUserFields = async (_context, args) => {
    const username = await generateAvailableUsername(
    args.profile.displayName.split(' '),
    { separator: '.' }
    )
    return { username }
    }

    Wasp exposes two functions that can help you generate usernames. Import them from @wasp/core/auth.js:

    • generateAvailableUsername takes an array of strings and an optional separator and generates a string ending with a random number that is not yet in the database. For example, the above could produce something like "Jim.Smith.3984" for a Github user Jim Smith.
    • generateAvailableDictionaryUsername generates a random dictionary phrase that is not yet in the database. For example, nice-blue-horse-27160.
- - + + \ No newline at end of file diff --git a/docs/auth/social-auth/google.html b/docs/auth/social-auth/google.html index 4eb7a42f32..937114ceab 100644 --- a/docs/auth/social-auth/google.html +++ b/docs/auth/social-auth/google.html @@ -19,18 +19,18 @@ - - + +
-

Google

Wasp supports Google Authentication out of the box. +

Google

Wasp supports Google Authentication out of the box. Google Auth is arguably the best external auth option, as most users on the web already have Google accounts.

Enabling it lets your users log in using their existing Google accounts, greatly simplifying the process and enhancing the user experience.

Let's walk through enabling Google authentication, explain some of the default settings, and show how to override them.

Setting up Google Auth

Enabling Google Authentication comes down to a series of steps:

  1. Enabling Google authentication in the Wasp file.
  2. Adding the necessary Entities.
  3. Creating a Google OAuth app.
  4. Adding the neccessary Routes and Pages
  5. Using Auth UI components in our Pages.

Here's a skeleton of how our main.wasp should look like after we're done:

main.wasp
// Configuring the social authentication
app myApp {
auth: { ... }
}

// Defining entities
entity User { ... }
entity SocialLogin { ... }

// Defining routes and pages
route LoginRoute { ... }
page LoginPage { ... }

1. Adding Google Auth to Your Wasp File

Let's start by properly configuring the Auth object:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
// 1. Specify the User entity (we'll define it next)
userEntity: User,
// 2. Specify the SocialLogin entity (we'll define it next)
externalAuthEntity: SocialLogin,
methods: {
// 3. Enable Google Auth
google: {}
},
onAuthFailedRedirectTo: "/login"
},
}

externalAuthEntity and userEntity are explained in the social auth overview.

2. Adding the Entities

Let's now define the entities acting as app.auth.userEntity and app.auth.externalAuthEntity:

main.wasp
// ...
// 4. Define the User entity
entity User {=psl
id Int @id @default(autoincrement())
// ...
externalAuthAssociations SocialLogin[]
psl=}

// 5. Define the SocialLogin entity
entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

3. Creating a Google OAuth App

To use Google as an authentication method, you'll first need to create a Google project and provide Wasp with your client key and secret. Here's how you do it:

  1. Create a Google Cloud Platform account if you do not already have one: https://cloud.google.com/
  2. Create and configure a new Google project here: https://console.cloud.google.com/home/dashboard

Google Console Screenshot 1

Google Console Screenshot 2

  1. Search for OAuth in the top bar, click on OAuth consent screen.

Google Console Screenshot 3

  • Select what type of app you want, we will go with External.

    Google Console Screenshot 4

  • Fill out applicable information on Page 1.

    Google Console Screenshot 5

  • On Page 2, Scopes, you should select userinfo.profile. You can optionally search for other things, like email.

    Google Console Screenshot 6

    Google Console Screenshot 7

    Google Console Screenshot 8

  • Add any test users you want on Page 3.

    Google Console Screenshot 9

  1. Next, click Credentials.

Google Console Screenshot 10

  • Select Create Credentials.

  • Select OAuth client ID.

    Google Console Screenshot 11

  • Complete the form

    Google Console Screenshot 12

  • Under Authorized redirect URIs, put in: http://localhost:3000/auth/login/google

    Google Console Screenshot 13

    • Once you know on which URL(s) your API server will be deployed, also add those URL(s).
      • For example: https://someotherhost.com/auth/login/google
  • When you save, you can click the Edit icon and your credentials will be shown.

    Google Console Screenshot 14

  1. Copy your Client ID and Client secret as you will need them in the next step.

4. Adding Environment Variables

Add these environment variables to the .env.server file at the root of your project (take their values from the previous step):

.env.server
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret

5. Adding the Necessary Routes and Pages

Let's define the necessary authentication Routes and Pages.

Add the following code to your main.wasp file:

main.wasp
// ...

// 6. Define the routes
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@client/pages/auth.jsx"
}

We'll define the React components for these pages in the client/pages/auth.tsx file below.

6. Create the Client Pages

info

We are using Tailwind CSS to style the pages. Read more about how to add it here.

Let's now create a auth.tsx file in the client/pages. It should have the following code:

client/pages/auth.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

export function Login() {
return (
<Layout>
<LoginForm />
</Layout>
)
}

// A layout component to center the content
export function Layout({ children }) {
return (
<div className="w-full h-full bg-white">
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
<div className="w-full h-full max-w-sm p-5 bg-white">
<div>{children}</div>
</div>
</div>
</div>
)
}
Auth UI

Our pages use an automatically-generated Auth UI component. Read more about Auth UI components here.

Conclusion

Yay, we've successfully set up Google Auth! 🎉

Google Auth

Running wasp db migrate-dev and wasp start should now give you a working app with authentication. To see how to protect specific pages (i.e., hide them from non-authenticated users), read the docs on using auth.

Default Behaviour

Add google: {} to the auth.methods dictionary to use it with default settings:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
google: {}
},
onAuthFailedRedirectTo: "/login"
},
}

When a user signs in for the first time, Wasp creates a new user account and links it to the chosen auth provider account for future logins.

Also, if the userEntity has:

  • A username field: Wasp sets it to a random username (e.g. nice-blue-horse-14357).
  • A password field: Wasp sets it to a random string.

This is a historical coupling between auth methods we plan to remove in the future.

Overrides

Wasp lets you override the default behavior. You can create custom setups, such as allowing users to define a custom username rather instead of getting a randomly generated one.

There are two mechanisms (functions) used for overriding the default behavior:

  • getUserFieldsFn
  • configFn

Let's explore them in more detail.

Using the User's Provider Account Details

When a user logs in using a social login provider, the backend receives some data about the user. Wasp lets you access this data inside the getUserFieldsFn function.

For example, the User entity can include a displayName field which you can set based on the details received from the provider.

Wasp also lets you customize the configuration of the providers' settings using the configFn function.

Let's use this example to show both functions in action:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
google: {
configFn: import { getConfig } from "@server/auth/google.js",
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
}
},
onAuthFailedRedirectTo: "/login"
},
}

entity User {=psl
id Int @id @default(autoincrement())
username String @unique
displayName String
externalAuthAssociations SocialLogin[]
psl=}

// ...
src/server/auth/google.js
import { generateAvailableDictionaryUsername } from '@wasp/core/auth.js'

export const getUserFields = async (_context, args) => {
const username = await generateAvailableDictionaryUsername()
const displayName = args.profile.displayName
return { username, displayName }
}

export function getConfig() {
return {
clientID, // look up from env or elsewhere
clientSecret, // look up from env or elsewhere
scope: ['profile', 'email'],
}
}

Using Auth

To read more about how to set up the logout button and get access to the logged-in user in both client and server code, read the docs on using auth.

API Reference

Provider-specific behavior comes down to implementing two functions.

  • configFn
  • getUserFieldsFn

The reference shows how to define both.

For behavior common to all providers, check the general API Reference.

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
google: {
configFn: import { getConfig } from "@server/auth/google.js",
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
}
},
onAuthFailedRedirectTo: "/login"
},
}

The google dict has the following properties:

  • configFn: ServerImport

    This function must return an object with the Client ID, the Client Secret, and the scope for the OAuth provider.

    src/server/auth/google.js
    export function getConfig() {
    return {
    clientID, // look up from env or elsewhere
    clientSecret, // look up from env or elsewhere
    scope: ['profile', 'email'],
    }
    }
  • getUserFieldsFn: ServerImport

    This function must return the user fields to use when creating a new user.

    The context contains the User entity, and the args object contains Google profile information. You can do whatever you want with this information (e.g., generate a username).

    Here is how to generate a username based on the Google display name:

    src/server/auth/google.js
    import { generateAvailableUsername } from '@wasp/core/auth.js'

    export const getUserFields = async (_context, args) => {
    const username = await generateAvailableUsername(
    args.profile.displayName.split(' '),
    { separator: '.' }
    )
    return { username }
    }

    Wasp exposes two functions that can help you generate usernames. Import them from @wasp/core/auth.js:

    • generateAvailableUsername takes an array of strings and an optional separator and generates a string ending with a random number that is not yet in the database. For example, the above could produce something like "Jim.Smith.3984" for a Github user Jim Smith.
    • generateAvailableDictionaryUsername generates a random dictionary phrase that is not yet in the database. For example, nice-blue-horse-27160.
- - + + \ No newline at end of file diff --git a/docs/auth/social-auth/overview.html b/docs/auth/social-auth/overview.html index a76ded90f8..69ba551e48 100644 --- a/docs/auth/social-auth/overview.html +++ b/docs/auth/social-auth/overview.html @@ -19,12 +19,12 @@ - - + +
-

Overview

Social login options (e.g., Log in with Google) are a great (maybe even the best) solution for handling user accounts. +

Overview

Social login options (e.g., Log in with Google) are a great (maybe even the best) solution for handling user accounts. A famous old developer joke tells us "The best auth system is the one you never have to make."

Wasp wants to make adding social login options to your app as painless as possible.

Using different social providers gives users a chance to sign into your app via their existing accounts on other platforms (Google, GitHub, etc.).

This page goes through the common behaviors between all supported social login providers and shows you how to customize them. It also gives an overview of Wasp's UI helpers - the quickest possible way to get started with social auth.

Available Providers

Wasp currently supports the following social login providers:

Social Login Entity

Wasp requires you to declare a userEntity for all auth methods (social or otherwise). This field tells Wasp which Entity represents the user.

Additionally, when using auth methods that rely on external providers(e.g., Google), you must also declare an externalAuthEntity. @@ -34,7 +34,7 @@ If you're looking for the fastest way to get your auth up and running, that's where you should look.

The UI helpers described below are lower-level and are useful for creating your custom forms.

Wasp provides sign-in buttons and URLs for each of the supported social login providers.

client/LoginPage.jsx
import {
SignInButton as GoogleSignInButton,
signInUrl as googleSignInUrl,
} from '@wasp/auth/helpers/Google'
import {
SignInButton as GitHubSignInButton,
signInUrl as gitHubSignInUrl,
} from '@wasp/auth/helpers/GitHub'

export const LoginPage = () => {
return (
<>
<GoogleSignInButton />
<GitHubSignInButton />
{/* or */}
<a href={googleSignInUrl}>Sign in with Google</a>
<a href={gitHubSignInUrl}>Sign in with GitHub</a>
</>
)
}

If you need even more customization, you can create your custom components using signInUrls.

API Reference

Fields in the app.auth Dictionary and Overrides

For more information on:

  • Allowed fields in app.auth
  • getUserFields and configFn functions

Check the provider-specific API References:

The externalAuthEntity and Its Fields

Using social login providers requires you to define an External Auth Entity and declare it with the app.auth.externalAuthEntity field. This Entity holds the data relevant to the social provider. All social providers share the same Entity.

main.wasp
// ...

entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}

// ...
info

You don't need to know these details, you can just copy and paste the entity definition above and you are good to go.

The Entity acting as app.auth.externalAuthEntity must include the following fields:

  • provider - The provider's name (e.g. google, github, etc.).
  • providerId - The user's ID on the provider's platform.
  • userId - The user's ID on your platform (this references the id field from the Entity acting as app.auth.userEntity).
  • user - A relation to the userEntity (see the userEntity section) for more details.
  • createdAt - A timestamp of when the association was created.
  • @@unique([provider, providerId, userId]) - A unique constraint on the combination of provider, providerId and userId.

Expected Fields on the userEntity

Using Social login providers requires you to add one extra field to the Entity acting as app.auth.userEntity:

main.wasp
// ...

entity User {=psl
id Int @id @default(autoincrement())
//...
externalAuthAssociations SocialLogin[]
psl=}

// ...
- - + + \ No newline at end of file diff --git a/docs/auth/ui.html b/docs/auth/ui.html index 1ceddf2490..56c2bb34bd 100644 --- a/docs/auth/ui.html +++ b/docs/auth/ui.html @@ -19,13 +19,13 @@ - - + +
-

Auth UI

To make using authentication in your app as easy as possible, Wasp generates the server-side code but also the client-side UI for you. It enables you to quickly get the login, signup, password reset and email verification flows in your app.

Below we cover all of the available UI components and how to use them.

Auth UI

Overview

After Wasp generates the UI components for your auth, you can use it as is, or customize it to your liking.

Based on the authentication providers you enabled in your main.wasp file, the Auth UI will show the corresponding UI (form and buttons). For example, if you enabled e-mail authentication:

main.wasp
app MyApp {
//...
auth: {
methods: {
email: {},
},
// ...
}
}

You'll get the following UI:

Auth UI

And then if you enable Google and Github:

main.wasp
app MyApp {
//...
auth: {
methods: {
email: {},
google: {},
github: {},
},
// ...
}
}

The form will automatically update to look like this:

Auth UI

Let's go through all of the available components and how to use them.

Auth Components

The following components are available for you to use in your app:

Login Form

Used with Username & Password, Email, Github and Google authentication.

Login form

You can use the LoginForm component to build your login page:

main.wasp
// ...

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { LoginPage } from "@client/LoginPage.jsx"
}
client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

// Use it like this
export function LoginPage() {
return <LoginForm />
}

It will automatically show the correct authentication providers based on your main.wasp file.

Signup Form

Used with Username & Password, Email, Github and Google authentication.

Signup form

You can use the SignupForm component to build your signup page:

main.wasp
// ...

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { SignupPage } from "@client/SignupPage.jsx"
}
client/SignupPage.jsx
import { SignupForm } from '@wasp/auth/forms/Signup'

// Use it like this
export function SignupPage() {
return <SignupForm />
}

It will automatically show the correct authentication providers based on your main.wasp file.

Read more about customizing the signup process like adding additional fields or extra UI in the Using Auth section.

Forgot Password Form

Used with Email authentication.

If users forget their password, they can use this form to reset it.

Forgot password form

You can use the ForgotPasswordForm component to build your own forgot password page:

main.wasp
// ...

route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
page RequestPasswordResetPage {
component: import { ForgotPasswordPage } from "@client/ForgotPasswordPage.jsx"
}
client/ForgotPasswordPage.jsx
import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword'

// Use it like this
export function ForgotPasswordPage() {
return <ForgotPasswordForm />
}

Reset Password Form

Used with Email authentication.

After users click on the link in the email they receive after submitting the forgot password form, they will be redirected to this form where they can reset their password.

Reset password form

You can use the ResetPasswordForm component to build your reset password page:

main.wasp
// ...

route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
page PasswordResetPage {
component: import { ResetPasswordPage } from "@client/ResetPasswordPage.jsx"
}
client/ResetPasswordPage.jsx
import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword'

// Use it like this
export function ResetPasswordPage() {
return <ResetPasswordForm />
}

Verify Email Form

Used with Email authentication.

After users sign up, they will receive an email with a link to this form where they can verify their email.

Verify email form

You can use the VerifyEmailForm component to build your email verification page:

main.wasp
// ...

route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
page EmailVerificationPage {
component: import { VerifyEmailPage } from "@client/VerifyEmailPage.jsx"
}
client/VerifyEmailPage.jsx
import { VerifyEmailForm } from '@wasp/auth/forms/VerifyEmail'

// Use it like this
export function VerifyEmailPage() {
return <VerifyEmailForm />
}

Customization 💅🏻

You customize all of the available forms by passing props to them.

Props you can pass to all of the forms:

  1. appearance - customize the form colors (via design tokens)
  2. logo - path to your logo
  3. socialLayout - layout of the social buttons, which can be vertical or horizontal

1. Customizing the Colors

We use Stitches to style the Auth UI. You can customize the styles by overriding the default theme tokens.

List of all available tokens

See the list of all available tokens which you can override.

client/appearance.js
export const authAppearance = {
colors: {
brand: '#5969b8', // blue
brandAccent: '#de5998', // pink
submitButtonText: 'white',
},
}
client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'
import { authAppearance } from './appearance'

export function LoginPage() {
return (
<LoginForm
// Pass the appearance object to the form
appearance={authAppearance}
/>
)
}

We recommend defining your appearance in a separate file and importing it into your components.

You can add your logo to the Auth UI by passing the logo prop to any of the components.

client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'
import Logo from './logo.png'

export function LoginPage() {
return (
<LoginForm
// Pass in the path to your logo
logo={Logo}
/>
)
}

3. Social Buttons Layout

You can change the layout of the social buttons by passing the socialLayout prop to any of the components. It can be either vertical or horizontal (default).

If we pass in vertical:

client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

export function LoginPage() {
return (
<LoginForm
// Pass in the socialLayout prop
socialLayout="vertical"
/>
)
}

We get this:

Vertical social buttons

Let's Put Everything Together 🪄

If we provide the logo and custom colors:

client/appearance.js
export const appearance = {
colors: {
brand: '#5969b8', // blue
brandAccent: '#de5998', // pink
submitButtonText: 'white',
},
}
client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

import { authAppearance } from './appearance'
import todoLogo from './todoLogo.png'

export function LoginPage() {
return <LoginForm appearance={appearance} logo={todoLogo} />
}

We get a form looking like this:

Custom login form
- - +

Auth UI

To make using authentication in your app as easy as possible, Wasp generates the server-side code but also the client-side UI for you. It enables you to quickly get the login, signup, password reset and email verification flows in your app.

Below we cover all of the available UI components and how to use them.

Auth UI

Overview

After Wasp generates the UI components for your auth, you can use it as is, or customize it to your liking.

Based on the authentication providers you enabled in your main.wasp file, the Auth UI will show the corresponding UI (form and buttons). For example, if you enabled e-mail authentication:

main.wasp
app MyApp {
//...
auth: {
methods: {
email: {},
},
// ...
}
}

You'll get the following UI:

Auth UI

And then if you enable Google and Github:

main.wasp
app MyApp {
//...
auth: {
methods: {
email: {},
google: {},
github: {},
},
// ...
}
}

The form will automatically update to look like this:

Auth UI

Let's go through all of the available components and how to use them.

Auth Components

The following components are available for you to use in your app:

Login Form

Used with Username & Password, Email, Github and Google authentication.

Login form

You can use the LoginForm component to build your login page:

main.wasp
// ...

route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { LoginPage } from "@client/LoginPage.jsx"
}
client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

// Use it like this
export function LoginPage() {
return <LoginForm />
}

It will automatically show the correct authentication providers based on your main.wasp file.

Signup Form

Used with Username & Password, Email, Github and Google authentication.

Signup form

You can use the SignupForm component to build your signup page:

main.wasp
// ...

route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { SignupPage } from "@client/SignupPage.jsx"
}
client/SignupPage.jsx
import { SignupForm } from '@wasp/auth/forms/Signup'

// Use it like this
export function SignupPage() {
return <SignupForm />
}

It will automatically show the correct authentication providers based on your main.wasp file.

Read more about customizing the signup process like adding additional fields or extra UI in the Using Auth section.

Forgot Password Form

Used with Email authentication.

If users forget their password, they can use this form to reset it.

Forgot password form

You can use the ForgotPasswordForm component to build your own forgot password page:

main.wasp
// ...

route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
page RequestPasswordResetPage {
component: import { ForgotPasswordPage } from "@client/ForgotPasswordPage.jsx"
}
client/ForgotPasswordPage.jsx
import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword'

// Use it like this
export function ForgotPasswordPage() {
return <ForgotPasswordForm />
}

Reset Password Form

Used with Email authentication.

After users click on the link in the email they receive after submitting the forgot password form, they will be redirected to this form where they can reset their password.

Reset password form

You can use the ResetPasswordForm component to build your reset password page:

main.wasp
// ...

route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
page PasswordResetPage {
component: import { ResetPasswordPage } from "@client/ResetPasswordPage.jsx"
}
client/ResetPasswordPage.jsx
import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword'

// Use it like this
export function ResetPasswordPage() {
return <ResetPasswordForm />
}

Verify Email Form

Used with Email authentication.

After users sign up, they will receive an email with a link to this form where they can verify their email.

Verify email form

You can use the VerifyEmailForm component to build your email verification page:

main.wasp
// ...

route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
page EmailVerificationPage {
component: import { VerifyEmailPage } from "@client/VerifyEmailPage.jsx"
}
client/VerifyEmailPage.jsx
import { VerifyEmailForm } from '@wasp/auth/forms/VerifyEmail'

// Use it like this
export function VerifyEmailPage() {
return <VerifyEmailForm />
}

Customization 💅🏻

You customize all of the available forms by passing props to them.

Props you can pass to all of the forms:

  1. appearance - customize the form colors (via design tokens)
  2. logo - path to your logo
  3. socialLayout - layout of the social buttons, which can be vertical or horizontal

1. Customizing the Colors

We use Stitches to style the Auth UI. You can customize the styles by overriding the default theme tokens.

List of all available tokens

See the list of all available tokens which you can override.

client/appearance.js
export const authAppearance = {
colors: {
brand: '#5969b8', // blue
brandAccent: '#de5998', // pink
submitButtonText: 'white',
},
}
client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'
import { authAppearance } from './appearance'

export function LoginPage() {
return (
<LoginForm
// Pass the appearance object to the form
appearance={authAppearance}
/>
)
}

We recommend defining your appearance in a separate file and importing it into your components.

You can add your logo to the Auth UI by passing the logo prop to any of the components.

client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'
import Logo from './logo.png'

export function LoginPage() {
return (
<LoginForm
// Pass in the path to your logo
logo={Logo}
/>
)
}

3. Social Buttons Layout

You can change the layout of the social buttons by passing the socialLayout prop to any of the components. It can be either vertical or horizontal (default).

If we pass in vertical:

client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

export function LoginPage() {
return (
<LoginForm
// Pass in the socialLayout prop
socialLayout="vertical"
/>
)
}

We get this:

Vertical social buttons

Let's Put Everything Together 🪄

If we provide the logo and custom colors:

client/appearance.js
export const appearance = {
colors: {
brand: '#5969b8', // blue
brandAccent: '#de5998', // pink
submitButtonText: 'white',
},
}
client/LoginPage.jsx
import { LoginForm } from '@wasp/auth/forms/Login'

import { authAppearance } from './appearance'
import todoLogo from './todoLogo.png'

export function LoginPage() {
return <LoginForm appearance={appearance} logo={todoLogo} />
}

We get a form looking like this:

Custom login form
+ + \ No newline at end of file diff --git a/docs/auth/username-and-pass.html b/docs/auth/username-and-pass.html index 1989c2765d..3dd421027d 100644 --- a/docs/auth/username-and-pass.html +++ b/docs/auth/username-and-pass.html @@ -19,13 +19,13 @@ - - + +
-

Username & Password

Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client-side.

Setting Up Username & Password Authentication

To set up username authentication we need to:

  1. Enable username authentication in the Wasp file
  2. Add the user entity
  3. Add the routes and pages
  4. Use Auth UI components in our pages

Structure of the main.wasp file we will end up with:

main.wasp
// Configuring e-mail authentication
app myApp {
auth: { ... }
}
// Defining User entity
entity User { ... }
// Defining routes and pages
route SignupRoute { ... }
page SignupPage { ... }
// ...

1. Enable Username Authentication

Let's start with adding the following to our main.wasp file:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
// 1. Specify the user entity (we'll define it next)
userEntity: User,
methods: {
// 2. Enable username authentication
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}

Read more about the usernameAndPassword auth method options here.

2. Add the User Entity

When username authentication is enabled, Wasp expects certain fields in your userEntity. Let's add these fields to our main.wasp file:

main.wasp
// 3. Define the user entity
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
// Add your own fields below
// ...
psl=}

Read more about the userEntity fields here.

3. Add the Routes and Pages

Next, we need to define the routes and pages for the authentication pages.

Add the following to the main.wasp file:

main.wasp
// ...
// 4. Define the routes
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@client/pages/auth.jsx"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@client/pages/auth.jsx"
}

We'll define the React components for these pages in the client/pages/auth.tsx file below.

4. Create the Client Pages

info

We are using Tailwind CSS to style the pages. Read more about how to add it here.

Let's create a auth.tsx file in the client/pages folder and add the following to it:

client/pages/auth.jsx
import { LoginForm } from "@wasp/auth/forms/Login";
import { SignupForm } from "@wasp/auth/forms/Signup";
import { Link } from "react-router-dom";

export function Login() {
return (
<Layout>
<LoginForm />
<br />
<span className="text-sm font-medium text-gray-900">
Don't have an account yet? <Link to="/signup">go to signup</Link>.
</span>
</Layout>
);
}

export function Signup() {
return (
<Layout>
<SignupForm />
<br />
<span className="text-sm font-medium text-gray-900">
I already have an account (<Link to="/login">go to login</Link>).
</span>
</Layout>
);
}

// A layout component to center the content
export function Layout({ children }) {
return (
<div className="w-full h-full bg-white">
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
<div className="w-full h-full max-w-sm p-5 bg-white">
<div>{children}</div>
</div>
</div>
</div>
);
}

We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

Conclusion

That's it! We have set up username authentication in our app. 🎉

Running wasp db migrate-dev and then wasp start should give you a working app with username authentication. If you want to put some of the pages behind authentication, read the using auth docs.

Customizing the Auth Flow

The login and signup flows are pretty standard: they allow the user to sign up and then log in with their username and password. The signup flow validates the username and password and then creates a new user entity in the database.

Read more about the default username and password validation rules and how to override them in the using auth docs.

If you require more control in your authentication flow, you can achieve that in the following ways:

  1. Create your UI and use signup and login actions.
  2. Create your custom sign-up and login actions which uses the Prisma client, along with your custom code.

1. Using the signup and login actions

login()

An action for logging in the user.

It takes two arguments:

  • username: string required

    Username of the user logging in.

  • password: string required

    Password of the user logging in.

You can use it like this:

client/pages/auth.jsx
// Importing the login action 👇
import login from '@wasp/auth/login'

import { useState } from 'react'
import { useHistory } from 'react-router-dom'
import { Link } from 'react-router-dom'

export function LoginPage() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const history = useHistory()

async function handleSubmit(event) {
event.preventDefault()
try {
await login(username, password)
history.push('/')
} catch (error) {
setError(error)
}
}

return (
<form onSubmit={handleSubmit}>
{/* ... */}
</form>
);
}
note

When using the exposed login() function, make sure to implement your redirect on success login logic (e.g. redirecting to home).

signup()

An action for signing up the user. This action does not log in the user, you still need to call login().

It takes one argument:

  • userFields: object required

    It has the following fields:

    • username: string required

    • password: string required

    info

    Wasp only stores the auth-related fields of the user entity. Adding extra fields to userFields will not have any effect.

    If you need to add extra fields to the user entity, we suggest doing it in a separate step after the user logs in for the first time.

You can use it like this:

client/pages/auth.jsx
// Importing the signup and login actions 👇
import signup from '@wasp/auth/signup'
import login from '@wasp/auth/login'

import { useState } from 'react'
import { useHistory } from 'react-router-dom'
import { Link } from 'react-router-dom'

export function Signup() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const history = useHistory()

async function handleSubmit(event) {
event.preventDefault()
try {
await signup({
username,
password,
})
await login(username, password)
history.push("/")
} catch (error) {
setError(error)
}
}

return (
<form onSubmit={handleSubmit}>
{/* ... */}
</form>
);
}

2. Creating your custom actions

The code of your custom sign-up action can look like this:

main.wasp
// ...

action signupUser {
fn: import { signUp } from "@server/auth/signup.js",
entities: [User]
}
src/server/auth/signup.js
export const signUp = async (args, context) => {
// Your custom code before sign-up.
// ...

const newUser = context.entities.User.create({
data: {
username: args.username,
password: args.password // password hashed automatically by Wasp! 🐝
}
})

// Your custom code after sign-up.
// ...
return newUser
}

Using Auth

To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the using auth docs.

API Reference

userEntity fields

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}

// Wasp requires the `userEntity` to have at least the following fields
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}

Username & password auth requires that userEntity specified in auth contains:

  • username field of type String
  • password field of type String

Fields in the usernameAndPassword dict

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}
// ...
info

usernameAndPassword dict doesn't have any options at the moment.

You can read about the rest of the auth options in the using auth section of the docs.

- - +

Username & Password

Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client-side.

Setting Up Username & Password Authentication

To set up username authentication we need to:

  1. Enable username authentication in the Wasp file
  2. Add the user entity
  3. Add the routes and pages
  4. Use Auth UI components in our pages

Structure of the main.wasp file we will end up with:

main.wasp
// Configuring e-mail authentication
app myApp {
auth: { ... }
}
// Defining User entity
entity User { ... }
// Defining routes and pages
route SignupRoute { ... }
page SignupPage { ... }
// ...

1. Enable Username Authentication

Let's start with adding the following to our main.wasp file:

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
// 1. Specify the user entity (we'll define it next)
userEntity: User,
methods: {
// 2. Enable username authentication
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}

Read more about the usernameAndPassword auth method options here.

2. Add the User Entity

When username authentication is enabled, Wasp expects certain fields in your userEntity. Let's add these fields to our main.wasp file:

main.wasp
// 3. Define the user entity
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
// Add your own fields below
// ...
psl=}

Read more about the userEntity fields here.

3. Add the Routes and Pages

Next, we need to define the routes and pages for the authentication pages.

Add the following to the main.wasp file:

main.wasp
// ...
// 4. Define the routes
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@client/pages/auth.jsx"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@client/pages/auth.jsx"
}

We'll define the React components for these pages in the client/pages/auth.tsx file below.

4. Create the Client Pages

info

We are using Tailwind CSS to style the pages. Read more about how to add it here.

Let's create a auth.tsx file in the client/pages folder and add the following to it:

client/pages/auth.jsx
import { LoginForm } from "@wasp/auth/forms/Login";
import { SignupForm } from "@wasp/auth/forms/Signup";
import { Link } from "react-router-dom";

export function Login() {
return (
<Layout>
<LoginForm />
<br />
<span className="text-sm font-medium text-gray-900">
Don't have an account yet? <Link to="/signup">go to signup</Link>.
</span>
</Layout>
);
}

export function Signup() {
return (
<Layout>
<SignupForm />
<br />
<span className="text-sm font-medium text-gray-900">
I already have an account (<Link to="/login">go to login</Link>).
</span>
</Layout>
);
}

// A layout component to center the content
export function Layout({ children }) {
return (
<div className="w-full h-full bg-white">
<div className="min-w-full min-h-[75vh] flex items-center justify-center">
<div className="w-full h-full max-w-sm p-5 bg-white">
<div>{children}</div>
</div>
</div>
</div>
);
}

We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.

Conclusion

That's it! We have set up username authentication in our app. 🎉

Running wasp db migrate-dev and then wasp start should give you a working app with username authentication. If you want to put some of the pages behind authentication, read the using auth docs.

Customizing the Auth Flow

The login and signup flows are pretty standard: they allow the user to sign up and then log in with their username and password. The signup flow validates the username and password and then creates a new user entity in the database.

Read more about the default username and password validation rules and how to override them in the using auth docs.

If you require more control in your authentication flow, you can achieve that in the following ways:

  1. Create your UI and use signup and login actions.
  2. Create your custom sign-up and login actions which uses the Prisma client, along with your custom code.

1. Using the signup and login actions

login()

An action for logging in the user.

It takes two arguments:

  • username: string required

    Username of the user logging in.

  • password: string required

    Password of the user logging in.

You can use it like this:

client/pages/auth.jsx
// Importing the login action 👇
import login from '@wasp/auth/login'

import { useState } from 'react'
import { useHistory } from 'react-router-dom'
import { Link } from 'react-router-dom'

export function LoginPage() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const history = useHistory()

async function handleSubmit(event) {
event.preventDefault()
try {
await login(username, password)
history.push('/')
} catch (error) {
setError(error)
}
}

return (
<form onSubmit={handleSubmit}>
{/* ... */}
</form>
);
}
note

When using the exposed login() function, make sure to implement your redirect on success login logic (e.g. redirecting to home).

signup()

An action for signing up the user. This action does not log in the user, you still need to call login().

It takes one argument:

  • userFields: object required

    It has the following fields:

    • username: string required

    • password: string required

    info

    Wasp only stores the auth-related fields of the user entity. Adding extra fields to userFields will not have any effect.

    If you need to add extra fields to the user entity, we suggest doing it in a separate step after the user logs in for the first time.

You can use it like this:

client/pages/auth.jsx
// Importing the signup and login actions 👇
import signup from '@wasp/auth/signup'
import login from '@wasp/auth/login'

import { useState } from 'react'
import { useHistory } from 'react-router-dom'
import { Link } from 'react-router-dom'

export function Signup() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const history = useHistory()

async function handleSubmit(event) {
event.preventDefault()
try {
await signup({
username,
password,
})
await login(username, password)
history.push("/")
} catch (error) {
setError(error)
}
}

return (
<form onSubmit={handleSubmit}>
{/* ... */}
</form>
);
}

2. Creating your custom actions

The code of your custom sign-up action can look like this:

main.wasp
// ...

action signupUser {
fn: import { signUp } from "@server/auth/signup.js",
entities: [User]
}
src/server/auth/signup.js
export const signUp = async (args, context) => {
// Your custom code before sign-up.
// ...

const newUser = context.entities.User.create({
data: {
username: args.username,
password: args.password // password hashed automatically by Wasp! 🐝
}
})

// Your custom code after sign-up.
// ...
return newUser
}

Using Auth

To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the using auth docs.

API Reference

userEntity fields

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}

// Wasp requires the `userEntity` to have at least the following fields
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}

Username & password auth requires that userEntity specified in auth contains:

  • username field of type String
  • password field of type String

Fields in the usernameAndPassword dict

main.wasp
app myApp {
wasp: {
version: "^0.11.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}
// ...
info

usernameAndPassword dict doesn't have any options at the moment.

You can read about the rest of the auth options in the using auth section of the docs.

+ + \ No newline at end of file diff --git a/docs/contact.html b/docs/contact.html index 24a4ad36c4..377ed5c8dd 100644 --- a/docs/contact.html +++ b/docs/contact.html @@ -19,13 +19,13 @@ - - + + - - +
+ + \ No newline at end of file diff --git a/docs/contributing.html b/docs/contributing.html index 633a3a6129..e13837c9ba 100644 --- a/docs/contributing.html +++ b/docs/contributing.html @@ -19,13 +19,13 @@ - - + +
-

Contributing

Any way you want to contribute is a good way, and we'd be happy to meet you! A single entry point for all contributors is the CONTRIBUTING.md file in our Github repo. All the requirements and instructions are there, so please check CONTRIBUTING.md for more details.

Some side notes to make your journey easier:

  1. Join us on Discord and let's talk! We can discuss language design, new/existing features, and weather, or you can tell us how you feel about Wasp :).

  2. Wasp's compiler is built with Haskell. That means you'll need to be somewhat familiar with this language if you'd like to contribute to the compiler itself. But Haskell is just a part of Wasp, and you can contribute to lot of parts that require web dev skills, either by coding or by suggesting how to improve Wasp and its design as a web framework. If you don't have Haskell knowledge (or any dev experience at all) - no problem. There are a lot of JS-related tasks and documentation updates as well!

  3. If there's something you'd like to bring to our attention, go to docs GitHub repo and make an issue/PR!

Happy hacking!

- - +

Contributing

Any way you want to contribute is a good way, and we'd be happy to meet you! A single entry point for all contributors is the CONTRIBUTING.md file in our Github repo. All the requirements and instructions are there, so please check CONTRIBUTING.md for more details.

Some side notes to make your journey easier:

  1. Join us on Discord and let's talk! We can discuss language design, new/existing features, and weather, or you can tell us how you feel about Wasp :).

  2. Wasp's compiler is built with Haskell. That means you'll need to be somewhat familiar with this language if you'd like to contribute to the compiler itself. But Haskell is just a part of Wasp, and you can contribute to lot of parts that require web dev skills, either by coding or by suggesting how to improve Wasp and its design as a web framework. If you don't have Haskell knowledge (or any dev experience at all) - no problem. There are a lot of JS-related tasks and documentation updates as well!

  3. If there's something you'd like to bring to our attention, go to docs GitHub repo and make an issue/PR!

Happy hacking!

+ + \ No newline at end of file diff --git a/docs/data-model/backends.html b/docs/data-model/backends.html index 10c531ebb5..a944a33d95 100644 --- a/docs/data-model/backends.html +++ b/docs/data-model/backends.html @@ -19,15 +19,14 @@ - - + +
-

Databases

Entities, Operations and Automatic CRUD together make a high-level interface for working with your app's data. Still, all that data has to live somewhere, so let's see how Wasp deals with databases.

Supported Database Backends

Wasp supports multiple database backends. We'll list and explain each one.

SQLite

The default database Wasp uses is SQLite.

SQLite is a great way for getting started with a new project because it doesn't require any configuration, but Wasp can only use it in development. Once you want to deploy your Wasp app to production, you'll need to switch to PostgreSQL and stick with it.

Fortunately, migrating from SQLite to PostgreSQL is pretty simple, and we have a guide to help you.

PostgreSQL

PostgreSQL is the most advanced open source database and the fourth most popular database overall. +

Databases

Entities, Operations and Automatic CRUD together make a high-level interface for working with your app's data. Still, all that data has to live somewhere, so let's see how Wasp deals with databases.

Supported Database Backends

Wasp supports multiple database backends. We'll list and explain each one.

SQLite

The default database Wasp uses is SQLite.

SQLite is a great way for getting started with a new project because it doesn't require any configuration, but Wasp can only use it in development. Once you want to deploy your Wasp app to production, you'll need to switch to PostgreSQL and stick with it.

Fortunately, migrating from SQLite to PostgreSQL is pretty simple, and we have a guide to help you.

PostgreSQL

PostgreSQL is the most advanced open source database and the fourth most popular database overall. It's been in active development for 20 years. -Therefore, if you're looking for a battle-tested database, look no further.

To use Wasp with PostgreSQL, you'll have to ensure a database instance is running during development. Wasp needs access to your database for commands such as wasp start or wasp db migrate-dev and expects to find a connection string in the DATABASE_URL environment variable.

We cover all supported ways of connecting to a database in the next section.

Migrating from SQLite to PostgreSQL

To run your Wasp app in production, you'll need to switch from SQLite to PostgreSQL.

  1. Set the app.db.system field to PostgreSQL.
main.wasp
app MyApp {
title: "My app",
// ...
db: {
system: PostgreSQL,
// ...
}
}
  1. Delete all the old migrations, since they are SQLite migrations and can't be used with PostgreSQL:

    rm -r migrations/
  2. Ensure your new database is running (check the section on connecing to a database to see how). Leave it running, since we need it for the next step.

  3. In a different terminal, run wasp db migrate-dev to apply the changes and create a new initial migration.

  4. That is it, you are all done!

Connecting to a Database

Assuming you're not using SQLite, Wasp offers two ways of connecting your app to a database instance:

  1. A ready-made dev database that requires minimal setup and is great for quick prototyping.
  2. A "real" database Wasp can connect to and use in production.

Using the Dev Database Provided by Wasp

The command wasp start db will start a default PostgreSQL dev database for you.

Your Wasp app will automatically connect to it, just keep wasp start db running in the background. -Also, make sure that:

Connecting to an existing database

If you want to spin up your own dev database (or connect to an external one), you can tell Wasp about it using the DATABASE_URL environment variable. Wasp will use the value of DATABASE_URL as a connection string.

The easiest way to set the necessary DATABASE_URL environment variable is by adding it to the .env.server file in the root dir of your Wasp project (if that file doesn't yet exist, create it).

Alternatively, you can set it inline when running wasp (this applies to all environment variables):

DATABASE_URL=<my-db-url> wasp ...

This trick is useful for running a certain wasp command on a specific database. +Therefore, if you're looking for a battle-tested database, look no further.

To use Wasp with PostgreSQL, you'll have to ensure a database instance is running during development. Wasp needs access to your database for commands such as wasp start or wasp db migrate-dev and expects to find a connection string in the DATABASE_URL environment variable.

We cover all supported ways of connecting to a database in the next section.

Migrating from SQLite to PostgreSQL

To run your Wasp app in production, you'll need to switch from SQLite to PostgreSQL.

  1. Set the app.db.system field to PostgreSQL.
main.wasp
app MyApp {
title: "My app",
// ...
db: {
system: PostgreSQL,
// ...
}
}
  1. Delete all the old migrations, since they are SQLite migrations and can't be used with PostgreSQL, as well as the SQLite database by running wasp clean:
rm -r migrations/
wasp clean

3. Ensure your new database is running (check the [section on connecing to a database](#connecting-to-a-database) to see how). Leave it running, since we need it for the next step.
4. In a different terminal, run `wasp db migrate-dev` to apply the changes and create a new initial migration.
5. That is it, you are all done!

## Connecting to a Database

Assuming you're not using SQLite, Wasp offers two ways of connecting your app to a database instance:

1. A ready-made dev database that requires minimal setup and is great for quick prototyping.
2. A "real" database Wasp can connect to and use in production.

### Using the Dev Database Provided by Wasp

The command `wasp start db` will start a default PostgreSQL dev database for you.

Your Wasp app will automatically connect to it, just keep `wasp start db` running in the background.
Also, make sure that:

- You have [Docker installed](https://www.docker.com/get-started/) and in `PATH`.
- The port `5432` isn't taken.

### Connecting to an existing database

If you want to spin up your own dev database (or connect to an external one), you can tell Wasp about it using the `DATABASE_URL` environment variable. Wasp will use the value of `DATABASE_URL` as a connection string.

The easiest way to set the necessary `DATABASE_URL` environment variable is by adding it to the [.env.server](/docs/project/env-vars) file in the root dir of your Wasp project (if that file doesn't yet exist, create it).

Alternatively, you can set it inline when running `wasp` (this applies to all environment variables):

```bash
DATABASE_URL=<my-db-url> wasp ...

This trick is useful for running a certain wasp command on a specific database. For example, you could do:

DATABASE_URL=<production-db-url> wasp db seed myProductionSeed

This command seeds the data for a fresh staging or production database. To more precisely understand how seeding works, keep reading.

Seeding the Database

Database seeding is a term used for populating the database with some initial data.

Seeding is most commonly used for two following scenarios:

  1. To put the development database into a state convenient for working and testing.
  2. To initialize any database (dev, staging, or prod) with essential data it requires to operate. For example, populating the Currency table with default currencies, or the Country table with all available countries.

Writing a Seed Function

You can define as many seed functions as you want in an array under the app.db.seeds field:

main.wasp
app MyApp {
// ...
db: {
// ...
seeds: [
import { devSeedSimple } from "@server/dbSeeds.js",
import { prodSeed } from "@server/dbSeeds.js"
]
}
}

Each seed function must be an async function that takes one argument, prismaClient, which is a Prisma Client instance used to interact with the database. @@ -35,8 +34,8 @@ The default value for the field is SQLite (this default value also applies if the entire db field is left unset). Whenever you modify the db.system field, make sure to run wasp db migrate-dev to apply the changes.

  • seeds: [ServerImport]

    Defines the seed functions you can use with the wasp db seed command to seed your database with initial data. Read the Seeding section for more details.

  • prisma: PrismaOptions

    Additional configuration for Prisma.

    main.wasp
    app MyApp {
    // ...
    db: {
    // ...
    prisma: {
    clientPreviewFeatures: ["postgresqlExtensions"],
    dbExtensions: [
    { name: "hstore", schema: "myHstoreSchema" },
    { name: "pg_trgm" },
    { name: "postgis", version: "2.1" },
    ]
    }
    }
    }

    It's a dictionary with the following fields:

    • clientPreviewFeatures : [string]

      Allows you to define Prisma client preview features, like for example, "postgresqlExtensions".

    • dbExtensions: DbExtension[]

      It allows you to define PostgreSQL extensions that should be enabled for your database. Read more about PostgreSQL extensions in Prisma.

      For each extension you define a dict with the following fields:

      • name: string required

        The name of the extension you would normally put in the Prisma file.

        schema.prisma
        extensions = [hstore(schema: "myHstoreSchema"), pg_trgm, postgis(version: "2.1")]
        // 👆 Extension name
      • map: string

        It sets the map argument of the extension. Explanation for the field from the Prisma docs:

        This is the database name of the extension. If this argument is not specified, the name of the extension in the Prisma schema must match the database name.

      • schema: string

        It sets the schema argument of the extension. Explanation for the field from the Prisma docs:

        This is the name of the schema in which to activate the extension's objects. If this argument is not specified, the current default object creation schema is used.

      • version: string

        It sets the version argument of the extension. Explanation for the field from the Prisma docs:

        This is the version of the extension to activate. If this argument is not specified, the value given in the extension's control file is used.

  • CLI Commands for Seeding the Database

    Use one of the following commands to run the seed functions:

    • wasp db seed

      If you've only defined a single seed function, this command runs it. If you've defined multiple seed functions, it asks you to choose one interactively.

    • wasp db seed <seed-name>

      This command runs the seed function with the specified name. The name is the identifier used in its import expression in the app.db.seeds list. -For example, to run the seed function devSeedSimple which was defined like this:

      main.wasp
      app MyApp {
      // ...
      db: {
      // ...
      seeds: [
      // ...
      import { devSeedSimple } from "@server/dbSeeds.js",
      ]
      }
      }

      Use the following command:

      wasp db seed devSeedSimple
    - - +For example, to run the seed function devSeedSimple which was defined like this:

    main.wasp
    app MyApp {
    // ...
    db: {
    // ...
    seeds: [
    // ...
    import { devSeedSimple } from "@server/dbSeeds.js",
    ]
    }
    }

    Use the following command:

    wasp db seed devSeedSimple
    + + \ No newline at end of file diff --git a/docs/data-model/crud.html b/docs/data-model/crud.html index bb6413bcfc..06b647dbad 100644 --- a/docs/data-model/crud.html +++ b/docs/data-model/crud.html @@ -19,15 +19,15 @@ - - + +
    -

    Automatic CRUD

    If you have a lot of experience writing full-stack apps, you probably ended up doing some of the same things many times: listing data, adding data, editing it, and deleting it.

    Wasp makes handling these boring bits easy by offering a higher-level concept called Automatic CRUD.

    With a single declaration, you can tell Wasp to automatically generate server-side logic (i.e., Queries and Actions) for creating, reading, updating and deleting Entities. As you update definitions for your Entities, Wasp automatically regenerates the backend logic.

    Early preview

    This feature is currently in early preview and we are actively working on it. Read more about our plans for CRUD operations.

    Overview

    Imagine we have a Task entity and we want to enable CRUD operations for it.

    main.wasp
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    psl=}

    We can then define a new crud called Tasks.

    We specify to use the Task entity and we enable the getAll, get, create and update operations (let's say we don't need the delete operation).

    main.wasp
    crud Tasks {
    entity: Task,
    operations: {
    getAll: {
    isPublic: true, // by default only logged in users can perform operations
    },
    get: {},
    create: {
    overrideFn: import { createTask } from "@server/tasks.js",
    },
    update: {},
    },
    }
    1. It uses default implementation for getAll, get, and update,
    2. ... while specifying a custom implementation for create.
    3. getAll will be public (no auth needed), while the rest of the operations will be private.

    Here's what it looks like when visualized:

    Automatic CRUD with Wasp
    Visualization of the Tasks crud declaration

    We can now use the CRUD queries and actions we just specified in our client code.

    Keep reading for an example of Automatic CRUD in action, or skip ahead for the API Reference

    Example: A Simple TODO App

    Let's create a full-app example that uses automatic CRUD. We'll stick to using the Task entity from the previous example, but we'll add a User entity and enable username and password based auth.

    Automatic CRUD with Wasp
    We are building a simple tasks app with username based auth

    Creating the App

    We can start by running wasp new tasksCrudApp and then adding the following to the main.wasp file:

    main.wasp
    app tasksCrudApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "Tasks Crud App",

    // We enabled auth and set the auth method to username and password
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login",
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    password String
    tasks Task[]
    psl=}

    // We defined a Task entity on which we'll enable CRUD later on
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    userId Int
    user User @relation(fields: [userId], references: [id])
    psl=}

    // Tasks app routes
    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import { MainPage } from "@client/MainPage.jsx",
    authRequired: true,
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@client/LoginPage.jsx",
    }

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@client/SignupPage.jsx",
    }

    We can then run wasp db migrate-dev to create the database and run the migrations.

    Adding CRUD to the Task Entity ✨

    Let's add the following crud declaration to our main.wasp file:

    main.wasp
    // ...

    crud Tasks {
    entity: Task,
    operations: {
    getAll: {},
    create: {
    overrideFn: import { createTask } from "@server/tasks.js",
    },
    },
    }

    You'll notice that we enabled only getAll and create operations. This means that only these operations will be available.

    We also overrode the create operation with a custom implementation. This means that the create operation will not be generated, but instead, the createTask function from @server/tasks.js will be used.

    Our Custom create Operation

    Here's the src/server/tasks.ts file:

    src/server/tasks.js
    import HttpError from '@wasp/core/HttpError.js'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401, 'User not authenticated.')
    }

    const { description, isDone } = args
    const { Task } = context.entities

    return await Task.create({
    data: {
    description,
    isDone,
    // Connect the task to the user that is creating it
    user: {
    connect: {
    id: context.user.id,
    },
    },
    },
    })
    }

    We made a custom create operation because we want to make sure that the task is connected to the user that is creating it. +

    Automatic CRUD

    If you have a lot of experience writing full-stack apps, you probably ended up doing some of the same things many times: listing data, adding data, editing it, and deleting it.

    Wasp makes handling these boring bits easy by offering a higher-level concept called Automatic CRUD.

    With a single declaration, you can tell Wasp to automatically generate server-side logic (i.e., Queries and Actions) for creating, reading, updating and deleting Entities. As you update definitions for your Entities, Wasp automatically regenerates the backend logic.

    Early preview

    This feature is currently in early preview and we are actively working on it. Read more about our plans for CRUD operations.

    Overview

    Imagine we have a Task entity and we want to enable CRUD operations for it.

    main.wasp
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    psl=}

    We can then define a new crud called Tasks.

    We specify to use the Task entity and we enable the getAll, get, create and update operations (let's say we don't need the delete operation).

    main.wasp
    crud Tasks {
    entity: Task,
    operations: {
    getAll: {
    isPublic: true, // by default only logged in users can perform operations
    },
    get: {},
    create: {
    overrideFn: import { createTask } from "@server/tasks.js",
    },
    update: {},
    },
    }
    1. It uses default implementation for getAll, get, and update,
    2. ... while specifying a custom implementation for create.
    3. getAll will be public (no auth needed), while the rest of the operations will be private.

    Here's what it looks like when visualized:

    Automatic CRUD with Wasp
    Visualization of the Tasks crud declaration

    We can now use the CRUD queries and actions we just specified in our client code.

    Keep reading for an example of Automatic CRUD in action, or skip ahead for the API Reference

    Example: A Simple TODO App

    Let's create a full-app example that uses automatic CRUD. We'll stick to using the Task entity from the previous example, but we'll add a User entity and enable username and password based auth.

    Automatic CRUD with Wasp
    We are building a simple tasks app with username based auth

    Creating the App

    We can start by running wasp new tasksCrudApp and then adding the following to the main.wasp file:

    main.wasp
    app tasksCrudApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "Tasks Crud App",

    // We enabled auth and set the auth method to username and password
    auth: {
    userEntity: User,
    methods: {
    usernameAndPassword: {},
    },
    onAuthFailedRedirectTo: "/login",
    },
    }

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    password String
    tasks Task[]
    psl=}

    // We defined a Task entity on which we'll enable CRUD later on
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean
    userId Int
    user User @relation(fields: [userId], references: [id])
    psl=}

    // Tasks app routes
    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import { MainPage } from "@client/MainPage.jsx",
    authRequired: true,
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import { LoginPage } from "@client/LoginPage.jsx",
    }

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import { SignupPage } from "@client/SignupPage.jsx",
    }

    We can then run wasp db migrate-dev to create the database and run the migrations.

    Adding CRUD to the Task Entity ✨

    Let's add the following crud declaration to our main.wasp file:

    main.wasp
    // ...

    crud Tasks {
    entity: Task,
    operations: {
    getAll: {},
    create: {
    overrideFn: import { createTask } from "@server/tasks.js",
    },
    },
    }

    You'll notice that we enabled only getAll and create operations. This means that only these operations will be available.

    We also overrode the create operation with a custom implementation. This means that the create operation will not be generated, but instead, the createTask function from @server/tasks.js will be used.

    Our Custom create Operation

    Here's the src/server/tasks.ts file:

    src/server/tasks.js
    import HttpError from '@wasp/core/HttpError.js'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401, 'User not authenticated.')
    }

    const { description, isDone } = args
    const { Task } = context.entities

    return await Task.create({
    data: {
    description,
    isDone,
    // Connect the task to the user that is creating it
    user: {
    connect: {
    id: context.user.id,
    },
    },
    },
    })
    }

    We made a custom create operation because we want to make sure that the task is connected to the user that is creating it. Automatic CRUD doesn't support this by default (yet!). Read more about the default implementations here.

    Using the Generated CRUD Operations on the Client

    And let's use the generated operations in our client code:

    pages/MainPage.jsx
    import { Tasks } from '@wasp/crud/Tasks'
    import { useState } from 'react'

    export const MainPage = () => {
    const { data: tasks, isLoading, error } = Tasks.getAll.useQuery()
    const createTask = Tasks.create.useAction()
    const [taskDescription, setTaskDescription] = useState('')

    function handleCreateTask() {
    createTask({ description: taskDescription, isDone: false })
    setTaskDescription('')
    }

    if (isLoading) return <div>Loading...</div>
    if (error) return <div>Error: {error.message}</div>
    return (
    <div
    style={{
    fontSize: '1.5rem',
    display: 'grid',
    placeContent: 'center',
    height: '100vh',
    }}
    >
    <div>
    <input
    value={taskDescription}
    onChange={(e) => setTaskDescription(e.target.value)}
    />
    <button onClick={handleCreateTask}>Create task</button>
    </div>
    <ul>
    {tasks.map((task) => (
    <li key={task.id}>{task.description}</li>
    ))}
    </ul>
    </div>
    )
    }

    And here are the login and signup pages, where we are using Wasp's Auth UI components:

    src/client/LoginPage.jsx
    import { LoginForm } from '@wasp/auth/forms/Login'
    import { Link } from 'react-router-dom'

    export function LoginPage() {
    return (
    <div
    style={{
    display: 'grid',
    placeContent: 'center',
    }}
    >
    <LoginForm />
    <div>
    <Link to="/signup">Create an account</Link>
    </div>
    </div>
    )
    }
    src/client/SignupPage.jsx
    import { SignupForm } from '@wasp/auth/forms/Signup'

    export function SignupPage() {
    return (
    <div
    style={{
    display: 'grid',
    placeContent: 'center',
    }}
    >
    <SignupForm />
    </div>
    )
    }

    That's it. You can now run wasp start and see the app in action. ⚡️

    You should see a login page and a signup page. After you log in, you should see a page with a list of tasks and a form to create new tasks.

    Future of CRUD Operations in Wasp

    CRUD operations currently have a limited set of knowledge about the business logic they are implementing.

    • For example, they don't know that a task should be connected to the user that is creating it. This is why we had to override the create operation in the example above.
    • Another thing: they are not aware of the authorization rules. For example, they don't know that a user should not be able to create a task for another user. In the future, we will be adding role-based authorization to Wasp, and we plan to make CRUD operations aware of the authorization rules.
    • Another issue is input validation and sanitization. For example, we might want to make sure that the task description is not empty.

    CRUD operations are a mechanism for getting a backend up and running quickly, but it depends on the information it can get from the Wasp app. The more information that it can pick up from your app, the more powerful it will be out of the box.

    We plan on supporting CRUD operations and growing them to become the easiest way to create your backend. Follow along on this GitHub issue to see how we are doing.

    API Reference

    CRUD declaration work on top of existing entity declaration. We'll fully explore the API using two examples:

    1. A basic CRUD declaration that relies on default options.
    2. A more involved CRUD declaration that uses extra options and overrides.

    Declaring a CRUD With Default Options

    If we create CRUD operations for an entity named Task, like this:

    main.wasp
    crud Tasks { // crud name here is "Tasks"
    entity: Task,
    operations: {
    get: {},
    getAll: {},
    create: {},
    update: {},
    delete: {},
    },
    }

    Wasp will give you the following default implementations:

    get - returns one entity based on the id field

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.findUnique({ where: { id: args.id } })

    getAll - returns all entities

    // ...

    // If the operation is not public, Wasp checks if an authenticated user
    // is making the request.

    return Task.findMany()

    create - creates a new entity

    // ...
    return Task.create({ data: args.data })

    update - updates an existing entity

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.update({ where: { id: args.id }, data: args.data })

    delete - deletes an existing entity

    // ...
    // Wasp uses the field marked with `@id` in Prisma schema as the id field.
    return Task.delete({ where: { id: args.id } })
    Current Limitations

    In the default create and update implementations, we are saving all of the data that the client sends to the server. This is not always desirable, i.e. in the case when the client should not be able to modify all of the data in the entity.

    In the future, we are planning to add validation of action input, where only the data that the user is allowed to change will be saved.

    For now, the solution is to provide an override function. You can override the default implementation by using the overrideFn option and implementing the validation logic yourself.

    Declaring a CRUD With All Available Options

    Here's an example of a more complex CRUD declaration:

    main.wasp
    crud Tasks { // crud name here is "Tasks"
    entity: Task,
    operations: {
    getAll: {
    isPublic: true, // optional, defaults to false
    },
    get: {},
    create: {
    overrideFn: import { createTask } from "@server/tasks.js", // optional
    },
    update: {},
    },
    }

    The CRUD declaration features the following fields:

    • entity: Entity required

      The entity to which the CRUD operations will be applied.

    • operations: { [operationName]: CrudOperationOptions } required

      The operations to be generated. The key is the name of the operation, and the value is the operation configuration.

      • The possible values for operationName are:
        • getAll
        • get
        • create
        • update
        • delete
      • CrudOperationOptions can have the following fields:
        • isPublic: bool - Whether the operation is public or not. If it is public, no auth is required to access it. If it is not public, it will be available only to authenticated users. Defaults to false.
        • overrideFn: ServerImport - The import statement of the optional override implementation in Node.js.

    Defining the overrides

    Like with actions and queries, you can define the implementation in a Javascript/Typescript file. The overrides are functions that take the following arguments:

    • args

      The arguments of the operation i.e. the data sent from the client.

    • context

      Context contains the user making the request and the entities object with the entity that's being operated on.

    For a usage example, check the example guide.

    Using the CRUD operations in client code

    On the client, you import the CRUD operations from @wasp/crud/{crud name}. The names of the imports are the same as the names of the operations. For example, if you have a CRUD called Tasks, you would import the operations like this:

    SomePage.jsx
    import { Tasks } from '@wasp/crud/Tasks'

    You can then access the operations like this:

    SomePage.jsx
    const { data } = Tasks.getAll.useQuery()
    const { data } = Tasks.get.useQuery({ id: 1 })
    const createAction = Tasks.create.useAction()
    const updateAction = Tasks.update.useAction()
    const deleteAction = Tasks.delete.useAction()

    All CRUD operations are implemented with Queries and Actions under the hood, which means they come with all the features you'd expect (e.g., automatic SuperJSON serialization, full-stack type safety when using TypeScript)


    Join our community on Discord, where we chat about full-stack web stuff. Join us to see what we are up to, share your opinions or get help with CRUD operations.

    - - + + \ No newline at end of file diff --git a/docs/data-model/entities.html b/docs/data-model/entities.html index 4d73cdcdaa..84878ad25b 100644 --- a/docs/data-model/entities.html +++ b/docs/data-model/entities.html @@ -19,15 +19,15 @@ - - + +
    -

    Entities

    Entities are the foundation of your app's data model. In short, an Entity defines a model in your database.

    Wasp uses the excellent Prisma ORM to implement all database functionality and occasionally enhances it with a thin abstraction layer. +

    Entities

    Entities are the foundation of your app's data model. In short, an Entity defines a model in your database.

    Wasp uses the excellent Prisma ORM to implement all database functionality and occasionally enhances it with a thin abstraction layer. Wasp Entities directly correspond to Prisma's data model. Still, you don't need to be familiar with Prisma to effectively use Wasp, as it comes with a simple API wrapper for working with Prisma's core features.

    The only requirement for defining Wasp Entities is familiarity with the Prisma Schema Language (PSL), a simple definition language explicitly created for defining models in Prisma. The language is declarative and very intuitive. We'll also go through an example later in the text, so there's no need to go and thoroughly learn it right away. Still, if you're curious, look no further than Prisma's official documentation:

    Defining an Entity

    As mentioned, an entity declaration represents a database model.

    Each Entity declaration corresponds 1-to-1 to Prisma's data model. Here's how you could define an Entity that represents a Task:

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    psl=}

    Let's go through this declaration in detail:

    • entity Task - This tells Wasp that we wish to define an Entity (i.e., database model) called Task. Wasp automatically creates a table called tasks.
    • {=psl ... psl=} - Wasp treats everything that comes between the two psl tags as PSL (Prisma Schema Language).

    The above PSL definition tells Wasp to create a table for storing Tasks where each task has three fields (i.e., the tasks table has three columns):

    • id - An integer value serving as a primary key. The database automatically generates it by incrementing the previously generated id.
    • description - A string value for storing the task's description.
    • isDone - A boolean value indicating the task's completion status. If you don't set it when creating a new task, the database sets it to false by default.

    Working with Entities

    Let's see how you can define and work with Wasp Entities:

    1. Create/update some Entities in your .wasp file.
    2. Run wasp db migrate-dev. This command syncs the database model with the Entity definitions in your .wasp file. It does this by creating migration scripts.
    3. Migration scripts are automatically placed in the migrations/ folder. Make sure to commit this folder into version control.
    4. Use Wasp's JavasScript API to work with the database when implementing Operations (we'll cover this in detail when we talk about operations).

    Using Entities in Operations

    Most of the time, you will be working with Entities within the context of Operations (Queries & Actions). We'll see how that's done on the next page.

    Using Entities directly

    If you need more control, you can directly interact with Entities by importing and using the Prisma Client. We recommend sticking with conventional Wasp-provided mechanisms, only resorting to directly using the Prisma client only if you need a feature Wasp doesn't provide.

    You can only use the Prisma Client in your Wasp server code. You can import it like this:

    import prismaClient from '@wasp/dbClient'`

    prismaClient.task.create({
    description: "Read the Entities doc",
    isDone: true // almost :)
    })

    Next steps

    Now that we've seen how to define Entities that represent Wasp's core data model, we'll see how to make the most of them in other parts of Wasp. Keep reading to learn all about Wasp Operations!

    - - + + \ No newline at end of file diff --git a/docs/data-model/operations/actions.html b/docs/data-model/operations/actions.html index 597d2fdca3..66f377ffd4 100644 --- a/docs/data-model/operations/actions.html +++ b/docs/data-model/operations/actions.html @@ -19,12 +19,12 @@ - - + +
    -

    Actions

    We'll explain what Actions are and how to use them. If you're looking for a detailed API specification, skip ahead to the API Reference.

    Actions are quite similar to Queries, but with a key distinction: Actions are designed to modify and add data, while Queries are solely for reading data. Examples of Actions include adding a comment to a blog post, liking a video, or updating a product's price.

    Actions and Queries work together to keep data caches up-to-date.

    tip

    Actions are almost identical to Queries in terms of their API. +

    Actions

    We'll explain what Actions are and how to use them. If you're looking for a detailed API specification, skip ahead to the API Reference.

    Actions are quite similar to Queries, but with a key distinction: Actions are designed to modify and add data, while Queries are solely for reading data. Examples of Actions include adding a comment to a blog post, liking a video, or updating a product's price.

    Actions and Queries work together to keep data caches up-to-date.

    tip

    Actions are almost identical to Queries in terms of their API. Therefore, if you're already familiar with Queries, you might find reading the entire guide repetitive.

    We instead recommend skipping ahead and only reading the differences between Queries and Actions, and consulting the API Reference as needed.

    Working with Actions

    Actions are declared in Wasp and implemented in NodeJS. Wasp runs Actions within the server's context, but it also generates code that allows you to call them from anywhere in your code (either client or server) using the same interface.

    This means you don't have to worry about building an HTTP API for the Action, managing server-side request handling, or even dealing with client-side response handling and caching. Instead, just focus on developing the business logic inside your Action, and let Wasp handle the rest!

    To create an Action, you need to:

    1. Declare the Action in Wasp using the action declaration.
    2. Implement the Action's NodeJS functionality.

    Once these two steps are completed, you can use the Action from anywhere in your code.

    Declaring Actions

    To create an Action in Wasp, we begin with an action declaration. Let's declare two Actions - one for creating a task, and another for marking tasks as done:

    main.wasp
    // ...

    action createTask {
    fn: import { createTask } from "@server/actions.js"
    }

    action markTaskAsDone {
    fn: import { markTaskAsDone } from "@server/actions.js"
    }

    If you want to know about all supported options for the action declaration, take a look at the API Reference.

    The names of Wasp Actions and their implementations don't necessarily have to match. However, to avoid confusion, we'll keep them the same.

    tip

    Wasp uses superjson under the hood. This means you're not limited to only sending and receiving JSON payloads.

    You can send and receive any superjson-compatible payload (like Dates, Sets, Lists, circular references, etc.) and let Wasp handle the (de)serialization.

    After declaring a Wasp Action, two important things happen:

    • Wasp generates a server-side NodeJS function that shares its name with the Action.

    • Wasp generates a client-side JavaScript function that shares its name with the Action (e.g., markTaskAsDone). @@ -42,7 +42,7 @@ Since both arguments are positional, you can name the parameters however you want, but we'll stick with args and context:

      1. args (type depends on the Action)

        An object containing the data passed in when calling the Action (e.g., filtering conditions). Check the usage examples to see how to pass this object to the Action.

      2. context (type depends on the Action)

        An additional context object passed into the Action by Wasp. This object contains user session information, as well as information about entities. Check the section about using entities in Actions to see how to use the entities field on the context object, or the auth section to see how to use the user object.

      Example

      The following Action:

      action createFoo {
      fn: import { createFoo } from "@server/actions.js"
      entities: [Foo]
      }

      Expects to find a named export createfoo from the file src/server/actions.js

      actions.js
      export const createFoo = (args, context) => {
      // implementation
      }

      The useAction Hook and Optimistic Updates

      Make sure you understand how Queries and Cache Invalidation work before reading this chapter.

      When using Actions in components, you can enhance them with the help of the useAction hook. This hook comes bundled with Wasp, and is used for decorating Wasp Actions. In other words, the hook returns a function whose API matches the original Action while also doing something extra under the hood (depending on how you configure it).

      The useAction hook accepts two arguments:

      • actionFn required

        The Wasp Action (i.e., the client-side Action function generated by Wasp based on a Action declaration) you wish to enhance.

      • actionOptions

        An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the useAction hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:

        • optimisticUpdates

          An array of objects where each object defines an optimistic update to perform on the Query cache. To define an optimistic update, you must specify the following properties:

          • getQuerySpecifier required

          A function returning the Query specifier (i.e., a value used to address the Query you want to update). A Query specifier is an array specifying the query function and arguments. For example, to optimistically update the Query used with useQuery(fetchFilteredTasks, {isDone: true }], your getQuerySpecifier function would have to return the array [fetchFilteredTasks, { isDone: true}]. Wasp will forward the argument you pass into the decorated Action to this function (i.e., you can use the properties of the added/changed item to address the Query).

          • updateQuery required

          The function used to perform the optimistic update. It should return the desired state of the cache. Wasp will call it with the following arguments:

          • item - The argument you pass into the decorated Action.
          • oldData - The currently cached value for the Query identified by the specifier.
      caution

      The updateQuery function must be a pure function. It must return the desired cache value identified by the getQuerySpecifier function and must not perform any side effects.

      Also, make sure you only update the Query caches affected by your Action causing the optimistic update (Wasp cannot yet verify this).

      Finally, your implementation of the updateQuery function should work correctly regardless of the state of oldData (e.g., don't rely on array positioning). If you need to do something else during your optimistic update, you can directly use react-query's lower-level API (read more about it here).

      Here's an example showing how to configure the Action markTaskAsDone that toggles a task's isDone status to perform an optimistic update:

      src/client/pages/Task.jsx
      import React from 'react'
      import { useQuery } from '@wasp/queries'
      import { useAction } from '@wasp/actions'
      import getTask from '@wasp/queries/getTask'
      import markTaskAsDone from '@wasp/actions/markTaskAsDone'

      const TaskPage = ({ id }) => {
      const { data: task } = useQuery(getTask, { id })
      const markTaskAsDoneOptimistically = useAction(markTaskAsDone, {
      optimisticUpdates: [
      {
      getQuerySpecifier: ({ id }) => [getTask, { id }],
      updateQuery: (_payload, oldData) => ({ ...oldData, isDone: true }),
      },
      ],
      })

      if (!task) {
      return <h1>"Loading"</h1>
      }

      const { description, isDone } = task
      return (
      <div>
      <p>
      <strong>Description: </strong>
      {description}
      </p>
      <p>
      <strong>Is done: </strong>
      {isDone ? 'Yes' : 'No'}
      </p>
      {isDone || (
      <button onClick={() => markTaskAsDoneOptimistically({ id })}>
      Mark as done.
      </button>
      )}
      </div>
      )
      }

      export default TaskPage

      Advanced usage

      The useAction hook currently only supports specifying optimistic updates. You can expect more features in future versions of Wasp.

      Wasp's optimistic update API is deliberately small and focuses exclusively on updating Query caches (as that's the most common use case). You might need an API that offers more options or a higher level of control. If that's the case, instead of using Wasp's useAction hook, you can use react-query's useMutation hook and directly work with their low-level API.

      If you decide to use react-query's API directly, you will need access to Query cache key. Wasp internally uses this key but abstracts it from the programmer. Still, you can easily obtain it by accessing the queryCacheKey property on any Query:

      import getTasks from '@wasp/queries/getTasks'

      const queryKey = getTasks.queryCacheKey
    - - + + \ No newline at end of file diff --git a/docs/data-model/operations/overview.html b/docs/data-model/operations/overview.html index 7f87a18185..e98795f6ba 100644 --- a/docs/data-model/operations/overview.html +++ b/docs/data-model/operations/overview.html @@ -19,14 +19,14 @@ - - + +
    -

    Overview

    While Entities enable help you define your app's data model and relationships, Operations are all about working with this data.

    There are two kinds of Operations: Queries and Actions. As their names suggest, +

    - - + + \ No newline at end of file diff --git a/docs/data-model/operations/queries.html b/docs/data-model/operations/queries.html index f673075260..9b238df627 100644 --- a/docs/data-model/operations/queries.html +++ b/docs/data-model/operations/queries.html @@ -19,12 +19,12 @@ - - + +
    -

    Queries

    We'll explain what Queries are and how to use them. If you're looking for a detailed API specification, skip ahead to the API Reference.

    You can use Queries to fetch data from the server. They shouldn't modify the server's state. +

    Queries

    We'll explain what Queries are and how to use them. If you're looking for a detailed API specification, skip ahead to the API Reference.

    You can use Queries to fetch data from the server. They shouldn't modify the server's state. Fetching all comments on a blog post, a list of users that liked a video, information about a single product based on its ID... All of these are perfect use cases for a Query.

    tip

    Queries are fairly similar to Actions in terms of their API. Therefore, if you're already familiar with Actions, you might find reading the entire guide repetitive.

    We instead recommend skipping ahead and only reading the differences between Queries and Actions, and consulting the API Reference as needed.

    Working with Queries

    You declare queries in the .wasp file and implement them using NodeJS. Wasp not only runs these queries within the server's context but also creates code that enables you to call them from any part of your codebase, whether it's on the client or server side.

    This means you don't have to build an HTTP API for your query, manage server-side request handling, or even deal with client-side response handling and caching. Instead, just concentrate on implementing the business logic inside your query, and let Wasp handle the rest!

    To create a Query, you must:

    1. Declare the Query in Wasp using the query declaration.
    2. Define the Query's NodeJS implementation.

    After completing these two steps, you'll be able to use the Query from any point in your code.

    Declaring Queries

    To create a Query in Wasp, we begin with a query declaration.

    Let's declare two Queries - one to fetch all tasks, and another to fetch tasks based on a filter, such as whether a task is done:

    main.wasp
    // ...

    query getAllTasks {
    fn: import { getAllTasks } from "@server/queries.js"
    }

    query getFilteredTasks {
    fn: import { getFilteredTasks } from "@server/queries.js"
    }

    If you want to know about all supported options for the query declaration, take a look at the API Reference.

    The names of Wasp Queries and their implementations don't need to match, but we'll keep them the same to avoid confusion.

    info

    You might have noticed that we told Wasp to import Query implementations that don't yet exist. Don't worry about that for now. We'll write the implementations imported from queries.ts in the next section.

    It's a good idea to start with the high-level concept (i.e., the Query declaration in the Wasp file) and only then deal with the implementation details (i.e., the Query's implementation in JavaScript).

    After declaring a Wasp Query, two important things happen:

    • Wasp generates a server-side NodeJS function that shares its name with the Query.

    • Wasp generates a client-side JavaScript function that shares its name with the Query (e.g., getFilteredTasks). @@ -46,7 +46,7 @@ behavior for this particular Query. If you want to change the global defaults, you can do so in the client setup function.

    For an example of usage, check this section.

    - - + + \ No newline at end of file diff --git a/docs/editor-setup.html b/docs/editor-setup.html index 2efda135ed..6f0213cc56 100644 --- a/docs/editor-setup.html +++ b/docs/editor-setup.html @@ -19,13 +19,13 @@ - - + +
    -

    Editor Setup

    note

    This page assumes you have already installed Wasp. If you do not have Wasp installed yet, check out the Quick Start guide.

    Wasp comes with the Wasp language server, which gives supported editors powerful support and integration with the language.

    VSCode

    Currently, Wasp only supports integration with VSCode. Install the Wasp language extension to get syntax highlighting and integration with the Wasp language server.

    The extension enables:

    • syntax highlighting for .wasp files
    • scaffolding of new project files
    • code completion
    • diagnostics (errors and warnings)
    • go to definition

    and more!

    - - +

    Editor Setup

    note

    This page assumes you have already installed Wasp. If you do not have Wasp installed yet, check out the Quick Start guide.

    Wasp comes with the Wasp language server, which gives supported editors powerful support and integration with the language.

    VSCode

    Currently, Wasp only supports integration with VSCode. Install the Wasp language extension to get syntax highlighting and integration with the Wasp language server.

    The extension enables:

    • syntax highlighting for .wasp files
    • scaffolding of new project files
    • code completion
    • diagnostics (errors and warnings)
    • go to definition

    and more!

    + + \ No newline at end of file diff --git a/docs/examples.html b/docs/examples.html index 815e43c93d..bb9c98b110 100644 --- a/docs/examples.html +++ b/docs/examples.html @@ -19,13 +19,13 @@ - - + +
    -

    Examples

    We have a constantly growing collection of fully-functioning example apps, which you can use to learn more about Wasp's features.

    The full list of examples can be found here. Here is a few of them:

    Todo App

    Waspello (Trello Clone)

    Waspleau (Realtime Statistics Dashboard)

    - - +

    Examples

    We have a constantly growing collection of fully-functioning example apps, which you can use to learn more about Wasp's features.

    The full list of examples can be found here. Here is a few of them:

    Todo App

    Waspello (Trello Clone)

    Waspleau (Realtime Statistics Dashboard)

    + + \ No newline at end of file diff --git a/docs/general/cli.html b/docs/general/cli.html index 7935ae74fd..429faf981b 100644 --- a/docs/general/cli.html +++ b/docs/general/cli.html @@ -19,13 +19,13 @@ - - + +
    -

    CLI Reference

    This guide provides an overview of the Wasp CLI commands, arguments, and options.

    Overview

    Once installed, you can use the wasp command from your command line.

    If you run the wasp command without any arguments, it will show you a list of available commands and their descriptions:

    USAGE
    wasp <command> [command-args]

    COMMANDS
    GENERAL
    new [<name>] [args] Creates a new Wasp project. Run it without arguments for interactive mode.
    OPTIONS:
    -t|--template <template-name>
    Check out the templates list here: https://github.com/wasp-lang/starters

    version Prints current version of CLI.
    waspls Run Wasp Language Server. Add --help to get more info.
    completion Prints help on bash completion.
    uninstall Removes Wasp from your system.
    IN PROJECT
    start Runs Wasp app in development mode, watching for file changes.
    start db Starts managed development database for you.
    db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.
    clean Deletes all generated code and other cached artifacts.
    Wasp equivalent of 'have you tried closing and opening it again?'.
    build Generates full web app code, ready for deployment. Use when deploying or ejecting.
    deploy Deploys your Wasp app to cloud hosting providers.
    telemetry Prints telemetry status.
    deps Prints the dependencies that Wasp uses in your project.
    dockerfile Prints the contents of the Wasp generated Dockerfile.
    info Prints basic information about current Wasp project.
    test Executes tests in your project.

    EXAMPLES
    wasp new MyApp
    wasp start
    wasp db migrate-dev

    Docs: https://wasp-lang.dev/docs
    Discord (chat): https://discord.gg/rzdnErX
    Newsletter: https://wasp-lang.dev/#signup

    Commands

    Creating a New Project

    • Use wasp new to start the interactive mode for setting up a new Wasp project.

      This will prompt you to input the project name and to select a template. The chosen template will then be used to generate the project directory with the specified name.

      $ wasp new
      Enter the project name (e.g. my-project) ▸ MyFirstProject
      Choose a starter template
      [1] basic (default)
      [2] saas
      [3] todo-ts
      ▸ 1

      🐝 --- Creating your project from the basic template... ---------------------------

      Created new Wasp app in ./MyFirstProject directory!
      To run it, do:

      cd MyFirstProject
      wasp start
    • To skip the interactive mode and create a new Wasp project with the default template, use wasp new <project-name>.

      $ wasp new MyFirstProject
      🐝 --- Creating your project from the basic template... ---------------------------

      Created new Wasp app in ./MyFirstProject directory!
      To run it, do:

      cd MyFirstProject
      wasp start

    Project Commands

    • wasp start launches the Wasp app in development mode. It automatically opens a browser tab with your application running and watches for any changes to .wasp or files in src/ to automatically reflect in the browser. It also shows messages from the web app, the server and the database on stdout/stderr.

    • wasp start db starts the database for you. This can be very handy since you don't need to spin up your own database or provide its connection URL to the Wasp app.

    • wasp clean removes all generated code and other cached artifacts. If using SQlite, it also deletes the SQlite database. Think of this as the Wasp version of the classic "turn it off and on again" solution.

      $ wasp clean

      Deleting .wasp/ directory...
      Deleted .wasp/ directory.
    • wasp build generates the complete web app code, which is ready for deployment. Use this command when you're deploying or ejecting. The generated code is stored in the .wasp/build folder.

    • wasp deploy makes it easy to get your app hosted on the web.

      Currently, Wasp offers support for Fly.io. If you prefer a different hosting provider, feel free to let us know on Discord or submit a PR by updating this TypeScript app.

      Read more about automatic deployment here.

    • wasp telemetry displays the status of telemetry.

      $ wasp telemetry

      Telemetry is currently: ENABLED
      Telemetry cache directory: /home/user/.cache/wasp/telemetry/
      Last time telemetry data was sent for this project: 2021-05-27 09:21:16.79537226 UTC
      Our telemetry is anonymized and very limited in its scope: check https://wasp-lang.dev/docs/telemetry for more details.

    • wasp deps lists the dependencies that Wasp uses in your project.

    • wasp info provides basic details about the current Wasp project.

    Database Commands

    Wasp provides a suite of commands for managing the database. These commands all begin with db and primarily execute Prisma commands behind the scenes.

    • wasp db migrate-dev synchronizes the development database with the current state of the schema (entities). If there are any changes in the schema, it generates a new migration and applies any pending migrations to the database.

      • The --name foo option allows you to specify a name for the migration, while the --create-only option lets you create an empty migration without applying it.
    • wasp db studio opens the GUI for inspecting your database.

    Bash Completion

    To set up Bash completion, run the wasp completion command and follow the instructions.

    Miscellaneous Commands

    • wasp version displays the current version of the CLI.

      $ wasp version

      0.11.1
    • wasp uninstall removes Wasp from your system.

      $ wasp uninstall

      🐝 --- Uninstalling Wasp ... ------------------------------------------------------

      We will remove the following directories:
      {home}/.local/share/wasp-lang/
      {home}/.cache/wasp/

      We will also remove the following files:
      {home}/.local/bin/wasp

      Are you sure you want to continue? [y/N]
      y

      ✅ --- Uninstalled Wasp -----------------------------------------------------------
    - - +

    CLI Reference

    This guide provides an overview of the Wasp CLI commands, arguments, and options.

    Overview

    Once installed, you can use the wasp command from your command line.

    If you run the wasp command without any arguments, it will show you a list of available commands and their descriptions:

    USAGE
    wasp <command> [command-args]

    COMMANDS
    GENERAL
    new [<name>] [args] Creates a new Wasp project. Run it without arguments for interactive mode.
    OPTIONS:
    -t|--template <template-name>
    Check out the templates list here: https://github.com/wasp-lang/starters

    version Prints current version of CLI.
    waspls Run Wasp Language Server. Add --help to get more info.
    completion Prints help on bash completion.
    uninstall Removes Wasp from your system.
    IN PROJECT
    start Runs Wasp app in development mode, watching for file changes.
    start db Starts managed development database for you.
    db <db-cmd> [args] Executes a database command. Run 'wasp db' for more info.
    clean Deletes all generated code and other cached artifacts.
    Wasp equivalent of 'have you tried closing and opening it again?'.
    build Generates full web app code, ready for deployment. Use when deploying or ejecting.
    deploy Deploys your Wasp app to cloud hosting providers.
    telemetry Prints telemetry status.
    deps Prints the dependencies that Wasp uses in your project.
    dockerfile Prints the contents of the Wasp generated Dockerfile.
    info Prints basic information about current Wasp project.
    test Executes tests in your project.

    EXAMPLES
    wasp new MyApp
    wasp start
    wasp db migrate-dev

    Docs: https://wasp-lang.dev/docs
    Discord (chat): https://discord.gg/rzdnErX
    Newsletter: https://wasp-lang.dev/#signup

    Commands

    Creating a New Project

    • Use wasp new to start the interactive mode for setting up a new Wasp project.

      This will prompt you to input the project name and to select a template. The chosen template will then be used to generate the project directory with the specified name.

      $ wasp new
      Enter the project name (e.g. my-project) ▸ MyFirstProject
      Choose a starter template
      [1] basic (default)
      [2] saas
      [3] todo-ts
      ▸ 1

      🐝 --- Creating your project from the basic template... ---------------------------

      Created new Wasp app in ./MyFirstProject directory!
      To run it, do:

      cd MyFirstProject
      wasp start
    • To skip the interactive mode and create a new Wasp project with the default template, use wasp new <project-name>.

      $ wasp new MyFirstProject
      🐝 --- Creating your project from the basic template... ---------------------------

      Created new Wasp app in ./MyFirstProject directory!
      To run it, do:

      cd MyFirstProject
      wasp start

    Project Commands

    • wasp start launches the Wasp app in development mode. It automatically opens a browser tab with your application running and watches for any changes to .wasp or files in src/ to automatically reflect in the browser. It also shows messages from the web app, the server and the database on stdout/stderr.

    • wasp start db starts the database for you. This can be very handy since you don't need to spin up your own database or provide its connection URL to the Wasp app.

    • wasp clean removes all generated code and other cached artifacts. If using SQlite, it also deletes the SQlite database. Think of this as the Wasp version of the classic "turn it off and on again" solution.

      $ wasp clean

      Deleting .wasp/ directory...
      Deleted .wasp/ directory.
    • wasp build generates the complete web app code, which is ready for deployment. Use this command when you're deploying or ejecting. The generated code is stored in the .wasp/build folder.

    • wasp deploy makes it easy to get your app hosted on the web.

      Currently, Wasp offers support for Fly.io. If you prefer a different hosting provider, feel free to let us know on Discord or submit a PR by updating this TypeScript app.

      Read more about automatic deployment here.

    • wasp telemetry displays the status of telemetry.

      $ wasp telemetry

      Telemetry is currently: ENABLED
      Telemetry cache directory: /home/user/.cache/wasp/telemetry/
      Last time telemetry data was sent for this project: 2021-05-27 09:21:16.79537226 UTC
      Our telemetry is anonymized and very limited in its scope: check https://wasp-lang.dev/docs/telemetry for more details.

    • wasp deps lists the dependencies that Wasp uses in your project.

    • wasp info provides basic details about the current Wasp project.

    Database Commands

    Wasp provides a suite of commands for managing the database. These commands all begin with db and primarily execute Prisma commands behind the scenes.

    • wasp db migrate-dev synchronizes the development database with the current state of the schema (entities). If there are any changes in the schema, it generates a new migration and applies any pending migrations to the database.

      • The --name foo option allows you to specify a name for the migration, while the --create-only option lets you create an empty migration without applying it.
    • wasp db studio opens the GUI for inspecting your database.

    Bash Completion

    To set up Bash completion, run the wasp completion command and follow the instructions.

    Miscellaneous Commands

    • wasp version displays the current version of the CLI.

      $ wasp version

      0.11.1
    • wasp uninstall removes Wasp from your system.

      $ wasp uninstall

      🐝 --- Uninstalling Wasp ... ------------------------------------------------------

      We will remove the following directories:
      {home}/.local/share/wasp-lang/
      {home}/.cache/wasp/

      We will also remove the following files:
      {home}/.local/bin/wasp

      Are you sure you want to continue? [y/N]
      y

      ✅ --- Uninstalled Wasp -----------------------------------------------------------
    + + \ No newline at end of file diff --git a/docs/general/language.html b/docs/general/language.html index 1384912de2..0d5967774c 100644 --- a/docs/general/language.html +++ b/docs/general/language.html @@ -19,15 +19,15 @@ - - + +
    -

    Wasp Language (.wasp)

    Wasp language (what you write in .wasp files) is a declarative, statically typed, domain-specific language (DSL).

    It is a quite simple language, closer to JSON, CSS or SQL than to e.g. Javascript or Python, since it is not a general programming language, but more of a configuration language.

    It is pretty intuitive to learn (there isn't much to learn really!) and you can probably do just fine without reading this page and learning from the rest of the docs as you go, but if you want a bit more formal definition and deeper understanding of how it works, then read on!

    Declarations

    The central point of Wasp language are declarations, and Wasp code is at the end just a bunch of declarations, each of them describing a part of your web app.

    app MyApp {
    title: "My app"
    }

    route RootRoute { path: "/", to: DashboardPage }

    page DashboardPage {
    component: import Dashboard from "@client/Dashboard.js"
    }

    In the example above we described a web app via three declarations: app MyApp { ... }, route RootRoute { ... } and page DashboardPage { ... }.

    Syntax for writing a declaration is <declaration_type> <declaration_name> <declaration_body>, where:

    • <declaration_type> is one of the declaration types offered by Wasp (app, route, ...)
    • <declaration_name> is an identifier chosen by you to name this specific declaration
    • <declaration_body> is the value/definition of the declaration itself, which has to match the specific declaration body type expected by the chosen declaration type.

    So, for app declaration above, we have:

    • declaration type app
    • declaration name MyApp (we could have used any other identifier, like foobar, foo_bar, or hi3Ho)
    • declaration body { title: "My app" }, which is a dictionary with field title that has string value. +

      Wasp Language (.wasp)

      Wasp language (what you write in .wasp files) is a declarative, statically typed, domain-specific language (DSL).

      It is a quite simple language, closer to JSON, CSS or SQL than to e.g. Javascript or Python, since it is not a general programming language, but more of a configuration language.

      It is pretty intuitive to learn (there isn't much to learn really!) and you can probably do just fine without reading this page and learning from the rest of the docs as you go, but if you want a bit more formal definition and deeper understanding of how it works, then read on!

      Declarations

      The central point of Wasp language are declarations, and Wasp code is at the end just a bunch of declarations, each of them describing a part of your web app.

      app MyApp {
      title: "My app"
      }

      route RootRoute { path: "/", to: DashboardPage }

      page DashboardPage {
      component: import Dashboard from "@client/Dashboard.js"
      }

      In the example above we described a web app via three declarations: app MyApp { ... }, route RootRoute { ... } and page DashboardPage { ... }.

      Syntax for writing a declaration is <declaration_type> <declaration_name> <declaration_body>, where:

      • <declaration_type> is one of the declaration types offered by Wasp (app, route, ...)
      • <declaration_name> is an identifier chosen by you to name this specific declaration
      • <declaration_body> is the value/definition of the declaration itself, which has to match the specific declaration body type expected by the chosen declaration type.

      So, for app declaration above, we have:

      • declaration type app
      • declaration name MyApp (we could have used any other identifier, like foobar, foo_bar, or hi3Ho)
      • declaration body { title: "My app" }, which is a dictionary with field title that has string value. Type of this dictionary is in line with the declaration body type of the app declaration type. If we provided something else, e.g. changed title to little, we would get a type error from Wasp compiler since that does not match the expected type of the declaration body for app.

      Each declaration has a meaning behind it that describes how your web app should behave and function.

      All the other types in Wasp language (primitive types (string, number), composite types (dict, list), enum types (DbSystem), ...) are used to define the declaration bodies.

      Complete List of Wasp Types

      Wasp's type system can be divided into two main categories of types: fundamental types and domain types.

      While fundamental types are here to be basic building blocks of a a language, and are very similar to what you would see in other popular languages, domain types are what makes Wasp special, as they model the concepts of a web app like page, route and similar.

      • Fundamental types (source of truth)
        • Primitive types
          • string ("foo", "they said: \"hi\"")
          • bool (true, false)
          • number (12, 14.5)
          • declaration reference (name of existing declaration: TaskPage, updateTask)
          • ServerImport (external server import) (import Foo from "@server/bar.js", import { Smth } from "@server/a/b.js")
            • The path has to start with "@server". The rest is relative to the src/server directory.
            • import has to be a default import import Foo or a single named import import { Foo }.
          • ClientImport (external client import) (import Foo from "@client/bar.js", import { Smth } from "@client/a/b.js")
            • The path has to start with "@client". The rest is relative to the src/client directory.
            • import has to be a default import import Foo or a single named import import { Foo }.
          • json ({=json { a: 5, b: ["hi"] } json=})
          • psl (Prisma Schema Language) ({=psl <psl data model syntax> psl=})
        • Composite types
          • dict (dictionary) ({ a: 5, b: "foo" })
          • list ([1, 2, 3])
          • tuple ((1, "bar"), (2, 4, true))
            • Tuples can be of size 2, 3 and 4.
      • Domain types (source of truth)
        • Declaration types
          • action
          • api
          • apiNamespace
          • app
          • entity
          • job
          • page
          • query
          • route
          • crud
        • Enum types
          • DbSystem
          • HttpMethod
          • JobExecutor
          • EmailProvider

      You can find more details about each of the domain types, both regarding their body types and what they mean, in the corresponding doc pages covering their features.

    - - + + \ No newline at end of file diff --git a/docs/language/features.html b/docs/language/features.html index 4b7eecad93..a7758f37a2 100644 --- a/docs/language/features.html +++ b/docs/language/features.html @@ -19,8 +19,8 @@ - - + +
    @@ -110,7 +110,7 @@ Example: you could do DATABASE_URL=<my-db-url> wasp db seed myProdSeed to seed data for a fresh staging or production database.

    Migrating from SQLite to PostgreSQL

    To run Wasp app in production, you will need to switch from SQLite to PostgreSQL.

    1. Set app.db.system to PostgreSQL.
    2. Delete old migrations, since they are SQLite migrations and can't be used with PostgreSQL: rm -r migrations/.
    3. Run wasp start db to start your new db running (or check instructions above if you prefer using your own db). Leave it running, since we need it for the next step.
    4. In a different terminal, run wasp db migrate-dev to apply new changes and create new, initial migration.
    5. That is it, you are all done!

    Seeding

    Database seeding is a term for populating database with some initial data.

    Seeding is most commonly used for two following scenarios:

    1. To put development database into a state convenient for testing / playing with it.
    2. To initialize dev/staging/prod database with some essential data needed for it to be useful, for example default currencies in a Currency table.

    Writing a seed function

    Wasp enables you to define multiple seed functions via app.db.seeds:

    app MyApp {
    // ...
    db: {
    // ...
    seeds: [
    import { devSeedSimple } from "@server/dbSeeds.js",
    import { prodSeed } from "@server/dbSeeds.js"
    ]
    }
    }

    Each seed function is expected to be an async function that takes one argument, prismaClient, which is a Prisma Client instance that you can use to interact with the database. This is the same instance of Prisma Client that Wasp uses internally, so you e.g. get password hashing for free.

    Since a seed function is part of the server-side code, it can also import other server-side code, so you can and will normally want to import and use Actions to perform the seeding.

    Example of a seed function that imports an Action (+ a helper function to create a user):

    import { createTask } from './actions.js'

    export const devSeedSimple = async (prismaClient) => {
    const user = await createUser(prismaClient, {
    username: "RiuTheDog",
    password: "bark1234"
    })

    await createTask(
    { description: "Chase the cat" },
    { user, entities: { Task: prismaClient.task } }
    )
    }

    async function createUser (prismaClient, data) {
    const { password, ...newUser } = await prismaClient.user.create({ data })
    return newUser
    }

    Running seed functions

    • wasp db seed: If you have just one seed function, it will run it. If you have multiple, it will interactively ask you to choose one to run.

    • wasp db seed <seed-name>: It will run the seed function with the specified name, where the name is the identifier you used in its import expression in the app.db.seeds list. Example: wasp db seed devSeedSimple.

    tip

    Often you will want to call wasp db seed right after you ran wasp db reset: first you empty your database, then you fill it with some initial data.

    Email sender

    provider: EmailProvider (required)

    We support multiple different providers for sending e-mails: SMTP, SendGrid and Mailgun.

    SMTP

    SMTP e-mail sender uses your SMTP server to send e-mails.

    Read our guide for setting up SMTP for more details.

    SendGrid

    SendGrid is a popular service for sending e-mails that provides both API and SMTP methods of sending e-mails. We use their official SDK for sending e-mails.

    Check out our guide for setting up Sendgrid for more details.

    Mailgun

    Mailgun is a popular service for sending e-mails that provides both API and SMTP methods of sending e-mails. We use their official SDK for sending e-mails.

    Check out our guide for setting up Mailgun for more details.

    defaultSender: EmailFromField (optional)

    You can optionally provide a default sender info that will be used when you don't provide it explicitly when sending an e-mail.

    app MyApp {
    title: "My app",
    // ...
    emailSender: {
    provider: SMTP,
    defaultFrom: {
    name: "Hello",
    email: "hello@itsme.com"
    },
    },
    }

    After you set up the email sender, you can use it in your code to send e-mails. For example, you can send an e-mail when a user signs up, or when a user resets their password.

    Sending e-mails

    Sending emails while developing

    When you run your app in development mode, the emails are not sent. Instead, they are logged to the console.

    To enable sending emails in development mode, you need to set the SEND_EMAILS_IN_DEVELOPMENT env variable to true in your .env.server file.

    To send an e-mail, you can use the emailSender that is provided by the @wasp/email module.

    src/actions/sendEmail.js
    import { emailSender } from '@wasp/email/index.js'

    // In some action handler...
    const info = await emailSender.send({
    to: 'user@domain.com',
    subject: 'Saying hello',
    text: 'Hello world',
    html: 'Hello <strong>world</strong>'
    })
    - - + + \ No newline at end of file diff --git a/docs/project/client-config.html b/docs/project/client-config.html index e24410f015..3a70346cf4 100644 --- a/docs/project/client-config.html +++ b/docs/project/client-config.html @@ -19,12 +19,12 @@ - - + +
    -

    Client Config

    You can configure the client using the client field inside the app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    client: {
    rootComponent: import Root from "@client/Root.jsx",
    setupFn: import mySetupFunction from "@client/myClientSetupCode.js"
    }
    }

    Root Component

    Wasp gives you the option to define a "wrapper" component for your React app.

    It can be used for a variety of purposes, but the most common ones are:

    • Defining a common layout for your application.
    • Setting up various providers that your application needs.

    Defining a Common Layout

    Let's define a common layout for your application:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    client: {
    rootComponent: import Root from "@client/Root.jsx",
    }
    }
    src/client/Root.jsx
    export default function Root({ children }) {
    return (
    <div>
    <header>
    <h1>My App</h1>
    </header>
    {children}
    <footer>
    <p>My App footer</p>
    </footer>
    </div>
    )
    }

    Setting up a Provider

    This is how to set up various providers that your application needs:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    client: {
    rootComponent: import Root from "@client/Root.jsx",
    }
    }
    src/client/Root.jsx
    import store from './store'
    import { Provider } from 'react-redux'

    export default function Root({ children }) {
    return <Provider store={store}>{children}</Provider>
    }

    As long as you render the children, you can do whatever you want in your root +

    Client Config

    You can configure the client using the client field inside the app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    client: {
    rootComponent: import Root from "@client/Root.jsx",
    setupFn: import mySetupFunction from "@client/myClientSetupCode.js"
    }
    }

    Root Component

    Wasp gives you the option to define a "wrapper" component for your React app.

    It can be used for a variety of purposes, but the most common ones are:

    • Defining a common layout for your application.
    • Setting up various providers that your application needs.

    Defining a Common Layout

    Let's define a common layout for your application:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    client: {
    rootComponent: import Root from "@client/Root.jsx",
    }
    }
    src/client/Root.jsx
    export default function Root({ children }) {
    return (
    <div>
    <header>
    <h1>My App</h1>
    </header>
    {children}
    <footer>
    <p>My App footer</p>
    </footer>
    </div>
    )
    }

    Setting up a Provider

    This is how to set up various providers that your application needs:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    client: {
    rootComponent: import Root from "@client/Root.jsx",
    }
    }
    src/client/Root.jsx
    import store from './store'
    import { Provider } from 'react-redux'

    export default function Root({ children }) {
    return <Provider store={store}>{children}</Provider>
    }

    As long as you render the children, you can do whatever you want in your root component.

    Read more about the root component in the API Reference.

    Setup Function

    setupFn declares a function that Wasp executes on the client before everything else.

    Running Some Code

    We can run any code we want in the setup function.

    For example, here's a setup function that logs a message every hour:

    src/client/myClientSetupCode.js
    export default async function mySetupFunction() {
    let count = 1
    setInterval(
    () => console.log(`You have been online for ${count++} hours.`),
    1000 * 60 * 60
    )
    }

    Overriding Default Behaviour for Queries

    info

    You can change the options for a single Query using the options object, as described here.

    Wasp's useQuery hook uses react-query's useQuery hook under the hood. Since react-query comes configured with aggressive but sane default options, you most likely won't have to change those defaults for all Queries.

    If you do need to change the global defaults, you can do so inside the client setup function.

    Wasp exposes a configureQueryClient hook that lets you configure react-query's QueryClient object:

    src/client/myClientSetupCode.js
    import { configureQueryClient } from '@wasp/queryClient'

    export default async function mySetupFunction() {
    // ... some setup
    configureQueryClient({
    defaultOptions: {
    queries: {
    staleTime: Infinity,
    },
    },
    })
    // ... some more setup
    }

    Make sure to pass in an object expected by the QueryClient's constructor, as explained in react-query's docs.

    Read more about the setup function in the API Reference.

    API Reference

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    client: {
    rootComponent: import Root from "@client/Root.jsx",
    setupFn: import mySetupFunction from "@client/myClientSetupCode.js"
    }
    }

    Client has the following options:

    • rootComponent: ClientImport

      rootComponent defines the root component of your client application. It is @@ -32,7 +32,7 @@ It must render its children, which are the actual pages of your application.

      Here's an example of a root component that both sets up a provider and renders a custom layout:

      src/client/Root.jsx
      import store from './store'
      import { Provider } from 'react-redux'

      export default function Root({ children }) {
      return (
      <Provider store={store}>
      <Layout>{children}</Layout>
      </Provider>
      )
      }

      function Layout({ children }) {
      return (
      <div>
      <header>
      <h1>My App</h1>
      </header>
      {children}
      <footer>
      <p>My App footer</p>
      </footer>
      </div>
      )
      }
    • setupFn: ClientImport

      You can use this function to perform any custom setup (e.g., setting up client-side periodic jobs).

      src/client/myClientSetupCode.js
      export default async function mySetupFunction() {
      // Run some code
      }
    - - + + \ No newline at end of file diff --git a/docs/project/css-frameworks.html b/docs/project/css-frameworks.html index c03a2a7064..17c1aab253 100644 --- a/docs/project/css-frameworks.html +++ b/docs/project/css-frameworks.html @@ -19,13 +19,13 @@ - - + +
    -

    CSS Frameworks

    Tailwind

    To enable support for Tailwind in your project, you need to add two config files — tailwind.config.cjs and postcss.config.cjs — to the root directory.

    With these files present, Wasp installs the necessary dependencies and copies your configuration to the generated project. You can then use Tailwind CSS directives in your CSS and Tailwind classes on your React components.

    tree .
    .
    ├── main.wasp
    ├── src
    │   ├── client
    │   │   ├── tsconfig.json
    │   │   ├── Main.css
    │   │   ├── MainPage.js
    │   │   └── waspLogo.png
    │   ├── server
    │   │   └── tsconfig.json
    │   └── shared
    │   └── tsconfig.json
    ├── postcss.config.cjs
    └── tailwind.config.cjs
    Tailwind not working?

    If you can not use Tailwind after adding the required config files, make sure to restart wasp start. This is sometimes needed to ensure that Wasp picks up the changes and enables Tailwind integration.

    Enabling Tailwind Step-by-Step

    caution

    Make sure to use the .cjs extension for these config files, if you name them with a .js extension, Wasp will not detect them.

    1. Add ./tailwind.config.cjs.

      ./tailwind.config.cjs
      /** @type {import('tailwindcss').Config} */
      module.exports = {
      content: [ "./src/**/*.{js,jsx,ts,tsx}" ],
      theme: {
      extend: {},
      },
      plugins: [],
      }
    2. Add ./postcss.config.cjs.

      ./postcss.config.cjs
      module.exports = {
      plugins: {
      tailwindcss: {},
      autoprefixer: {},
      },
      }
    3. Import Tailwind into your CSS file. For example, in a new project you might import Tailwind into Main.css.

      ./src/client/Main.css
      @tailwind base;
      @tailwind components;
      @tailwind utilities;

      /* ... */
    4. Start using Tailwind 🥳

      ./src/client/MainPage.jsx
      // ...

      <h1 className="text-3xl font-bold underline">
      Hello world!
      </h1>

      // ...

    Adding Tailwind Plugins

    To add Tailwind plugins, add it to dependencies in your main.wasp file and to the plugins list in your tailwind.config.cjs file:

    ./main.wasp
    app todoApp {
    // ...
    dependencies: [
    ("@tailwindcss/forms", "^0.5.3"),
    ("@tailwindcss/typographjy", "^0.5.7"),
    ],
    // ...
    }
    ./tailwind.config.cjs
    /** @type {import('tailwindcss').Config} */
    module.exports = {
    // ...
    plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
    ],
    // ...
    }
    - - +

    CSS Frameworks

    Tailwind

    To enable support for Tailwind in your project, you need to add two config files — tailwind.config.cjs and postcss.config.cjs — to the root directory.

    With these files present, Wasp installs the necessary dependencies and copies your configuration to the generated project. You can then use Tailwind CSS directives in your CSS and Tailwind classes on your React components.

    tree .
    .
    ├── main.wasp
    ├── src
    │   ├── client
    │   │   ├── tsconfig.json
    │   │   ├── Main.css
    │   │   ├── MainPage.js
    │   │   └── waspLogo.png
    │   ├── server
    │   │   └── tsconfig.json
    │   └── shared
    │   └── tsconfig.json
    ├── postcss.config.cjs
    └── tailwind.config.cjs
    Tailwind not working?

    If you can not use Tailwind after adding the required config files, make sure to restart wasp start. This is sometimes needed to ensure that Wasp picks up the changes and enables Tailwind integration.

    Enabling Tailwind Step-by-Step

    caution

    Make sure to use the .cjs extension for these config files, if you name them with a .js extension, Wasp will not detect them.

    1. Add ./tailwind.config.cjs.

      ./tailwind.config.cjs
      /** @type {import('tailwindcss').Config} */
      module.exports = {
      content: [ "./src/**/*.{js,jsx,ts,tsx}" ],
      theme: {
      extend: {},
      },
      plugins: [],
      }
    2. Add ./postcss.config.cjs.

      ./postcss.config.cjs
      module.exports = {
      plugins: {
      tailwindcss: {},
      autoprefixer: {},
      },
      }
    3. Import Tailwind into your CSS file. For example, in a new project you might import Tailwind into Main.css.

      ./src/client/Main.css
      @tailwind base;
      @tailwind components;
      @tailwind utilities;

      /* ... */
    4. Start using Tailwind 🥳

      ./src/client/MainPage.jsx
      // ...

      <h1 className="text-3xl font-bold underline">
      Hello world!
      </h1>

      // ...

    Adding Tailwind Plugins

    To add Tailwind plugins, add it to dependencies in your main.wasp file and to the plugins list in your tailwind.config.cjs file:

    ./main.wasp
    app todoApp {
    // ...
    dependencies: [
    ("@tailwindcss/forms", "^0.5.3"),
    ("@tailwindcss/typographjy", "^0.5.7"),
    ],
    // ...
    }
    ./tailwind.config.cjs
    /** @type {import('tailwindcss').Config} */
    module.exports = {
    // ...
    plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
    ],
    // ...
    }
    + + \ No newline at end of file diff --git a/docs/project/custom-vite-config.html b/docs/project/custom-vite-config.html new file mode 100644 index 0000000000..2b292d9b55 --- /dev/null +++ b/docs/project/custom-vite-config.html @@ -0,0 +1,31 @@ + + + + + +Custom Vite Config | Wasp + + + + + + + + + + + + + + + + + + + +
    +

    Custom Vite Config

    Wasp uses Vite for serving the client during development and bundling it for production. If you want to customize the Vite config, you can do that by editing the vite.config.ts file in your src/client directory.

    Wasp will use your config and merge it with the default Wasp's Vite config.

    Vite config customization can be useful for things like:

    • Adding custom Vite plugins.
    • Customising the dev server.
    • Customising the build process.

    Be careful with making changes to the Vite config, as it can break the Wasp's client build process. Check out the default Vite config here to see what you can change.

    Examples

    Below are some examples of how you can customize the Vite config.

    Changing the Dev Server Behaviour

    If you want to stop Vite from opening the browser automatically when you run wasp start, you can do that by customizing the open option.

    src/client/vite.config.js
    export default {
    server: {
    open: false,
    },
    }

    Custom Dev Server Port

    You have access to all of the Vite dev server options in your custom Vite config. You can change the dev server port by setting the port option.

    src/client/vite.config.js
    export default {
    server: {
    port: 4000,
    },
    }
    .env.server
    WASP_WEB_CLIENT_URL=http://localhost:4000
    Changing the dev server port

    ⚠️ Be careful when changing the dev server port, you'll need to update the WASP_WEB_CLIENT_URL env var in your .env.server file.

    Customising the Base Path

    If you, for example, want to serve the client from a different path than /, you can do that by customizing the base option.

    src/client/vite.config.js
    export default {
    base: '/my-app/',
    }
    + + + + \ No newline at end of file diff --git a/docs/project/customizing-app.html b/docs/project/customizing-app.html index 0536460f27..4b47a277bc 100644 --- a/docs/project/customizing-app.html +++ b/docs/project/customizing-app.html @@ -19,14 +19,14 @@ - - + +
    -

    Customizing the App

    Each Wasp project can have only one app type declaration. It is used to configure your app and its components.

    app todoApp {
    wasp: {
    version: "^0.11.1"
    },
    title: "ToDo App",
    head: [
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
    ]
    }

    We'll go through some common customizations you might want to do to your app. For more details on each of the fields, check out the API Reference.

    Changing the App Title

    You may want to change the title of your app, which appears in the browser tab, next to the favicon. You can change it by changing the title field of your app declaration:

    app myApp {
    wasp: {
    version: "^0.11.1"
    },
    title: "BookFace"
    }

    Adding Additional Lines to the Head

    If you are looking to add additional style sheets or scripts to your app, you can do so by adding them to the head field of your app declaration.

    An example of adding extra style sheets and scripts:

    app myApp {
    wasp: {
    version: "^0.11.1"
    },
    title: "My App",
    head: [ // optional
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />",
    "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js\"></script>",
    "<meta name=\"viewport\" content=\"minimum-scale=1, initial-scale=1, width=device-width\" />"
    ]
    }

    API Reference

    app todoApp {
    wasp: {
    version: "^0.11.1"
    },
    title: "ToDo App",
    head: [
    "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
    ],
    auth: {
    // ...
    },
    client: {
    // ...
    },
    server: {
    // ...
    },
    db: {
    // ...
    },
    dependencies: [
    // ...
    ],
    emailSender: {
    // ...
    },
    webSocket: {
    // ...
    }
    }

    The app declaration has the following fields:

    - - + + \ No newline at end of file diff --git a/docs/project/dependencies.html b/docs/project/dependencies.html index 2d96b67b00..d22168473b 100644 --- a/docs/project/dependencies.html +++ b/docs/project/dependencies.html @@ -19,15 +19,15 @@ - - + +
    -

    Dependencies

    Specifying npm dependencies in Wasp project is done via the dependencies field in the app declaration, in the following way:

    app MyApp {
    title: "My app",
    // ...
    dependencies: [
    ("redux", "^4.0.5"),
    ("react-redux", "^7.1.3")
    ]
    }

    You will need to re-run wasp start after adding a dependency for Wasp to pick it up.

    The quickest way to find out the latest version of a package is to run:

    npm view <package-name> version
    Using Packages that are Already Used by Wasp Internally

    In the current implementation of Wasp, if Wasp is already internally using a certain npm dependency with a certain version specified, you are not allowed to define that same npm dependency yourself while specifying a different version. +

    Dependencies

    Specifying npm dependencies in Wasp project is done via the dependencies field in the app declaration, in the following way:

    app MyApp {
    title: "My app",
    // ...
    dependencies: [
    ("redux", "^4.0.5"),
    ("react-redux", "^7.1.3")
    ]
    }

    You will need to re-run wasp start after adding a dependency for Wasp to pick it up.

    The quickest way to find out the latest version of a package is to run:

    npm view <package-name> version
    Using Packages that are Already Used by Wasp Internally

    In the current implementation of Wasp, if Wasp is already internally using a certain npm dependency with a certain version specified, you are not allowed to define that same npm dependency yourself while specifying a different version. If you do that, you will get an error message telling you which exact version you have to use for that dependency. This means Wasp dictates exact versions of certain packages, so for example you can't choose version of React you want to use.

    We are currently working on a restructuring that will solve this and some other quirks that the current dependency system has: check issue #734 to follow our progress.

    - - + + \ No newline at end of file diff --git a/docs/project/env-vars.html b/docs/project/env-vars.html index 26d394abad..9ce86e479f 100644 --- a/docs/project/env-vars.html +++ b/docs/project/env-vars.html @@ -19,13 +19,13 @@ - - + +
    -

    Env Variables

    Environment variables are used to configure projects based on the context in which they run. This allows them to exhibit different behaviors in different environments, such as development, staging, or production.

    For instance, during development, you may want your project to connect to a local development database running on your machine, but in production, you may prefer it to connect to the production database. Similarly, in development, you may want to use a test Stripe account, while in production, your app should use a real Stripe account.

    While some env vars are required by Wasp, such as the database connection or secrets for social auth, you can also define your env vars for any other useful purposes.

    In Wasp, you can use environment variables in both the client and the server code.

    Client Env Vars

    Client environment variables are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    To enable Wasp to pick them up, client environment variables must be prefixed with REACT_APP_, for example: REACT_APP_SOME_VAR_NAME=....

    You can read them from the client code like this:

    src/App.js
    console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)

    Check below on how to define them.

    Server Env Vars

    In server environment variables, you can store secret values (e.g. secret API keys) since are not publicly readable. You can define them without any special prefix, such as SOME_VAR_NAME=....

    You can read them in the server code like this:

    console.log(process.env.SOME_VAR_NAME)

    Check below on how to define them.

    Defining Env Vars in Development

    During development, there are two ways to provide env vars to your Wasp project:

    1. Using .env files. (recommended)
    2. Using shell. (useful for overrides)

    1. Using .env (dotenv) Files

    Env vars usage in development

    This is the recommended method for providing env vars to your Wasp project during development.

    In the root of your Wasp project you can create two distinct files:

    • .env.server for env vars that will be provided to the server.

      Variables are defined in these files in the form of NAME=VALUE, for example:

      .env.server
      DATABASE_URL=postgresql://localhost:5432
      SOME_VAR_NAME=somevalue
    • .env.client for env vars that will be provided to the client.

      Variables are defined in these files in the form of NAME=VALUE, for example:

      .env.client
      REACT_APP_SOME_VAR_NAME=somevalue

    These files should not be committed to version control, and they are already ignored by default in the .gitignore file that comes with Wasp.

    2. Using Shell

    If you set environment variables in the shell where you run your Wasp commands (e.g., wasp start), Wasp will recognize them.

    You can set environment variables in the .profile or a similar file, or by defining them at the start of a command:

    SOME_VAR_NAME=SOMEVALUE wasp start

    This is not specific to Wasp and is simply how environment variables can be set in the shell.

    Defining environment variables in this way can be cumbersome even for a single project and even more challenging to manage if you have multiple Wasp projects. Therefore, we do not recommend this as a default method for providing environment variables to Wasp projects. However, it can be useful for occasionally overriding specific environment variables because environment variables set this way take precedence over those defined in .env files.

    Defining Env Vars in Production

    While in development, we had the option of using .env files which made it easy to define and manage env vars. However, in production, we need to provide env vars differently.

    Env vars usage in development and production

    Client Env Vars

    Client env vars are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    You should provide them to the build command, for example:

    REACT_APP_SOME_VAR_NAME=somevalue npm run build
    How it works

    What happens behind the scenes is that Wasp will replace all occurrences of import.meta.env.REACT_APP_SOME_VAR_NAME with the value you provided. This is done during the build process, so the value is embedded into the client code.

    Read more about it in Vite's docs.

    Server Env Vars

    The way you provide env vars to your Wasp project in production depends on where you deploy it. For example, if you deploy your project to Fly, you can define them using the flyctl CLI tool:

    flyctl secrets set SOME_VAR_NAME=somevalue

    You can read a lot more details in the deployment section of the docs. We go into detail on how to define env vars for each deployment option.

    - - +

    Env Variables

    Environment variables are used to configure projects based on the context in which they run. This allows them to exhibit different behaviors in different environments, such as development, staging, or production.

    For instance, during development, you may want your project to connect to a local development database running on your machine, but in production, you may prefer it to connect to the production database. Similarly, in development, you may want to use a test Stripe account, while in production, your app should use a real Stripe account.

    While some env vars are required by Wasp, such as the database connection or secrets for social auth, you can also define your env vars for any other useful purposes.

    In Wasp, you can use environment variables in both the client and the server code.

    Client Env Vars

    Client environment variables are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    To enable Wasp to pick them up, client environment variables must be prefixed with REACT_APP_, for example: REACT_APP_SOME_VAR_NAME=....

    You can read them from the client code like this:

    src/App.js
    console.log(import.meta.env.REACT_APP_SOME_VAR_NAME)

    Check below on how to define them.

    Server Env Vars

    In server environment variables, you can store secret values (e.g. secret API keys) since are not publicly readable. You can define them without any special prefix, such as SOME_VAR_NAME=....

    You can read them in the server code like this:

    console.log(process.env.SOME_VAR_NAME)

    Check below on how to define them.

    Defining Env Vars in Development

    During development, there are two ways to provide env vars to your Wasp project:

    1. Using .env files. (recommended)
    2. Using shell. (useful for overrides)

    1. Using .env (dotenv) Files

    Env vars usage in development

    This is the recommended method for providing env vars to your Wasp project during development.

    In the root of your Wasp project you can create two distinct files:

    • .env.server for env vars that will be provided to the server.

      Variables are defined in these files in the form of NAME=VALUE, for example:

      .env.server
      DATABASE_URL=postgresql://localhost:5432
      SOME_VAR_NAME=somevalue
    • .env.client for env vars that will be provided to the client.

      Variables are defined in these files in the form of NAME=VALUE, for example:

      .env.client
      REACT_APP_SOME_VAR_NAME=somevalue

    These files should not be committed to version control, and they are already ignored by default in the .gitignore file that comes with Wasp.

    2. Using Shell

    If you set environment variables in the shell where you run your Wasp commands (e.g., wasp start), Wasp will recognize them.

    You can set environment variables in the .profile or a similar file, or by defining them at the start of a command:

    SOME_VAR_NAME=SOMEVALUE wasp start

    This is not specific to Wasp and is simply how environment variables can be set in the shell.

    Defining environment variables in this way can be cumbersome even for a single project and even more challenging to manage if you have multiple Wasp projects. Therefore, we do not recommend this as a default method for providing environment variables to Wasp projects. However, it can be useful for occasionally overriding specific environment variables because environment variables set this way take precedence over those defined in .env files.

    Defining Env Vars in Production

    While in development, we had the option of using .env files which made it easy to define and manage env vars. However, in production, we need to provide env vars differently.

    Env vars usage in development and production

    Client Env Vars

    Client env vars are embedded into the client code during the build and shipping process, making them public and readable by anyone. Therefore, you should never store secrets in them (such as secret API keys).

    You should provide them to the build command, for example:

    REACT_APP_SOME_VAR_NAME=somevalue npm run build
    How it works

    What happens behind the scenes is that Wasp will replace all occurrences of import.meta.env.REACT_APP_SOME_VAR_NAME with the value you provided. This is done during the build process, so the value is embedded into the client code.

    Read more about it in Vite's docs.

    Server Env Vars

    The way you provide env vars to your Wasp project in production depends on where you deploy it. For example, if you deploy your project to Fly, you can define them using the flyctl CLI tool:

    flyctl secrets set SOME_VAR_NAME=somevalue

    You can read a lot more details in the deployment section of the docs. We go into detail on how to define env vars for each deployment option.

    + + \ No newline at end of file diff --git a/docs/project/server-config.html b/docs/project/server-config.html index 7ac1638120..824525c91c 100644 --- a/docs/project/server-config.html +++ b/docs/project/server-config.html @@ -19,13 +19,13 @@ - - + +
    -

    Server Config

    You can configure the behavior of the server via the server field of app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@server/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@server/myServerSetupCode.js"
    }
    }

    Setup Function

    Adding a Custom Route

    As an example, adding a custom route would look something like:

    src/server/myServerSetupCode.ts
    export const mySetupFunction = async ({ app }) => {
    addCustomRoute(app)
    }

    function addCustomRoute(app) {
    app.get('/customRoute', (_req, res) => {
    res.send('I am a custom route')
    })
    }

    Storing Some Values for Later Use

    In case you want to store some values for later use, or to be accessed by the Operations you do that in the setupFn function.

    Dummy example of such function and its usage:

    src/server/myServerSetupCode.js
    let someResource = undefined

    export const mySetupFunction = async () => {
    // Let's pretend functions setUpSomeResource and startSomeCronJob
    // are implemented below or imported from another file.
    someResource = await setUpSomeResource()
    startSomeCronJob()
    }

    export const getSomeResource = () => someResource
    src/server/queries.js
    import { getSomeResource } from './myServerSetupCode.js'

    ...

    export const someQuery = async (args, context) => {
    const someResource = getSomeResource()
    return queryDataFromSomeResource(args, someResource)
    }
    note

    The recommended way is to put the variable in the same module where you defined the setup function and then expose additional functions for reading those values, which you can then import directly from Operations and use.

    This effectively turns your module into a singleton whose construction is performed on server start.

    Read more about server setup function below.

    Middleware Config Function

    You can configure the global middleware via the middlewareConfigFn. This will modify the middleware stack for all operations and APIs.

    Read more about middleware config function below.

    API Reference

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@server/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@server/myServerSetupCode.js"
    }
    }

    app.server is a dictionary with the following fields:

    • setupFn: ServerImport

      setupFn declares a function that will be executed on server start. This function is expected to be async and will be awaited before the server starts accepting any requests.

      It allows you to do any custom setup, e.g. setting up additional database/websockets or starting cron/scheduled jobs.

      The setupFn function receives the express.Application and the http.Server instances as part of its context. They can be useful for setting up any custom server routes or for example, setting up socket.io.

      src/server/myServerSetupCode.js
      export const mySetupFunction = async () => {
      await setUpSomeResource()
      }
    • middlewareConfigFn: ServerImport

      The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See more in the configuring middleware section.

    - - +

    Server Config

    You can configure the behavior of the server via the server field of app declaration:

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@server/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@server/myServerSetupCode.js"
    }
    }

    Setup Function

    Adding a Custom Route

    As an example, adding a custom route would look something like:

    src/server/myServerSetupCode.ts
    export const mySetupFunction = async ({ app }) => {
    addCustomRoute(app)
    }

    function addCustomRoute(app) {
    app.get('/customRoute', (_req, res) => {
    res.send('I am a custom route')
    })
    }

    Storing Some Values for Later Use

    In case you want to store some values for later use, or to be accessed by the Operations you do that in the setupFn function.

    Dummy example of such function and its usage:

    src/server/myServerSetupCode.js
    let someResource = undefined

    export const mySetupFunction = async () => {
    // Let's pretend functions setUpSomeResource and startSomeCronJob
    // are implemented below or imported from another file.
    someResource = await setUpSomeResource()
    startSomeCronJob()
    }

    export const getSomeResource = () => someResource
    src/server/queries.js
    import { getSomeResource } from './myServerSetupCode.js'

    ...

    export const someQuery = async (args, context) => {
    const someResource = getSomeResource()
    return queryDataFromSomeResource(args, someResource)
    }
    note

    The recommended way is to put the variable in the same module where you defined the setup function and then expose additional functions for reading those values, which you can then import directly from Operations and use.

    This effectively turns your module into a singleton whose construction is performed on server start.

    Read more about server setup function below.

    Middleware Config Function

    You can configure the global middleware via the middlewareConfigFn. This will modify the middleware stack for all operations and APIs.

    Read more about middleware config function below.

    API Reference

    main.wasp
    app MyApp {
    title: "My app",
    // ...
    server: {
    setupFn: import { mySetupFunction } from "@server/myServerSetupCode.js",
    middlewareConfigFn: import { myMiddlewareConfigFn } from "@server/myServerSetupCode.js"
    }
    }

    app.server is a dictionary with the following fields:

    • setupFn: ServerImport

      setupFn declares a function that will be executed on server start. This function is expected to be async and will be awaited before the server starts accepting any requests.

      It allows you to do any custom setup, e.g. setting up additional database/websockets or starting cron/scheduled jobs.

      The setupFn function receives the express.Application and the http.Server instances as part of its context. They can be useful for setting up any custom server routes or for example, setting up socket.io.

      src/server/myServerSetupCode.js
      export const mySetupFunction = async () => {
      await setUpSomeResource()
      }
    • middlewareConfigFn: ServerImport

      The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See more in the configuring middleware section.

    + + \ No newline at end of file diff --git a/docs/project/starter-templates.html b/docs/project/starter-templates.html index 8b8cb5345d..47028526ef 100644 --- a/docs/project/starter-templates.html +++ b/docs/project/starter-templates.html @@ -19,13 +19,13 @@ - - + +
    -

    Starter Templates

    We created a few starter templates to help you get started with Wasp. Check out the list below.

    Using a Template

    Run wasp new to run the interactive mode for creating a new Wasp project.

    It will ask you for the project name, and then for the template to use:

    $ wasp new
    Enter the project name (e.g. my-project) ▸ MyFirstProject
    Choose a starter template
    [1] basic (default)
    [2] saas
    [3] todo-ts
    ▸ 1

    🐝 --- Creating your project from the basic template... ---------------------------

    Created new Wasp app in ./MyFirstProject directory!
    To run it, do:

    cd MyFirstProject
    wasp start

    Available Templates

    When you have a good idea for a new product, you don't want to waste your time on setting up common things like authentication, database, etc. That's why we created a few starter templates to help you get started with Wasp.

    Vector Similarity Search Template

    Vector Similarity Search Template

    A template for generating embeddings and performing vector similarity search on your text data!

    Features: w/ Embeddings & vector similarity search, OpenAI Embeddings API, Vector DB (Pinecone), Tailwind, Fullstack Type Safety

    Use this template:

    wasp new <project-name> -t embeddings

    SaaS Template

    SaaS Template

    A SaaS Template to get your profitable side project started quickly and easily!

    Features: w/ Stripe Payments, OpenAI GPT API, Google Auth, SendGrid, Tailwind, & Cron Jobs

    Use this template:

    wasp new <project-name> -t saas

    Todo App w/ Typescript

    A simple Todo App with Typescript and Fullstack Type Safety.

    Features: Auth (username/password), Fullstack Type Safety

    Use this template:

    wasp new <project-name> -t todo-ts
    - - +

    Starter Templates

    We created a few starter templates to help you get started with Wasp. Check out the list below.

    Using a Template

    Run wasp new to run the interactive mode for creating a new Wasp project.

    It will ask you for the project name, and then for the template to use:

    $ wasp new
    Enter the project name (e.g. my-project) ▸ MyFirstProject
    Choose a starter template
    [1] basic (default)
    [2] saas
    [3] todo-ts
    ▸ 1

    🐝 --- Creating your project from the basic template... ---------------------------

    Created new Wasp app in ./MyFirstProject directory!
    To run it, do:

    cd MyFirstProject
    wasp start

    Available Templates

    When you have a good idea for a new product, you don't want to waste your time on setting up common things like authentication, database, etc. That's why we created a few starter templates to help you get started with Wasp.

    Vector Similarity Search Template

    Vector Similarity Search Template

    A template for generating embeddings and performing vector similarity search on your text data!

    Features: w/ Embeddings & vector similarity search, OpenAI Embeddings API, Vector DB (Pinecone), Tailwind, Fullstack Type Safety

    Use this template:

    wasp new <project-name> -t embeddings

    SaaS Template

    SaaS Template

    A SaaS Template to get your profitable side project started quickly and easily!

    Features: w/ Stripe Payments, OpenAI GPT API, Google Auth, SendGrid, Tailwind, & Cron Jobs

    Use this template:

    wasp new <project-name> -t saas

    Todo App w/ Typescript

    A simple Todo App with Typescript and Fullstack Type Safety.

    Features: Auth (username/password), Fullstack Type Safety

    Use this template:

    wasp new <project-name> -t todo-ts
    + + \ No newline at end of file diff --git a/docs/project/static-assets.html b/docs/project/static-assets.html index c851f32526..356b4d8e6d 100644 --- a/docs/project/static-assets.html +++ b/docs/project/static-assets.html @@ -19,13 +19,13 @@ - - + +
    -

    Static Asset Handling

    Importing Asset as URL

    Importing a static asset (e.g. an image) will return its URL. For example:

    src/client/App.jsx
    import imgUrl from './img.png'

    function App() {
    return <img src={imgUrl} alt="img" />
    }

    For example, imgUrl will be /img.png during development, and become /assets/img.2d8efhg.png in the production build.

    This is what you want to use most of the time, as it ensures that the asset file exists and is included in the bundle.

    We are using Vite under the hood, read more about importing static assets in Vite's docs.

    The public Directory

    If you have assets that are:

    • Never referenced in source code (e.g. robots.txt)
    • Must retain the exact same file name (without hashing)
    • ...or you simply don't want to have to import an asset first just to get its URL

    Then you can place the asset in a special public directory in the client folder:

    src
    └── client
    ├── public
    │ ├── favicon.ico
    │ └── robots.txt
    └── ...

    Assets in this directory will be served at root path / during dev, and copied to the root of the dist directory as-is.

    For example, if you have a file favicon.ico in the public directory, and your app is hosted at https://myapp.com, it will be made available at https://myapp.com/favicon.ico.

    Usage in client code

    Note that:

    • You should always reference public assets using root absolute path - for example, src/client/public/icon.png should be referenced in source code as /icon.png.
    • Assets in the public directory cannot be imported from .
    - - +

    Static Asset Handling

    Importing Asset as URL

    Importing a static asset (e.g. an image) will return its URL. For example:

    src/client/App.jsx
    import imgUrl from './img.png'

    function App() {
    return <img src={imgUrl} alt="img" />
    }

    For example, imgUrl will be /img.png during development, and become /assets/img.2d8efhg.png in the production build.

    This is what you want to use most of the time, as it ensures that the asset file exists and is included in the bundle.

    We are using Vite under the hood, read more about importing static assets in Vite's docs.

    The public Directory

    If you have assets that are:

    • Never referenced in source code (e.g. robots.txt)
    • Must retain the exact same file name (without hashing)
    • ...or you simply don't want to have to import an asset first just to get its URL

    Then you can place the asset in a special public directory in the client folder:

    src
    └── client
    ├── public
    │ ├── favicon.ico
    │ └── robots.txt
    └── ...

    Assets in this directory will be served at root path / during dev, and copied to the root of the dist directory as-is.

    For example, if you have a file favicon.ico in the public directory, and your app is hosted at https://myapp.com, it will be made available at https://myapp.com/favicon.ico.

    Usage in client code

    Note that:

    • You should always reference public assets using root absolute path - for example, src/client/public/icon.png should be referenced in source code as /icon.png.
    • Assets in the public directory cannot be imported from .
    + + \ No newline at end of file diff --git a/docs/project/testing.html b/docs/project/testing.html index 9c5440e53f..b15920cf97 100644 --- a/docs/project/testing.html +++ b/docs/project/testing.html @@ -19,13 +19,13 @@ - - + +
    -

    Testing

    info

    Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly some changes with testing support in the future. If you encounter any issues, reach out to us on Discord and we will make sure to help you out!

    Testing Your React App

    Wasp enables you to quickly and easily write both unit tests and React component tests for your frontend code. Because Wasp uses Vite, we support testing web apps through Vitest.

    Included Libraries

    vitest: Unit test framework with native Vite support.

    @vitest/ui: A nice UI for seeing your test results.

    jsdom: A web browser test environment for Node.js.

    @testing-library/react / @testing-library/jest-dom: Testing helpers.

    msw: A server mocking library.

    Writing Tests

    For Wasp to pick up your tests, they should be placed within the src/client directory and use an extension that matches these glob patterns. Some of the file names that Wasp will pick up as tests:

    • yourFile.test.ts
    • YourComponent.spec.jsx

    Within test files, you can import your other source files as usual. For example, if you have a component Counter.jsx, you test it by creating a file in the same directory called Counter.test.jsx and import the component with import Counter from './Counter'.

    Running Tests

    Running wasp test client will start Vitest in watch mode and recompile your Wasp project when changes are made.

    • If you want to see a realtime UI, pass --ui as an option.
    • To run the tests just once, use wasp test client run.

    All arguments after wasp test client are passed directly to the Vitest CLI, so check out their documentation for all of the options.

    Be Careful

    You should not run wasp test while wasp start is running. Both will try to compile your project to .wasp/out.

    React Testing Helpers

    Wasp provides several functions to help you write React tests:

    • renderInContext: Takes a React component, wraps it inside a QueryClientProvider and Router, and renders it. This is the function you should use to render components in your React component tests.

      import { renderInContext } from "@wasp/test";

      renderInContext(<MainPage />);
    • mockServer: Sets up the mock server and returns an object containing the mockQuery and mockApi utilities. This should be called outside of any test case, in each file that wants to use those helpers.

      import { mockServer } from "@wasp/test";

      const { mockQuery, mockApi } = mockServer();
      • mockQuery: Takes a Wasp query to mock and the JSON data it should return.

        import getTasks from "@wasp/queries/getTasks";

        mockQuery(getTasks, []);
        • Helpful when your component uses useQuery.
        • Behind the scenes, Wasp uses msw to create a server request handle that responds with the specified data.
        • Mock are cleared between each test.
      • mockApi: Similar to mockQuery, but for APIs. Instead of a Wasp query, it takes a route containing an HTTP method and a path.

        import { HttpMethod } from "@wasp/types";

        mockApi({ method: HttpMethod.Get, path: "/foor/bar" }, { res: "hello" });

    Testing Your Server-Side Code

    Wasp currently does not provide a way to test your server-side code, but we will be adding support soon. You can track the progress at this GitHub issue and express your interest by commenting.

    Examples

    You can see some tests in a Wasp project here.

    Client Unit Tests

    src/client/helpers.js
    export function areThereAnyTasks(tasks) {
    return tasks.length === 0;
    }
    src/client/helpers.test.js
    import { test, expect } from "vitest";

    import { areThereAnyTasks } from "./helpers";

    test("areThereAnyTasks", () => {
    expect(areThereAnyTasks([])).toBe(false);
    });

    React Component Tests

    src/client/Todo.jsx
    import { useQuery } from "@wasp/queries";
    import getTasks from "@wasp/queries/getTasks";

    const Todo = (_props) => {
    const { data: tasks } = useQuery(getTasks);
    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/client/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "@wasp/test";
    import getTasks from "@wasp/queries/getTasks";
    import Todo from "./Todo";

    const { mockQuery } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockQuery(getTasks, mockTasks);

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });

    Testing With Mocked APIs

    src/client/Todo.jsx
    import api from "@wasp/api";

    const Todo = (_props) => {
    const [tasks, setTasks] = useState([]);
    useEffect(() => {
    api
    .get("/tasks")
    .then((res) => res.json())
    .then((tasks) => setTasks(tasks))
    .catch((err) => window.alert(err));
    });

    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/client/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "@wasp/test";
    import Todo from "./Todo";

    const { mockApi } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockApi("/tasks", { res: mockTasks });

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });
    - - +

    Testing

    info

    Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly some changes with testing support in the future. If you encounter any issues, reach out to us on Discord and we will make sure to help you out!

    Testing Your React App

    Wasp enables you to quickly and easily write both unit tests and React component tests for your frontend code. Because Wasp uses Vite, we support testing web apps through Vitest.

    Included Libraries

    vitest: Unit test framework with native Vite support.

    @vitest/ui: A nice UI for seeing your test results.

    jsdom: A web browser test environment for Node.js.

    @testing-library/react / @testing-library/jest-dom: Testing helpers.

    msw: A server mocking library.

    Writing Tests

    For Wasp to pick up your tests, they should be placed within the src/client directory and use an extension that matches these glob patterns. Some of the file names that Wasp will pick up as tests:

    • yourFile.test.ts
    • YourComponent.spec.jsx

    Within test files, you can import your other source files as usual. For example, if you have a component Counter.jsx, you test it by creating a file in the same directory called Counter.test.jsx and import the component with import Counter from './Counter'.

    Running Tests

    Running wasp test client will start Vitest in watch mode and recompile your Wasp project when changes are made.

    • If you want to see a realtime UI, pass --ui as an option.
    • To run the tests just once, use wasp test client run.

    All arguments after wasp test client are passed directly to the Vitest CLI, so check out their documentation for all of the options.

    Be Careful

    You should not run wasp test while wasp start is running. Both will try to compile your project to .wasp/out.

    React Testing Helpers

    Wasp provides several functions to help you write React tests:

    • renderInContext: Takes a React component, wraps it inside a QueryClientProvider and Router, and renders it. This is the function you should use to render components in your React component tests.

      import { renderInContext } from "@wasp/test";

      renderInContext(<MainPage />);
    • mockServer: Sets up the mock server and returns an object containing the mockQuery and mockApi utilities. This should be called outside of any test case, in each file that wants to use those helpers.

      import { mockServer } from "@wasp/test";

      const { mockQuery, mockApi } = mockServer();
      • mockQuery: Takes a Wasp query to mock and the JSON data it should return.

        import getTasks from "@wasp/queries/getTasks";

        mockQuery(getTasks, []);
        • Helpful when your component uses useQuery.
        • Behind the scenes, Wasp uses msw to create a server request handle that responds with the specified data.
        • Mock are cleared between each test.
      • mockApi: Similar to mockQuery, but for APIs. Instead of a Wasp query, it takes a route containing an HTTP method and a path.

        import { HttpMethod } from "@wasp/types";

        mockApi({ method: HttpMethod.Get, path: "/foor/bar" }, { res: "hello" });

    Testing Your Server-Side Code

    Wasp currently does not provide a way to test your server-side code, but we will be adding support soon. You can track the progress at this GitHub issue and express your interest by commenting.

    Examples

    You can see some tests in a Wasp project here.

    Client Unit Tests

    src/client/helpers.js
    export function areThereAnyTasks(tasks) {
    return tasks.length === 0;
    }
    src/client/helpers.test.js
    import { test, expect } from "vitest";

    import { areThereAnyTasks } from "./helpers";

    test("areThereAnyTasks", () => {
    expect(areThereAnyTasks([])).toBe(false);
    });

    React Component Tests

    src/client/Todo.jsx
    import { useQuery } from "@wasp/queries";
    import getTasks from "@wasp/queries/getTasks";

    const Todo = (_props) => {
    const { data: tasks } = useQuery(getTasks);
    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/client/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "@wasp/test";
    import getTasks from "@wasp/queries/getTasks";
    import Todo from "./Todo";

    const { mockQuery } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockQuery(getTasks, mockTasks);

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });

    Testing With Mocked APIs

    src/client/Todo.jsx
    import api from "@wasp/api";

    const Todo = (_props) => {
    const [tasks, setTasks] = useState([]);
    useEffect(() => {
    api
    .get("/tasks")
    .then((res) => res.json())
    .then((tasks) => setTasks(tasks))
    .catch((err) => window.alert(err));
    });

    return (
    <ul>
    {tasks &&
    tasks.map((task) => (
    <li key={task.id}>
    <input type="checkbox" value={task.isDone} />
    {task.description}
    </li>
    ))}
    </ul>
    );
    };
    src/client/Todo.test.jsx
    import { test, expect } from "vitest";
    import { screen } from "@testing-library/react";

    import { mockServer, renderInContext } from "@wasp/test";
    import Todo from "./Todo";

    const { mockApi } = mockServer();

    const mockTasks = [
    {
    id: 1,
    description: "test todo 1",
    isDone: true,
    userId: 1,
    },
    ];

    test("handles mock data", async () => {
    mockApi("/tasks", { res: mockTasks });

    renderInContext(<Todo />);

    await screen.findByText("test todo 1");

    expect(screen.getByRole("checkbox")).toBeChecked();

    screen.debug();
    });
    + + \ No newline at end of file diff --git a/docs/quick-start.html b/docs/quick-start.html index 801e6687fe..bb2bb20f64 100644 --- a/docs/quick-start.html +++ b/docs/quick-start.html @@ -19,13 +19,13 @@ - - + +
    -

    Quick Start

    Installation

    Try Wasp Without Installing 🤔?

    Give Wasp a spin in the browser without any setup by running our Wasp Template for Gitpod

    Welcome, new Waspeteer 🐝!

    To install Wasp on Linux / OSX / WSL(Win), open your terminal and run:

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

    ℹ️ Wasp requires Node.js and will warn you if it is missing: check below for more details.

    Then, create a new app by running:

    wasp new

    and then run the app:

    cd <my-project-name>
    wasp start

    That's it 🎉 You have successfully created and served a new web app at http://localhost:3000 and Wasp is serving both frontend and backend for you.

    Something Unclear?

    Check More Details section below if anything went wrong, or if you have additional questions.

    What next?

    • 👉 Check out the Todo App tutorial, which will take you through all the core features of Wasp! 👈
    • Setup your editor for working with Wasp.
    • Join us on Discord! Any feedback or questions you have, we are there for you.
    • Follow Wasp development by subscribing to our newsletter: https://wasp-lang.dev/#signup . We usually send 1 per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    More details

    Requirements

    You must have Node.js (and NPM) installed on your machine and available in PATH. We rely on the latest Node.js LTS version (currently v18.14.2).

    We recommend using nvm for managing your Node.js installation version(s).

    Quick guide on installing/using nvm

    Install nvm via your OS package manager (apt, pacman, homebrew, ...) or via the nvm install script.

    Then, install a version of Node.js that you need:

    nvm install 18

    Finally, whenever you need to ensure a specific version of Node.js is used, run:

    nvm use 18

    to set the Node.js version for the current shell session.

    You can run

    node -v

    to check the version of Node.js currently being used in this shell session.

    Check NVM repo for more details: https://github.com/nvm-sh/nvm.

    Installation

    Open your terminal and run:

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

    Quick Start

    Installation

    Try Wasp Without Installing 🤔?

    Give Wasp a spin in the browser without any setup by running our Wasp Template for Gitpod

    Welcome, new Waspeteer 🐝!

    To install Wasp on Linux / OSX / WSL(Win), open your terminal and run:

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

    ℹ️ Wasp requires Node.js and will warn you if it is missing: check below for more details.

    Then, create a new app by running:

    wasp new

    and then run the app:

    cd <my-project-name>
    wasp start

    That's it 🎉 You have successfully created and served a new web app at http://localhost:3000 and Wasp is serving both frontend and backend for you.

    Something Unclear?

    Check More Details section below if anything went wrong, or if you have additional questions.

    What next?

    • 👉 Check out the Todo App tutorial, which will take you through all the core features of Wasp! 👈
    • Setup your editor for working with Wasp.
    • Join us on Discord! Any feedback or questions you have, we are there for you.
    • Follow Wasp development by subscribing to our newsletter: https://wasp-lang.dev/#signup . We usually send 1 per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    More details

    Requirements

    You must have Node.js (and NPM) installed on your machine and available in PATH. We rely on the latest Node.js LTS version (currently v18.14.2).

    We recommend using nvm for managing your Node.js installation version(s).

    Quick guide on installing/using nvm

    Install nvm via your OS package manager (apt, pacman, homebrew, ...) or via the nvm install script.

    Then, install a version of Node.js that you need:

    nvm install 18

    Finally, whenever you need to ensure a specific version of Node.js is used, run:

    nvm use 18

    to set the Node.js version for the current shell session.

    You can run

    node -v

    to check the version of Node.js currently being used in this shell session.

    Check NVM repo for more details: https://github.com/nvm-sh/nvm.

    Installation

    Open your terminal and run:

    curl -sSL https://get.wasp-lang.dev/installer.sh | sh
    + + \ No newline at end of file diff --git a/docs/telemetry.html b/docs/telemetry.html index a05d054050..0311aa1bb5 100644 --- a/docs/telemetry.html +++ b/docs/telemetry.html @@ -19,15 +19,15 @@ - - + +
    -

    Telemetry

    Overview

    The term telemetry refers to the collection of certain usage data to help improve the quality of a piece of software (in this case, Wasp).

    Our telemetry implementation is anonymized and very limited in its scope, focused on answering following questions:

    • How many people and how often: tried to install Wasp, use Wasp, have built a Wasp app, or have deployed one?
    • How many projects are created with Wasp?

    When and what is sent?

    - - + + \ No newline at end of file diff --git a/docs/tutorial/actions.html b/docs/tutorial/actions.html index 13adf3ed2f..04d76ca7c0 100644 --- a/docs/tutorial/actions.html +++ b/docs/tutorial/actions.html @@ -19,13 +19,13 @@ - - + +
    -

    6. Modifying Data

    In the previous section, we learned about using queries to fetch data and only briefly mentioned that actions can be used to update the database. Let's learn more about actions so we can add and update tasks in the database.

    We have to create:

    1. A Wasp action that creates a new task.
    2. A React form that calls that action when the user creates a task.

    Creating a New Action

    Creating an action is very similar to creating a query.

    Declaring an Action

    We must first declare the action in main.wasp:

    main.wasp
    // ...

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

    Implementing an Action

    Let's now define a function for our createTask action:

    src/server/actions.js
    export const createTask = async (args, context) => {
    return context.entities.Task.create({
    data: { description: args.description },
    })
    }
    tip

    We put the function in a new file src/server/actions.ts, but we could have put it anywhere we wanted! There are no limitations here, as long as the declaration in the Wasp file imports it correctly and the file is located within src/server.

    Invoking the Action on the Client

    First, let's define a form that the user can create new tasks with.

    src/client/MainPage.jsx
    import getTasks from '@wasp/queries/getTasks'
    import createTask from '@wasp/actions/createTask'
    import { useQuery } from '@wasp/queries'

    // ...

    const NewTaskForm = () => {
    const handleSubmit = async (event) => {
    event.preventDefault()
    try {
    const target = event.target
    const description = target.description.value
    target.reset()
    await createTask({ description })
    } catch (err) {
    window.alert('Error: ' + err.message)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    <input name="description" type="text" defaultValue="" />
    <input type="submit" value="Create task" />
    </form>
    )
    }

    Unlike queries, you call actions directly (i.e., without wrapping it with a hook) because we don't need reactivity. The rest is just regular React code.

    Now, we just need to add this form to the page component:

    src/client/MainPage.tsx
    import getTasks from '@wasp/queries/getTasks'
    import createTask from '@wasp/actions/createTask'
    import { useQuery } from '@wasp/queries'

    const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    <NewTaskForm />

    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    And now we have a form that creates new tasks.

    Try creating a "Build a Todo App in Wasp" task and see it appear in the list below. The task is created on the server and saved in the database.

    Try refreshing the page or opening it in another browser, you'll see the tasks are still there!

    Todo App - creating new task

    Automatic Query Invalidation

    When you create a new task, the list of tasks is automatically updated to display the new task, even though we have not written any code that would do that! These automatic updates are handled by code that Wasp generates.

    When you declared the getTasks and createTask operations, you specified that they both use the Task entity. So when createTask is called, Wasp knows that the data getTasks fetches may have changed and automatically updates it in the background. This means that out of the box, Wasp will make sure that all your queries are kept in-sync with changes made by any actions.

    This behavior is convenient as a default but can cause poor performance in large apps. While there is no mechanism for overriding this behavior yet, it is something that we plan to include in Wasp in the future. This feature is tracked here.

    A Second Action

    Our Todo app isn't finished if you can't mark a task as done! We'll create a new action to update a task's status and call it from React whenever a task's checkbox is toggled.

    Since we've already created one task together, try to create this one yourself. It should be an action named updateTask that takes a task id and an isDone in its arguments. You can check our implementation below.

    Solution

    The action declaration:

    main.wasp
    // ...

    action updateTask {
    fn: import { updateTask } from "@server/actions.js",
    entities: [Task]
    }

    The implementation on the server:

    src/server/actions.js
    // ...

    export const updateTask = async ({ id, isDone }, context) => {
    return context.entities.Task.update({
    where: { id },
    data: {
    isDone: isDone,
    },
    })
    }

    Now, we can call updateTask from the React component:

    src/client/MainPage.jsx
    // ...
    import updateTask from "@wasp/actions/updateTask"

    // ...

    const Task = ({ task }) => {
    const handleIsDoneChange = async (event) => {
    try {
    await updateTask({
    id: task.id,
    isDone: event.target.checked,
    })
    } catch (error: any) {
    window.alert("Error while updating task: " + error.message)
    }
    }

    return (
    <div>
    <input
    type="checkbox"
    id={String(task.id)}
    checked={task.isDone}
    onChange={handleIsDoneChange}
    />
    {task.description}
    </div>
    )
    }
    // ...

    Awesome! Now we can check off this task 🙃 Let's add one more interesting feature to our app.

    - - +

    6. Modifying Data

    In the previous section, we learned about using queries to fetch data and only briefly mentioned that actions can be used to update the database. Let's learn more about actions so we can add and update tasks in the database.

    We have to create:

    1. A Wasp action that creates a new task.
    2. A React form that calls that action when the user creates a task.

    Creating a New Action

    Creating an action is very similar to creating a query.

    Declaring an Action

    We must first declare the action in main.wasp:

    main.wasp
    // ...

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

    Implementing an Action

    Let's now define a function for our createTask action:

    src/server/actions.js
    export const createTask = async (args, context) => {
    return context.entities.Task.create({
    data: { description: args.description },
    })
    }
    tip

    We put the function in a new file src/server/actions.ts, but we could have put it anywhere we wanted! There are no limitations here, as long as the declaration in the Wasp file imports it correctly and the file is located within src/server.

    Invoking the Action on the Client

    First, let's define a form that the user can create new tasks with.

    src/client/MainPage.jsx
    import getTasks from '@wasp/queries/getTasks'
    import createTask from '@wasp/actions/createTask'
    import { useQuery } from '@wasp/queries'

    // ...

    const NewTaskForm = () => {
    const handleSubmit = async (event) => {
    event.preventDefault()
    try {
    const target = event.target
    const description = target.description.value
    target.reset()
    await createTask({ description })
    } catch (err) {
    window.alert('Error: ' + err.message)
    }
    }

    return (
    <form onSubmit={handleSubmit}>
    <input name="description" type="text" defaultValue="" />
    <input type="submit" value="Create task" />
    </form>
    )
    }

    Unlike queries, you call actions directly (i.e., without wrapping it with a hook) because we don't need reactivity. The rest is just regular React code.

    Now, we just need to add this form to the page component:

    src/client/MainPage.tsx
    import getTasks from '@wasp/queries/getTasks'
    import createTask from '@wasp/actions/createTask'
    import { useQuery } from '@wasp/queries'

    const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    <NewTaskForm />

    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    And now we have a form that creates new tasks.

    Try creating a "Build a Todo App in Wasp" task and see it appear in the list below. The task is created on the server and saved in the database.

    Try refreshing the page or opening it in another browser, you'll see the tasks are still there!

    Todo App - creating new task

    Automatic Query Invalidation

    When you create a new task, the list of tasks is automatically updated to display the new task, even though we have not written any code that would do that! These automatic updates are handled by code that Wasp generates.

    When you declared the getTasks and createTask operations, you specified that they both use the Task entity. So when createTask is called, Wasp knows that the data getTasks fetches may have changed and automatically updates it in the background. This means that out of the box, Wasp will make sure that all your queries are kept in-sync with changes made by any actions.

    This behavior is convenient as a default but can cause poor performance in large apps. While there is no mechanism for overriding this behavior yet, it is something that we plan to include in Wasp in the future. This feature is tracked here.

    A Second Action

    Our Todo app isn't finished if you can't mark a task as done! We'll create a new action to update a task's status and call it from React whenever a task's checkbox is toggled.

    Since we've already created one task together, try to create this one yourself. It should be an action named updateTask that takes a task id and an isDone in its arguments. You can check our implementation below.

    Solution

    The action declaration:

    main.wasp
    // ...

    action updateTask {
    fn: import { updateTask } from "@server/actions.js",
    entities: [Task]
    }

    The implementation on the server:

    src/server/actions.js
    // ...

    export const updateTask = async ({ id, isDone }, context) => {
    return context.entities.Task.update({
    where: { id },
    data: {
    isDone: isDone,
    },
    })
    }

    Now, we can call updateTask from the React component:

    src/client/MainPage.jsx
    // ...
    import updateTask from '@wasp/actions/updateTask'

    // ...

    const Task = ({ task }) => {
    const handleIsDoneChange = async (event) => {
    try {
    await updateTask({
    id: task.id,
    isDone: event.target.checked,
    })
    } catch (error) {
    window.alert('Error while updating task: ' + error.message)
    }
    }

    return (
    <div>
    <input
    type="checkbox"
    id={String(task.id)}
    checked={task.isDone}
    onChange={handleIsDoneChange}
    />
    {task.description}
    </div>
    )
    }
    // ...

    Awesome! Now we can check off this task 🙃 Let's add one more interesting feature to our app.

    + + \ No newline at end of file diff --git a/docs/tutorial/auth.html b/docs/tutorial/auth.html index dc3e47d2d4..cc38b2489e 100644 --- a/docs/tutorial/auth.html +++ b/docs/tutorial/auth.html @@ -19,13 +19,13 @@ - - + +
    -

    7. Adding Authentication

    Most apps today require some sort of registration and login flow, so Wasp has first-class support for it. Let's add it to our Todo app!

    First, we'll create a Todo list for what needs to be done (luckily we have an app for this now 😄).

    • Create a User entity.
    • Tell Wasp to use the username and password authentication.
    • Add login and signup pages.
    • Update the main page to require authentication.
    • Add a relation between User and Task entities.
    • Modify our queries and actions so that users can only see and modify their tasks.
    • Add a logout button.

    Creating a User Entity

    Since Wasp manages authentication, it expects certain fields to exist on the User entity. Specifically, it expects a unique username field and a password field, both of which should be strings.

    main.wasp
    // ...

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    password String
    psl=}

    As we talked about earlier, we have to remember to update the database schema:

    wasp db migrate-dev

    Adding Auth to the Project

    Next, we want to tell Wasp that we want to use full-stack authentication in our app:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "Todo app",

    auth: {
    // Tells Wasp which entity to use for storing users.
    userEntity: User,
    methods: {
    // Enable username and password auth.
    usernameAndPassword: {}
    },
    // We'll see how this is used a bit later.
    onAuthFailedRedirectTo: "/login"
    }
    }

    // ...

    By doing this, Wasp will create:

    • Auth UI with login and signup forms.
    • A logout() action.
    • A React hook useAuth().
    • context.user for use in Queries and Actions.
    info

    Wasp also supports authentication using Google, GitHub, and email, with more on the way!

    Adding Login and Signup Pages

    Wasp creates the login and signup forms for us, but we still need to define the pages to display those forms on. We'll start by declaring the pages in the Wasp file:

    main.wasp
    // ...

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import Signup from "@client/SignupPage.jsx"
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import Login from "@client/LoginPage.jsx"
    }

    Great, Wasp now knows these pages exist! Now, the React code for the pages:

    src/client/LoginPage.jsx
    import { Link } from 'react-router-dom'

    import { LoginForm } from '@wasp/auth/forms/Login'

    const LoginPage = () => {
    return (
    <>
    <LoginForm />
    <br />
    <span>
    I don't have an account yet (<Link to="/signup">go to signup</Link>).
    </span>
    </>
    )
    }

    export default LoginPage

    The Signup page is very similar to the login one:

    src/client/SignupPage.jsx
    import { Link } from 'react-router-dom'

    import { SignupForm } from '@wasp/auth/forms/Signup'

    const SignupPage = () => {
    return (
    <>
    <SignupForm />
    <br />
    <span>
    I already have an account (<Link to="/login">go to login</Link>).
    </span>
    </>
    )
    }

    export default SignupPage

    Update the Main Page to Require Auth

    We don't want users who are not logged in to access the main page, because they won't be able to create any tasks. So let's make the page private by requiring the user to be logged in:

    main.wasp
    // ...

    page MainPage {
    authRequired: true,
    component: import Main from "@client/MainPage"
    }

    Now that auth is required for this page, unauthenticated users will be redirected to /login, as we specified with app.auth.onAuthFailedRedirectTo.

    Additionally, when authRequired is true, the page's React component will be provided a user object as prop.

    src/client/MainPage.jsx
    const MainPage = ({ user }) => {
    // Do something with the user
    }

    Ok, time to test this out. Navigate to the main page (/) of the app. You'll get redirected to /login, where you'll be asked to authenticate.

    Since we just added users, you don't have an account yet. Go to the signup page and create one. You'll be sent back to the main page where you will now be able to see the TODO list!

    Let's check out what the database looks like. Start the Prisma Studio:

    wasp db studio
    Database demonstration - password hashing

    We see there is a user and that its password is already hashed 🤯

    However, you will notice that if you try logging in as different users and creating some tasks, all users share the same tasks. That's because we haven't yet updated the queries and actions to have per-user tasks. Let's do that next.

    Defining a User-Task Relation

    First, let's define a one-to-many relation between users and tasks (check the Prisma docs on relations):

    main.wasp
    // ...

    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=}

    // ...

    As always, we have to update the database:

    wasp db migrate-dev
    note

    We made user and userId in Task optional (via ?) because that allows us to keep the existing tasks, which don't have a user assigned, in the database. This is not recommended because it allows an unwanted state in the database (what is the purpose of the task not belonging to anybody?) and normally we would not make these fields optional. Instead, we would do a data migration to take care of those tasks, even if it means just deleting them all. However, for this tutorial, for the sake of simplicity, we will stick with this.

    Updating Operations to Check Authentication

    Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks:

    src/server/queries.js
    import HttpError from '@wasp/core/HttpError.js'

    export const getTasks = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.findMany({
    where: { user: { id: context.user.id } },
    })
    }
    src/server/actions.js
    import HttpError from '@wasp/core/HttpError.js'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.create({
    data: {
    description: args.description,
    user: { connect: { id: context.user.id } },
    },
    })
    }

    export const updateTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.updateMany({
    where: { id: args.id, user: { id: context.user.id } },
    data: { isDone: args.isDone },
    })
    }
    note

    Due to how Prisma works, we had to convert update to updateMany in updateTask action to be able to specify the user id in where.

    With these changes, each user should have a list of tasks that only they can see and edit.

    Try playing around, adding a few users and some tasks for each of them. Then open the DB studio:

    wasp db studio
    Database demonstration

    You will see that each user has their tasks, just as we specified in our code!

    Logout Button

    Last, but not least, let's add the logout functionality:

    src/client/MainPage.jsx
    // ...
    import logout from '@wasp/auth/logout'
    //...

    const MainPage = () => {
    // ...
    return (
    <div>
    // ...
    <button onClick={logout}>Logout</button>
    </div>
    )
    }

    This is it, we have a working authentication system, and our Todo app is multi-user!

    What's Next?

    We did it 🎉 You've followed along with this tutorial to create a basic Todo app with Wasp.

    You can find the complete code for the tutorial here.

    You should be ready to learn about more complicated features and go more in-depth with the features already covered. Scroll through the sidebar on the left side of the page to see every feature Wasp has to offer. Or, let your imagination run wild and start building your app! ✨

    Looking for inspiration?

    note

    If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on Discord or create an issue on Github, so we can learn which features to add/improve next 🙏

    If you would like to contribute or help to build a feature, let us know! You can find more details on contributing here.

    Oh, and do subscribe to our newsletter! We usually send one per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    - - +

    7. Adding Authentication

    Most apps today require some sort of registration and login flow, so Wasp has first-class support for it. Let's add it to our Todo app!

    First, we'll create a Todo list for what needs to be done (luckily we have an app for this now 😄).

    • Create a User entity.
    • Tell Wasp to use the username and password authentication.
    • Add login and signup pages.
    • Update the main page to require authentication.
    • Add a relation between User and Task entities.
    • Modify our queries and actions so that users can only see and modify their tasks.
    • Add a logout button.

    Creating a User Entity

    Since Wasp manages authentication, it expects certain fields to exist on the User entity. Specifically, it expects a unique username field and a password field, both of which should be strings.

    main.wasp
    // ...

    entity User {=psl
    id Int @id @default(autoincrement())
    username String @unique
    password String
    psl=}

    As we talked about earlier, we have to remember to update the database schema:

    wasp db migrate-dev

    Adding Auth to the Project

    Next, we want to tell Wasp that we want to use full-stack authentication in our app:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "Todo app",

    auth: {
    // Tells Wasp which entity to use for storing users.
    userEntity: User,
    methods: {
    // Enable username and password auth.
    usernameAndPassword: {}
    },
    // We'll see how this is used a bit later.
    onAuthFailedRedirectTo: "/login"
    }
    }

    // ...

    By doing this, Wasp will create:

    • Auth UI with login and signup forms.
    • A logout() action.
    • A React hook useAuth().
    • context.user for use in Queries and Actions.
    info

    Wasp also supports authentication using Google, GitHub, and email, with more on the way!

    Adding Login and Signup Pages

    Wasp creates the login and signup forms for us, but we still need to define the pages to display those forms on. We'll start by declaring the pages in the Wasp file:

    main.wasp
    // ...

    route SignupRoute { path: "/signup", to: SignupPage }
    page SignupPage {
    component: import Signup from "@client/SignupPage.jsx"
    }

    route LoginRoute { path: "/login", to: LoginPage }
    page LoginPage {
    component: import Login from "@client/LoginPage.jsx"
    }

    Great, Wasp now knows these pages exist! Now, the React code for the pages:

    src/client/LoginPage.jsx
    import { Link } from 'react-router-dom'
    import { LoginForm } from '@wasp/auth/forms/Login'

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

    export default LoginPage

    The Signup page is very similar to the login one:

    src/client/SignupPage.jsx
    import { Link } from 'react-router-dom'
    import { SignupForm } from '@wasp/auth/forms/Signup'

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

    export default SignupPage

    Update the Main Page to Require Auth

    We don't want users who are not logged in to access the main page, because they won't be able to create any tasks. So let's make the page private by requiring the user to be logged in:

    main.wasp
    // ...

    page MainPage {
    authRequired: true,
    component: import Main from "@client/MainPage"
    }

    Now that auth is required for this page, unauthenticated users will be redirected to /login, as we specified with app.auth.onAuthFailedRedirectTo.

    Additionally, when authRequired is true, the page's React component will be provided a user object as prop.

    src/client/MainPage.jsx
    const MainPage = ({ user }) => {
    // Do something with the user
    }

    Ok, time to test this out. Navigate to the main page (/) of the app. You'll get redirected to /login, where you'll be asked to authenticate.

    Since we just added users, you don't have an account yet. Go to the signup page and create one. You'll be sent back to the main page where you will now be able to see the TODO list!

    Let's check out what the database looks like. Start the Prisma Studio:

    wasp db studio
    Database demonstration - password hashing

    We see there is a user and that its password is already hashed 🤯

    However, you will notice that if you try logging in as different users and creating some tasks, all users share the same tasks. That's because we haven't yet updated the queries and actions to have per-user tasks. Let's do that next.

    Defining a User-Task Relation

    First, let's define a one-to-many relation between users and tasks (check the Prisma docs on relations):

    main.wasp
    // ...

    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=}

    // ...

    As always, we have to update the database:

    wasp db migrate-dev
    note

    We made user and userId in Task optional (via ?) because that allows us to keep the existing tasks, which don't have a user assigned, in the database. This is not recommended because it allows an unwanted state in the database (what is the purpose of the task not belonging to anybody?) and normally we would not make these fields optional. Instead, we would do a data migration to take care of those tasks, even if it means just deleting them all. However, for this tutorial, for the sake of simplicity, we will stick with this.

    Updating Operations to Check Authentication

    Next, let's update the queries and actions to forbid access to non-authenticated users and to operate only on the currently logged-in user's tasks:

    src/server/queries.js
    import HttpError from '@wasp/core/HttpError.js'

    export const getTasks = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.findMany({
    where: { user: { id: context.user.id } },
    orderBy: { id: 'asc' },
    })
    }
    src/server/actions.js
    import HttpError from '@wasp/core/HttpError.js'

    export const createTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.create({
    data: {
    description: args.description,
    user: { connect: { id: context.user.id } },
    },
    })
    }

    export const updateTask = async (args, context) => {
    if (!context.user) {
    throw new HttpError(401)
    }
    return context.entities.Task.updateMany({
    where: { id: args.id, user: { id: context.user.id } },
    data: { isDone: args.isDone },
    })
    }
    note

    Due to how Prisma works, we had to convert update to updateMany in updateTask action to be able to specify the user id in where.

    With these changes, each user should have a list of tasks that only they can see and edit.

    Try playing around, adding a few users and some tasks for each of them. Then open the DB studio:

    wasp db studio
    Database demonstration

    You will see that each user has their tasks, just as we specified in our code!

    Logout Button

    Last, but not least, let's add the logout functionality:

    src/client/MainPage.jsx
    // ...
    import logout from '@wasp/auth/logout'
    //...

    const MainPage = () => {
    // ...
    return (
    <div>
    // ...
    <button onClick={logout}>Logout</button>
    </div>
    )
    }

    This is it, we have a working authentication system, and our Todo app is multi-user!

    What's Next?

    We did it 🎉 You've followed along with this tutorial to create a basic Todo app with Wasp.

    You should be ready to learn about more complicated features and go more in-depth with the features already covered. Scroll through the sidebar on the left side of the page to see every feature Wasp has to offer. Or, let your imagination run wild and start building your app! ✨

    Looking for inspiration?

    note

    If you notice that some of the features you'd like to have are missing, or have any other kind of feedback, please write to us on Discord or create an issue on Github, so we can learn which features to add/improve next 🙏

    If you would like to contribute or help to build a feature, let us know! You can find more details on contributing here.

    Oh, and do subscribe to our newsletter! We usually send one per month, and Matija does his best to unleash his creativity to make them engaging and fun to read :D!

    + + \ No newline at end of file diff --git a/docs/tutorial/create.html b/docs/tutorial/create.html index 50c598a4cc..8026910269 100644 --- a/docs/tutorial/create.html +++ b/docs/tutorial/create.html @@ -19,13 +19,13 @@ - - + +
    -

    1. Creating a New Project

    info

    You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the QuickStart guide!

    In this section, we'll guide you through the process of creating a simple Todo app with Wasp. In the process, we'll take you through the most important and useful features of Wasp.

    How Todo App will work once it is done

    If you get stuck at any point (or just want to chat), reach out to us on Discord and we will help you!

    You can find the complete code of the app we're about to build here.

    See Wasp In Action

    Prefer videos? We have a YouTube tutorial whick walks you through building this Todo app step by step. Check it out here!.

    We've also set up an in-browser dev environment for you on Gitpod which allows you to view and edit the completed app with no installation required.

    Creating a Project

    To setup a new Wasp project, run the following command in your terminal

    $ wasp new TodoApp

    Enter the newly created directory and start the development server:

    $ cd TodoApp
    $ wasp start
    note

    wasp start will take a bit of time to start the server the first time you run it in a new project.

    You will see log messages from the client, server, and database setting themselves up. When everything is ready, a new tab should open in your browser at http://localhost:3000 with a simple placeholder plage:

    Screenshot of new Wasp app

    Wasp has generated for you the full front-end and back-end code the app! Next, we'll take a closer look at how the project is structured.

    - - +

    1. Creating a New Project

    info

    You'll need to have the latest version of Wasp installed locally to follow this tutorial. If you haven't installed it yet, check out the QuickStart guide!

    In this section, we'll guide you through the process of creating a simple Todo app with Wasp. In the process, we'll take you through the most important and useful features of Wasp.

    How Todo App will work once it is done

    If you get stuck at any point (or just want to chat), reach out to us on Discord and we will help you!

    You can find the complete code of the app we're about to build here.

    See Wasp In Action

    Prefer videos? We have a YouTube tutorial whick walks you through building this Todo app step by step. Check it out here!.

    We've also set up an in-browser dev environment for you on Gitpod which allows you to view and edit the completed app with no installation required.

    Creating a Project

    To setup a new Wasp project, run the following command in your terminal

    $ wasp new TodoApp

    Enter the newly created directory and start the development server:

    $ cd TodoApp
    $ wasp start
    note

    wasp start will take a bit of time to start the server the first time you run it in a new project.

    You will see log messages from the client, server, and database setting themselves up. When everything is ready, a new tab should open in your browser at http://localhost:3000 with a simple placeholder plage:

    Screenshot of new Wasp app

    Wasp has generated for you the full front-end and back-end code the app! Next, we'll take a closer look at how the project is structured.

    + + \ No newline at end of file diff --git a/docs/tutorial/entities.html b/docs/tutorial/entities.html index 0b1577de4b..4129573501 100644 --- a/docs/tutorial/entities.html +++ b/docs/tutorial/entities.html @@ -19,13 +19,13 @@ - - + +
    -

    4. Database Entities

    Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.

    Since our Todo app is all about tasks, we will define a Task entity in the Wasp file:

    main.wasp
    // ...

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    psl=}
    note

    Wasp uses Prisma as a way to talk to the database. You define entities by defining Prisma models using the Prisma Schema Language (PSL) between the {=psl psl=} tags.

    Read more in the Entities section of the docs.

    To update the database schema to include this entity, stop the wasp start process, if its running, and run:

    wasp db migrate-dev

    You'll need to do this any time you change an entity's definition. It instructs Prisma to create a new database migration and apply it to the database.

    To take a look at the database and the new Task entity, run:

    wasp db studio

    This will open a new page in your browser to view and edit the data in your database.

    Todo App - Db studio showing Task schema

    Click on the Task entity and check out its fields! We don't have any data in our database yet, but we are about to change that.

    - - +

    4. Database Entities

    Entities are one of the most important concepts in Wasp and are how you define what gets stored in the database.

    Since our Todo app is all about tasks, we will define a Task entity in the Wasp file:

    main.wasp
    // ...

    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    psl=}
    note

    Wasp uses Prisma as a way to talk to the database. You define entities by defining Prisma models using the Prisma Schema Language (PSL) between the {=psl psl=} tags.

    Read more in the Entities section of the docs.

    To update the database schema to include this entity, stop the wasp start process, if its running, and run:

    wasp db migrate-dev

    You'll need to do this any time you change an entity's definition. It instructs Prisma to create a new database migration and apply it to the database.

    To take a look at the database and the new Task entity, run:

    wasp db studio

    This will open a new page in your browser to view and edit the data in your database.

    Todo App - Db studio showing Task schema

    Click on the Task entity and check out its fields! We don't have any data in our database yet, but we are about to change that.

    + + \ No newline at end of file diff --git a/docs/tutorial/pages.html b/docs/tutorial/pages.html index 0e89ea9b6d..bd77293adc 100644 --- a/docs/tutorial/pages.html +++ b/docs/tutorial/pages.html @@ -19,13 +19,13 @@ - - + +
    -

    3. Pages & Routes

    In the default main.wasp file created by wasp new, there is a page and a route declaration:

    main.wasp
    route RootRoute { path: "/", to: MainPage }

    page MainPage {
    // We specify that the React implementation of the page is the default export
    // of `src/client/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@client` to reference files inside the `src/client` folder.
    component: import Main from "@client/MainPage.jsx"
    }

    Together, these declarations tell Wasp that when a user navigates to /, it should render the default export from src/client/MainPage.

    The MainPage Component

    Let's take a look at the React component referenced by the page declaration:

    src/client/MainPage.jsx
    import waspLogo from './waspLogo.png'
    import './Main.css'

    const MainPage = () => {
    // ...
    }
    export default MainPage

    Since Wasp uses React for the frontend, this is a normal functional React component. It also uses the CSS and logo image that are located next to it in the src/client folder.

    That is all the code you need! Wasp takes care of everything else necessary to define, build, and run the web app.

    tip

    wasp start automatically picks up the changes you make and restarts the app, so keep it running in the background.

    Adding a Second Page

    To add more pages, you can create another set of page and route declarations. You can even add parameters to the URL path, using the same syntax as React Router. Let's test this out by adding a new page:

    main.wasp
    route HelloRoute { path: "/hello/:name", to: HelloPage }
    page HelloPage {
    component: import Hello from "@client/HelloPage.jsx"
    }

    When a user visits /hello/their-name, Wasp will render the component exported from src/client/HelloPage and pass the URL parameter the same way as in React Router:

    src/client/HelloPage.jsx
    const HelloPage = (props) => {
    return <div>Here's {props.match.params.name}!</div>
    }

    export default HelloPage

    Now you can visit /hello/johnny and see "Here's johnny!"

    Cleaning Up

    Let's prepare for building the Todo app by cleaning up the project and removing files and code we won't need. Start by deleting Main.css, waspLogo.png, and HelloPage.tsx that we just created in the src/client/ folder.

    Since we deleted HelloPage.tsx, we also need to remember to remove the route and page declarations we wrote for it. Your Wasp file should now look like this:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "Todo app"
    }

    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import Main from "@client/MainPage.jsx"
    }

    Next, we'll remove most of the code from the MainPage component:

    src/client/MainPage.jsx
    const MainPage = () => {
    return <div>Hello world!</div>
    }

    export default MainPage

    At this point, the main page should look like this:

    Todo App - Hello World

    In the next section, we'll start implementing some features of the Todo app!

    - - +

    3. Pages & Routes

    In the default main.wasp file created by wasp new, there is a page and a route declaration:

    main.wasp
    route RootRoute { path: "/", to: MainPage }

    page MainPage {
    // We specify that the React implementation of the page is the default export
    // of `src/client/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@client` to reference files inside the `src/client` folder.
    component: import Main from "@client/MainPage.jsx"
    }

    Together, these declarations tell Wasp that when a user navigates to /, it should render the default export from src/client/MainPage.

    The MainPage Component

    Let's take a look at the React component referenced by the page declaration:

    src/client/MainPage.jsx
    import waspLogo from './waspLogo.png'
    import './Main.css'

    const MainPage = () => {
    // ...
    }
    export default MainPage

    Since Wasp uses React for the frontend, this is a normal functional React component. It also uses the CSS and logo image that are located next to it in the src/client folder.

    That is all the code you need! Wasp takes care of everything else necessary to define, build, and run the web app.

    tip

    wasp start automatically picks up the changes you make and restarts the app, so keep it running in the background.

    Adding a Second Page

    To add more pages, you can create another set of page and route declarations. You can even add parameters to the URL path, using the same syntax as React Router. Let's test this out by adding a new page:

    main.wasp
    route HelloRoute { path: "/hello/:name", to: HelloPage }
    page HelloPage {
    component: import Hello from "@client/HelloPage.jsx"
    }

    When a user visits /hello/their-name, Wasp will render the component exported from src/client/HelloPage and pass the URL parameter the same way as in React Router:

    src/client/HelloPage.jsx
    const HelloPage = (props) => {
    return <div>Here's {props.match.params.name}!</div>
    }

    export default HelloPage

    Now you can visit /hello/johnny and see "Here's johnny!"

    Cleaning Up

    Let's prepare for building the Todo app by cleaning up the project and removing files and code we won't need. Start by deleting Main.css, waspLogo.png, and HelloPage.tsx that we just created in the src/client/ folder.

    Since we deleted HelloPage.tsx, we also need to remember to remove the route and page declarations we wrote for it. Your Wasp file should now look like this:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.11.0"
    },
    title: "Todo app"
    }

    route RootRoute { path: "/", to: MainPage }
    page MainPage {
    component: import Main from "@client/MainPage.jsx"
    }

    Next, we'll remove most of the code from the MainPage component:

    src/client/MainPage.jsx
    const MainPage = () => {
    return <div>Hello world!</div>
    }

    export default MainPage

    At this point, the main page should look like this:

    Todo App - Hello World

    In the next section, we'll start implementing some features of the Todo app!

    + + \ No newline at end of file diff --git a/docs/tutorial/project-structure.html b/docs/tutorial/project-structure.html index f8151c1a7c..864c70061d 100644 --- a/docs/tutorial/project-structure.html +++ b/docs/tutorial/project-structure.html @@ -19,13 +19,15 @@ - - + +
    -

    2. Project Structure

    After creating a new Wasp project, you'll get a file structure that looks like this:

    .
    ├── .gitignore
    ├── main.wasp # Your Wasp code goes here.
    ├── src
    │   ├── client # Your client code (JS/CSS/HTML) goes here.
    │   │   ├── Main.css
    │   │   ├── MainPage.jsx
    │   │   ├── vite-env.d.ts
    │   │   ├── tsconfig.json
    │   │   └── waspLogo.png
    │   ├── server # Your server code (Node JS) goes here.
    │   │   └── tsconfig.json
    │   ├── shared # Your shared (runtime independent) code goes here.
    │   │   └── tsconfig.json
    │   └── .waspignore
    └── .wasproot

    By your code, we mean the "the code you write", as opposed to the code generated by Wasp. Wasp expects you to separate all of your code—which we call external code—into three folders to make it obvious how each file is executed:

    • src/client: Contains the code executed on the client, in the browser.
    • src/server: Contains the code executed on the server, with Node.
    • src/shared: Contains code that may be executed on both the client and server.

    Many of the other files (tsconfig.json, vite-env.d.ts, etc.) are used by your IDE to improve your development experience with tools like autocompletion, intellisense, and error reporting.

    TypeScript Support

    Wasp supports TypeScript out of the box, but you are free to choose between or mix JavaScript and TypeScript as you see fit.

    We'll provide you with both JavaScript and TypeScript code in this tutorial. Code blocks will have a toggle to switch between vanilla 🍦 JavaScript and TypeScript.

    The most important file in the project is main.wasp. Wasp uses the configuration within it to perform its magic. Based on what you write, it generates a bunch of code for your database, server-client communication, React routing, and more.

    Let's look a bit closer at main.wasp.

    main.wasp

    This file, written in our Wasp configuration language, defines your app and lets Wasp take care a ton of features to your app for you. The file contains several declarations which, together, describe all the components of your app.

    The default Wasp file generated via wasp new on the previous page looks like:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.11.0" // Pins the version of Wasp to use.
    },
    title: "Todo app" // Used as the browser tab title. Note that all strings in Wasp are double quoted!
    }

    route RootRoute { path: "/", to: MainPage }

    page MainPage {
    // We specify that the React implementation of the page is the default export
    // of `src/client/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@client` to reference files inside the `src/client` folder.
    component: import Main from "@client/MainPage.jsx"
    }

    This file uses three declaration types:

    • app: Top-level configuration information about your app.

    • route: Describes which path each page should be accessible from.

    • page: Defines a web page and the React component that will be rendered when the page is loaded.

    In the next section, we'll explore how route and page work together to build your web app.

    - - +

    2. Project Structure

    After creating a new Wasp project, you'll get a file structure that looks like this:

    .
    ├── .gitignore
    ├── main.wasp # Your Wasp code goes here.
    ├── src
    │   ├── client # Your client code (JS/CSS/HTML) goes here.
    │   │   ├── Main.css
    │   │   ├── MainPage.jsx
    │   │   ├── tsconfig.json
    │   │   ├── vite.config.ts
    │   │   ├── vite-env.d.ts
    │   │   └── waspLogo.png
    │   ├── server # Your server code (Node JS) goes here.
    │   │   └── tsconfig.json
    │   ├── shared # Your shared (runtime independent) code goes here.
    │   │   └── tsconfig.json
    │   └── .waspignore
    └── .wasproot

    By your code, we mean the "the code you write", as opposed to the code generated by Wasp. Wasp expects you to separate all of your code—which we call external code—into three folders to make it obvious how each file is executed:

    • src/client: Contains the code executed on the client, in the browser.
    • src/server: Contains the code executed on the server, with Node.
    • src/shared: Contains code that may be executed on both the client and server.

    Many of the other files (tsconfig.json, vite-env.d.ts, etc.) are used by your IDE to improve your development experience with tools like autocompletion, intellisense, and error reporting. +The file vite.config.ts is used to configure Vite, Wasp's build tool of choice. +We won't be configuring Vite in this tutorial, so you can safely ignore the file. Still, if you ever end up wanting more control over Vite, you'll find everything you need to know in custom Vite config docs.

    TypeScript Support

    Wasp supports TypeScript out of the box, but you are free to choose between or mix JavaScript and TypeScript as you see fit.

    We'll provide you with both JavaScript and TypeScript code in this tutorial. Code blocks will have a toggle to switch between vanilla 🍦 JavaScript and TypeScript.

    The most important file in the project is main.wasp. Wasp uses the configuration within it to perform its magic. Based on what you write, it generates a bunch of code for your database, server-client communication, React routing, and more.

    Let's look a bit closer at main.wasp.

    main.wasp

    This file, written in our Wasp configuration language, defines your app and lets Wasp take care a ton of features to your app for you. The file contains several declarations which, together, describe all the components of your app.

    The default Wasp file generated via wasp new on the previous page looks like:

    main.wasp
    app TodoApp {
    wasp: {
    version: "^0.11.6" // Pins the version of Wasp to use.
    },
    title: "Todo app" // Used as the browser tab title. Note that all strings in Wasp are double quoted!
    }

    route RootRoute { path: "/", to: MainPage }

    page MainPage {
    // We specify that the React implementation of the page is the default export
    // of `src/client/MainPage.jsx`. This statement uses standard JS import syntax.
    // Use `@client` to reference files inside the `src/client` folder.
    component: import Main from "@client/MainPage.jsx"
    }

    This file uses three declaration types:

    • app: Top-level configuration information about your app.

    • route: Describes which path each page should be accessible from.

    • page: Defines a web page and the React component that will be rendered when the page is loaded.

    In the next section, we'll explore how route and page work together to build your web app.

    + + \ No newline at end of file diff --git a/docs/tutorial/queries.html b/docs/tutorial/queries.html index 6b01ad5fbe..5444832fa2 100644 --- a/docs/tutorial/queries.html +++ b/docs/tutorial/queries.html @@ -19,13 +19,13 @@ - - + +
    -

    5. Querying the Database

    We want to know which tasks we need to do, so let's list them! The primary way of interacting with entities in Wasp is by using queries and actions, collectively known as operations.

    Queries are used to read an entity, while actions are used to create, modify, and delete entities. Since we want to list the tasks, we'll want to use a query.

    To list tasks we have to:

    1. Create a query that fetches tasks from the database.
    2. Update the MainPage.tsx to use that query and display the results.

    Defining the Query

    We'll create a new query called getTasks. We'll need to declare the query in the Wasp file and write its implementation in .

    Declaring a Query

    We need to add a query declaration to main.wasp so that Wasp knows it exists:

    main.wasp
    // ...

    query getTasks {
    // Specifies where the implementation for the query function is.
    // Use `@server` to import files inside the `src/server` folder.
    fn: import { getTasks } from "@server/queries.js",
    // Tell Wasp that this query reads from the `Task` entity. By doing this, Wasp
    // will automatically update the results of this query when tasks are modified.
    entities: [Task]
    }

    Implementing a Query

    src/server/queries.js
    export const getTasks = async (args, context) => {
    return context.entities.Task.findMany({})
    }

    Query function parameters:

    • args: object, arguments the query is given by the caller.
    • context: object, information provided by Wasp.

    Since we declared in main.wasp that our query uses the Task entity, Wasp injected a Prisma client for the Task entity as context.entities.Task - we used it above to fetch all the tasks from the database.

    info

    Queries and actions are NodeJS functions that are executed on the server. Therefore, we put them in the src/server folder.

    Invoking the Query On the Frontend

    While we implement queries on the server, Wasp generates client-side functions that automatically takes care of serialization, network calls, and chache invalidation, allowing you to call the server code like it's a regular function. This makes it easy for us to use the getTasks query we just created in our React component:

    src/client/MainPage.jsx
    import getTasks from '@wasp/queries/getTasks'
    import { useQuery } from '@wasp/queries'

    const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    const Task = ({ task }) => {
    return (
    <div>
    <input type="checkbox" id={String(task.id)} checked={task.isDone} />
    {task.description}
    </div>
    )
    }

    const TasksList = ({ tasks }) => {
    if (!tasks?.length) return <div>No tasks</div>

    return (
    <div>
    {tasks.map((task, idx) => (
    <Task task={task} key={idx} />
    ))}
    </div>
    )
    }

    export default MainPage

    Most of this code is regular React, the only exception being the special @wasp imports:

    We could have called the query directly using getTasks(), but the useQuery hook makes it reactive: React will re-render the component every time the query changes. Remember that Wasp automatically refreshes queries whenever the data is modified.

    With these changes, you should be seeing the text "No tasks" on the screen:

    Todo App - No Tasks

    We'll create a form to add tasks in the next step 🪄

    - - +

    5. Querying the Database

    We want to know which tasks we need to do, so let's list them! The primary way of interacting with entities in Wasp is by using queries and actions, collectively known as operations.

    Queries are used to read an entity, while actions are used to create, modify, and delete entities. Since we want to list the tasks, we'll want to use a query.

    To list tasks we have to:

    1. Create a query that fetches tasks from the database.
    2. Update the MainPage.tsx to use that query and display the results.

    Defining the Query

    We'll create a new query called getTasks. We'll need to declare the query in the Wasp file and write its implementation in .

    Declaring a Query

    We need to add a query declaration to main.wasp so that Wasp knows it exists:

    main.wasp
    // ...

    query getTasks {
    // Specifies where the implementation for the query function is.
    // Use `@server` to import files inside the `src/server` folder.
    fn: import { getTasks } from "@server/queries.js",
    // Tell Wasp that this query reads from the `Task` entity. By doing this, Wasp
    // will automatically update the results of this query when tasks are modified.
    entities: [Task]
    }

    Implementing a Query

    src/server/queries.js
    export const getTasks = async (args, context) => {
    return context.entities.Task.findMany({
    orderBy: { id: 'asc' },
    })
    }

    Query function parameters:

    • args: object, arguments the query is given by the caller.
    • context: object, information provided by Wasp.

    Since we declared in main.wasp that our query uses the Task entity, Wasp injected a Prisma client for the Task entity as context.entities.Task - we used it above to fetch all the tasks from the database.

    info

    Queries and actions are NodeJS functions that are executed on the server. Therefore, we put them in the src/server folder.

    Invoking the Query On the Frontend

    While we implement queries on the server, Wasp generates client-side functions that automatically takes care of serialization, network calls, and chache invalidation, allowing you to call the server code like it's a regular function. This makes it easy for us to use the getTasks query we just created in our React component:

    src/client/MainPage.jsx
    import getTasks from '@wasp/queries/getTasks'
    import { useQuery } from '@wasp/queries'

    const MainPage = () => {
    const { data: tasks, isLoading, error } = useQuery(getTasks)

    return (
    <div>
    {tasks && <TasksList tasks={tasks} />}

    {isLoading && 'Loading...'}
    {error && 'Error: ' + error}
    </div>
    )
    }

    const Task = ({ task }) => {
    return (
    <div>
    <input type="checkbox" id={String(task.id)} checked={task.isDone} />
    {task.description}
    </div>
    )
    }

    const TasksList = ({ tasks }) => {
    if (!tasks?.length) return <div>No tasks</div>

    return (
    <div>
    {tasks.map((task, idx) => (
    <Task task={task} key={idx} />
    ))}
    </div>
    )
    }

    export default MainPage

    Most of this code is regular React, the only exception being the special @wasp imports:

    We could have called the query directly using getTasks(), but the useQuery hook makes it reactive: React will re-render the component every time the query changes. Remember that Wasp automatically refreshes queries whenever the data is modified.

    With these changes, you should be seeing the text "No tasks" on the screen:

    Todo App - No Tasks

    We'll create a form to add tasks in the next step 🪄

    + + \ No newline at end of file diff --git a/docs/tutorials/dev-excuses-app.html b/docs/tutorials/dev-excuses-app.html deleted file mode 100644 index a8bfc616dd..0000000000 --- a/docs/tutorials/dev-excuses-app.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - -Introduction | Wasp - - - - - - - - - - - - - - - - - - - -
    -

    Introduction

    info

    Make sure you've set up Wasp! Check out Getting Started first for installation instructions, and then continue with the tutorial. In case of any issues - please, ping us on Discord .

    We’ll build a web app to solve every developer's most common problem – finding an excuse to justify our messy work! We will start with a single config file that outlines the full-stack architecture of our app plus several dozen lines of code for our specific business logic. There's no faster way to do it, so we can’t excuse ourselves from building it!

    We’ll use Michele Gerarduzzi’s open-source project. It provides a simple API and a solid number of predefined excuses. A perfect fit for our needs. Let’s define the requirements for the project:

    • The app must be able to pull excuses data from a public API.
    • Users can save the excuses they like (and your boss doesn't) to the database for future reference.
    • Building an app shouldn’t take more than 15 minutes.
    • Use modern web dev technologies (NodeJS + React)

    As a result – we’ll get a simple and fun pet project. You can find the complete codebase here.

    Final result
    - - - - \ No newline at end of file diff --git a/docs/tutorials/dev-excuses-app/01-creating-the-project.html b/docs/tutorials/dev-excuses-app/01-creating-the-project.html deleted file mode 100644 index 7f30aced27..0000000000 --- a/docs/tutorials/dev-excuses-app/01-creating-the-project.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - -Creating the project | Wasp - - - - - - - - - - - - - - - - - - - -
    -

    Creating the project

    By now you've already learned how to install Wasp and create a new project. So let’s create a new web app appropriately named ItWaspsOnMyMachine.

    wasp new ItWaspsOnMyMachine

    Changing the working directory:

    cd ItWaspsOnMyMachine

    Starting the app:

    wasp start

    Now your default browser should open up with a simple predefined text message. That’s it! 🥳 For now – the codebase consists of only two files! main.wasp is the config file that defines the application’s functionality. And MainPage.js is the front-end. You can delete Main.css, we won't use that. And don't forget to remove import './Main.css' from MainPage.js file.

    Initial page
    - - - - \ No newline at end of file diff --git a/docs/tutorials/dev-excuses-app/02-modifying-main-wasp-file.html b/docs/tutorials/dev-excuses-app/02-modifying-main-wasp-file.html deleted file mode 100644 index 16a678b877..0000000000 --- a/docs/tutorials/dev-excuses-app/02-modifying-main-wasp-file.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - -Modifying main.wasp file | Wasp - - - - - - - - - - - - - - - - - - - -
    -

    Modifying main.wasp file

    First and foremost, we need to add some dependencies and introduce operations to our project. We’ll add Tailwind to make our UI prettier and Axios for making API requests.

    Also, we’ll declare a database entity called Excuse, queries, and action. The Excuse entity consists of the entity’s ID and the text.

    Queries are here when we need to fetch/read something, while actions are here when we need to change/update data. Both query and action declaration consists of two lines – a reference to the file that contains implementation and a data model to operate on. You can find more info in the docs section below. Let's move on!

    Let's add the following code to the main.wasp file's app section:

    main.wasp | Adding dependencies
      head: [
    "<script src='https://cdn.tailwindcss.com'></script>"
    ],

    dependencies: [
    ("axios", "^0.21.1")
    ]

    Next, we'll add an Excuse entity to the bottom of the file. You'll also need to define queries and an action that operates on nit.

    main.wasp | Defining Excuse entity, queries and action
    entity Excuse {=psl                                          
    id Int @id @default(autoincrement())
    text String
    psl=}

    query getExcuse {
    fn: import { getExcuse } from "@ext/queries.js",
    entities: [Excuse]
    }

    query getAllSavedExcuses {
    fn: import { getAllSavedExcuses } from "@ext/queries.js",
    entities: [Excuse]
    }

    action saveExcuse {
    fn: import { saveExcuse } from "@ext/actions.js",
    entities: [Excuse]
    }

    The resulting main.wasp file should look like this:

    main.wasp | Final result

    // Main declaration, defines a new web app.
    app ItWaspsOnMyMachine {

    // Used as a browser tab title.
    title: "It Wasps On My Machine",

    head: [
    // Adding Tailwind to make our UI prettier
    "<script src='https://cdn.tailwindcss.com'></script>"
    ],

    dependencies: [
    // Adding Axios for making HTTP requests
    ("axios", "^0.21.1")
    ]
    }

    // Render page MainPage on url `/` (default url).
    route RootRoute { path: "/", to: MainPage }

    // ReactJS implementation of our page located in `ext/MainPage.js` as a default export
    page MainPage {
    component: import Main from "@ext/MainPage.js"
    }

    // Prisma database entity
    entity Excuse {=psl
    id Int @id @default(autoincrement())
    text String
    psl=}

    // Query declaration to get a new excuse
    query getExcuse {
    fn: import { getExcuse } from "@ext/queries.js",
    entities: [Excuse]
    }

    // Query declaration to get all excuses
    query getAllSavedExcuses {
    fn: import { getAllSavedExcuses } from "@ext/queries.js",
    entities: [Excuse]
    }

    // Action to save current excuse
    action saveExcuse {
    fn: import { saveExcuse } from "@ext/actions.js",
    entities: [Excuse]
    }

    Perfect! We've set up all the architecture of our app. Now let's add some logic.

    - - - - \ No newline at end of file diff --git a/docs/tutorials/dev-excuses-app/03-adding-operations.html b/docs/tutorials/dev-excuses-app/03-adding-operations.html deleted file mode 100644 index 9edc9a8479..0000000000 --- a/docs/tutorials/dev-excuses-app/03-adding-operations.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - -Adding operations | Wasp - - - - - - - - - - - - - - - - - - - -
    -

    Adding operations

    Now you'll need to create two files: actions.js and queries.js in the ext folder.

    Let’s add saveExcuse() action to our actions.js file. This action will save the text of our excuse to the database.

    .../ext/actions.js | Defining an action
    export const saveExcuse = async (excuse, context) => {
    return context.entities.Excuse.create({
    data: { text: excuse.text }
    })
    }

    Then we need to create two queries in the queries.js file. First, one getExcuse will call an external API and fetch a new excuse. The second one, named getAllSavedExcuses, will pull all the excuses we’ve saved to our database.

    .../ext/queries.js | Defining queries
    import axios from 'axios';

    export const getExcuse = async () => {
    const response = await axios.get('https://api.devexcus.es/')
    return response.data
    }

    export const getAllSavedExcuses = async (_args, context) => {
    return context.entities.Excuse.findMany()
    }

    That’s it! We finished our back-end. 🎉 Now, let’s use those queries/actions on our UI.

    - - - - \ No newline at end of file diff --git a/docs/tutorials/dev-excuses-app/04-updating-main-page-js-file.html b/docs/tutorials/dev-excuses-app/04-updating-main-page-js-file.html deleted file mode 100644 index d28f63dec1..0000000000 --- a/docs/tutorials/dev-excuses-app/04-updating-main-page-js-file.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - -Updating MainPage.js file | Wasp - - - - - - - - - - - - - - - - - - - -
    -

    Updating MainPage.js file

    This is the most complex part, but it really comes down to mostly writing React. To make our life easier - let’s erase everything we had in the MainPage.js file and substitute it with our new UI markup.

    .../ext/MainPage.js | Updating the UI
    import React, { useState } from 'react'
    import { useQuery } from '@wasp/queries'
    import getExcuse from '@wasp/queries/getExcuse'
    import getAllSavedExcuses from '@wasp/queries/getAllSavedExcuses'
    import saveExcuse from '@wasp/actions/saveExcuse'

    const MainPage = () => {
    const [currentExcuse, setCurrentExcuse] = useState({ text: "" })
    const { data: excuses } = useQuery(getAllSavedExcuses)

    const handleGetExcuse = async () => {
    try {
    setCurrentExcuse(await getExcuse())
    } catch (err) {
    window.alert('Error while getting the excuse: ' + err.message)
    }
    }

    const handleSaveExcuse = async () => {
    if (currentExcuse.text) {
    try {
    await saveExcuse(currentExcuse)
    } catch (err) {
    window.alert('Error while saving the excuse: ' + err.message)
    }
    }
    }

    return (
    <div className="grid grid-cols-2 text-3xl">
    <div>
    <button onClick={handleGetExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Get excuse </button>
    <button onClick={handleSaveExcuse} className="mx-2 my-1 p-2 bg-blue-600 hover:bg-blue-400 text-white rounded"> Save excuse </button>
    <Excuse excuse={currentExcuse} />
    </div>
    <div>
    <div className="px-6 py-2 bg-blue-600 text-white"> Saved excuses: </div>
    {excuses && <ExcuseList excuses={excuses} />}
    </div>
    </div>
    )
    }

    const ExcuseList = (props) => {
    return props.excuses?.length ? props.excuses.map((excuse, idx) => <Excuse excuse={excuse} key={idx} />) : 'No saved excuses'
    }

    const Excuse = ({ excuse }) => {
    return (
    <div className="px-6 py-2">
    {excuse.text}
    </div>
    )
    }

    export default MainPage

    Our page consists of three components: MainPage, ExcuseList, and Excuse. It may seem at first that this file is pretty complex, but let's take a closer look:

    Excuse is just a div with an excuse text, ExcuseList checks if there are any excuses. If the list is empty – show a message No saved excuses. In other case – excuses will be displayed.

    MainPage contains info about the current excuses and the list of already saved excuses. Two button click handlers are handleGetExcuse and handleSaveExcuse. Plus, the markup itself with some Tailwind flavor.

    - - - - \ No newline at end of file diff --git a/docs/tutorials/dev-excuses-app/05-perform-migration-and-run.html b/docs/tutorials/dev-excuses-app/05-perform-migration-and-run.html deleted file mode 100644 index 15f8b6d69d..0000000000 --- a/docs/tutorials/dev-excuses-app/05-perform-migration-and-run.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - -Perform migration and run the app | Wasp - - - - - - - - - - - - - - - - - - - -
    -

    Perform migration and run the app

    Before we run our app, we need to execute a database migration. We changed the DB schema by adding new entities. By doing the migration, we sync the database schema with the entities we defined. If you’ve had something running in the terminal – stop it and run:

    wasp db migrate-dev

    You’ll be prompted to enter a name for the migration. Something like init will be ok. Now we can start the application!

    wasp start
    Final empty result

    Now you can click the “Get excuse” button to receive an excuse. You should also be able to save the ones you like with the “Save excuse” button. Our final project should look like this:

    Final result

    Now we can think of some additional improvements. For example:

    • Add a unique constraint to Entity’s ID so we won’t be able to save duplicated excuses.
    • Add exceptions and edge cases handling.
    • Make the markup prettier.
    • Optimize and polish the code

    So, we’ve been able to build a full-stack application with a database and external API call in a couple of minutes. And now we have a box full of excuses for all our development needs.

    Box of excuses for the win!

    P.S: now you're familiar with Wasp and can build full-stack apps, horaay! 🎉 How did it go? Was it fun? Drop us a message at our Discord . Now it's time to look at Todo App in Wasp if you haven't already. It will introduce some additional concepts so you'd be able to become a true Wasp overlord!

    - - - - \ No newline at end of file diff --git a/docs/typescript.html b/docs/typescript.html index 1cab7b8a8d..942a580107 100644 --- a/docs/typescript.html +++ b/docs/typescript.html @@ -19,13 +19,13 @@ - - + +

    Using Wasp with TypeScript

    Deprecated Page
    This page is part of a previous documentation version and is no longer actively maintained. The content is likely out of date and may no longer be relevant to current releases.

    Go to the current documentation for updated content.

    TypeScript is a programming language that brings static type analysis to JavaScript. It is a superset of JavaScript (i.e., all valid JavaScript programs are valid TypeScript programs) and compiles to JavaScript before running. TypeScript's type system detects common errors at build time (reducing the chance of runtime errors in production) and enables type-based auto-completion in IDEs.

    This document assumes you are familiar with TypeScript and primarily focuses on how to use it with Wasp. To learn more about TypeScript itself, we recommend reading the official docs.

    The document also assumes a basic understanding of core Wasp features (e.g., Queries, Actions, Entities). You can read more about these features in our feature docs.

    Besides allowing you to write your code in TypeScript, Wasp also supports:

    • Importing and using Wasp Entity types (on both the server and the client).
    • Automatic full-stack type support for Queries and Actions - frontend types are automatically inferred from backend definitions.
    • Type-safe generic hooks (useQuery and useAction) with the accompanying type inference.
    • Type-safe optimistic update definitions.

    We'll dig into the details of each feature in the following sections. But first, let's see how you can introduce TypeScript to an existing Wasp project.

    info

    To get the best IDE experience, make sure to leave wasp start running in the background. Wasp will track the working directory and ensure the generated code/types are up to date with your changes.

    Your editor may sometimes report type and import errors even while wasp start is running. This happens when the TypeScript Language Server gets out of sync with the current code. If you're using VS Code, you can manually restart the language server by opening the command palette and selecting "TypeScript: Restart TS Server."

    Migrating your project to TypeScript

    Wasp supports TypeScript out of the box!

    Our scaffolding already includes TypeScript, so migrating your project to TypeScript is as simple as changing file extensions and using the language. This approach allows you to gradually migrate your project on a file-by-file basis.

    Example

    Let's first assume your Wasp file contains the following definitions:

    main.wasp
    entity Task {=psl
    id Int @id @default(autoincrement())
    description String
    isDone Boolean @default(false)
    psl=}

    query getTaskInfo {
    fn: import { getTaskInfo } from "@server/queries.js",
    entities: [Task]
    }

    Let's now assume that your queries.js file looks something like this:

    src/server/queries.js
    import HttpError from "@wasp/core/HttpError.js"

    function getInfoMessage(task) {
    const isDoneText = task.isDone ? "is done" : "is not done"
    return `Task '${task.description}' is ${isDoneText}.`
    }

    export const getTaskInfo = async ({ id }, context) => {
    const Task = context.entities.Task
    const task = await Task.findUnique({ where: { id } })
    if (!task) {
    throw new HttpError(404)
    }
    return getInfoMessage(task)
    }

    To migrate this file to TypeScript, all you have to do is:

    1. Change the filename from queries.js to queries.ts.
    2. Write some types.

    Let's start by only providing a basic getInfoMessage function. We'll see how to properly type the rest of the file in the following sections.

    src/server/queries.ts
    import HttpError from "@wasp/core/HttpError.js"

    function getInfoMessage(task: {
    isDone: boolean
    description: string
    }): string {
    const isDoneText = task.isDone ? "is done" : "is not done"
    return `Task '${task.description}' is ${isDoneText}.`
    }

    export const getTaskInfo = async ({ id }, context) => {
    const Task = context.entities.Task
    const task = await Task.findUnique({ where: { id } })
    if (!task) {
    throw new HttpError(404)
    }
    return getInfoMessage(task)
    }

    You don't need to change anything inside the .wasp file.

    caution

    Even when you use TypeScript, and your file is called queries.ts, you still need to import it using the .js extension:

    query getTaskInfo {
    fn: import { getTaskInfo } from "@server/queries.js",
    entities: [Task]
    }

    Wasp internally uses esnext module resolution, which always requires specifying the extension as .js (i.e., the extension used in the emitted JS file). This applies to all @server imports (and files on the server in general). This quirk does not apply to client files (the transpiler takes care of it).

    Read more about ES modules in TypeScript here. If you're interested in the discussion and the reasoning behind this, read about it in this GitHub issue.

    Entity Types

    Instead of manually specifying the types for isDone and description, we can get them from the Task entity type. Wasp will generate types for all entities and let you import them from "@wasp/entities":

    src/server/queries.ts
    import HttpError from "@wasp/core/HttpError.js"
    import { Task } from "@wasp/entities"

    function getInfoMessage(task: Pick<Task, "isDone" | "description">): string {
    const isDoneText = task.isDone ? "is done" : "is not done"
    return `Task '${task.description}' is ${isDoneText}.`
    }

    export const getTaskInfo = async ({ id }, context) => {
    const Task = context.entities.Task
    const task = await Task.findUnique({ where: { id } })
    if (!task) {
    throw new HttpError(404)
    }
    return getInfoMessage(task)
    }

    By doing this, we've connected the argument type of the getInfoMessage function with the Task entity. This coupling removes duplication and ensures the function keeps the correct signature even if we change the entity. Of course, the function might throw type errors depending on how we change the entity, but that's precisely what we want!

    Don't worry about typing the query function for now. We'll see how to handle this in the next section.

    Entity types are also available on the client under the same import:

    src/client/Main.jsx
    import { Task } from "@wasp/entities"

    export function ExamplePage() {}
    const task: Task = {
    id: 123,
    description: "Some random task",
    isDone: false,
    }
    return <div>{task.description}</div>
    }

    The mentioned type safety mechanisms also apply here: changing the task entity in our .wasp file changes the imported type, which might throw a type error and warn us that our task definition is outdated.

    Backend type support for Queries and Actions

    Wasp automatically generates the appropriate types for all Operations (i.e., Actions and Queries) you define inside your .wasp file. Assuming your .wasp file contains the following definition:

    main.wasp
    // ...

    query GetTaskInfo {
    fn: import { getTaskInfo } from "@server/queries.js",
    entities: [Task]
    }

    Wasp will generate a type called GetTaskInfo, which you can use to type the Query's implementation. By assigning the GetTaskInfo type to your function, you get the type information for its context. In this case, TypeScript will know the context.entities object must include the Task entity. If the Query had auth enabled, it would also know that context includes user information.

    GetTaskInfo can is a generic type that takes two (optional) type arguments:

    1. Input - The argument (i.e., payload) received by the query function.
    2. Output - The query function's return type.

    Suppose you don't care about typing the Query's inputs and outputs. In that case, you can omit both type arguments, and TypeScript will infer the most general types (i.e., never for the input, unknown for the output.).

    src/server/queries.ts
    import HttpError from "@wasp/core/HttpError.js"
    import { Task } from "@wasp/entities"
    import { GetTaskInfo } from "@wasp/queries/types"

    function getInfoMessage(task: Pick<Task, "isDone" | "description">): string {
    const isDoneText = task.isDone ? "is done" : "is not done"
    return `Task '${task.description}' is ${isDoneText}.`
    }

    // Use the type parameters to specify the Query's argument and return types.
    export const getTaskInfo: GetTaskInfo<Pick<Task, "id">, string> = async ({ id }, context) => {
    // Thanks to the definition in your .wasp file, the compiler knows the type of
    // `context` (and that it contains the `Task` entity).
    const Task = context.entities.Task

    // Thanks to the first type argument in `GetTaskInfo`, the compiler knows `args`
    // is of type `Pick<Task, "id">`.
    const task = await Task.findUnique({ where: { id } })
    if (!task) {
    throw new HttpError(404)
    }

    // Thanks to the second type argument in `GetTaskInfo`, the compiler knows the
    // function must return a value of type `string`.
    return getInfoMessage(task)
    }

    Everything described above applies to Actions as well.

    tip

    If don't want to define a new type for the Query's return value, the new satisfies keyword will allow TypeScript to infer it automatically:

    const getFoo = (async (_args, context) => {
    const foos = await context.entities.Foo.findMany()
    return {
    foos,
    message: "Here are some foos!",
    queriedAt: new Date(),
    }
    }) satisfies GetFoo

    From the snippet above, TypeScript knows:

    1. The correct type for context.
    2. The Query's return type is { foos: Foo[], message: string, queriedAt: Date }.

    If you don't need the context, you can skip specifying the Query's type (and arguments):

    const getFoo = () => {{ name: 'Foo', date: new Date() }}

    Frontend type support for Queries and Actions

    Wasp supports automatic full-stack type safety à la tRPC. You only need to define the Operation's type on the backend, and the frontend will automatically know how to call it.

    Frontend type support for Queries

    The examples assume you've defined the Query getTaskInfo from the previous sections:

    src/server/queries.ts
    export const getTaskInfo: GetTaskInfo<Pick<Task, "id">, string> = 
    async ({ id }, context) => {
    // ...
    }

    Wasp will use the type of getTaskInfo to infer the Query's types on the frontend:

    src/client/TaskInfo.tsx
    import { useQuery } from "@wasp/queries"
    // Wasp knows the type of `getTaskInfo` thanks to your backend definition.
    import getTaskInfo from "@wasp/queries/getTaskInfo"

    export const TaskInfo = () => {
    const {
    // TypeScript knows `taskInfo` is a `string | undefined` thanks to the
    // backend definition.
    data: taskInfo,
    // TypeScript also knows `isError` is a `boolean`.
    isError,
    // TypeScript knows `error` is of type `Error`.
    error,
    // TypeScript knows `id` must be a `Task["id"]` (i.e., a number) thanks to
    // your backend definition.
    } = useQuery(getTaskInfo, { id: 1 })

    if (isError) {
    return <div> Error during fetching tasks: {error.message || "unknown"}</div>
    }

    // TypeScript forces you to perform this check.
    return taskInfo === undefined ? (
    <div>Waiting for info...</div>
    ) : (
    <div>{taskInfo}</div>
    )
    }

    Frontend type support for Actions

    Assuming the following action definition in your .wasp file

    main.wasp
    action addTask {
    fn: import { addTask } from "@server/actions.js"
    entities: [Task]
    }

    And its corresponding implementation in src/server/actions.ts:

    src/server/actions.ts
    import { AddTask } from "@wasp/actions/types"

    type TaskPayload = Pick<Task, "description" | "isDone">

    const addTask: AddTask<TaskPayload, Task> = async (args, context) => {
    // ...
    }

    Here's how to use it on the frontend:

    src/client/AddTask.tsx
    import { useAction } from "@wasp/actions"
    // TypeScript knows `addTask` is a function that expects a value of type
    // `TaskPayload` and returns a value of type `Promise<Task>`.
    import addTask from "@wasp/queries/addTask"

    const AddTask = ({ description }: Pick<Task, "description">) => {
    return (
    <div>
    <button onClick={() => addTask({ description, isDone: false })}>
    Add unfinished task
    </button>
    <button onClick={() => addTask({ description, isDone: true })}>
    Add finished task
    </button>
    </div>
    )
    }

    Type support for the useAction hook

    Type inference also works if you decide to use the action via the useAction hook:

    // addTaskFn is of type (args: TaskPayload) => Task
    const addTaskFn = useAction(addTask)

    The useAction hook also includes support for optimistic updates. Read the feature docs to understand more about optimistic updates and how to define them in Wasp.

    Here's an example that shows how you can use static type checking in their definitions (the example assumes an appropriate action defined in the .wasp file and implemented on the server):

    Task.tsx
    import { useQuery } from "@wasp/queries"
    import { OptimisticUpdateDefinition, useAction } from "@wasp/actions"
    import updateTaskIsDone from "@wasp/actions/updateTaskIsDone"

    type TaskPayload = Pick<Task, "id" | "isDone">

    const Task = ({ taskId }: Pick<Task, "id">) => {
    const updateTaskIsDoneOptimistically = useAction(
    updateTaskIsDone,
    {
    optimisticUpdates: [
    {
    getQuerySpecifier: () => [getTask, { id: taskId }],
    // This query's cache should should never be empty
    updateQuery: ({ isDone }, oldTask) => ({ ...oldTask!, isDone }),
    } as OptimisticUpdateDefinition<TaskPayload, Task>,
    {
    getQuerySpecifier: () => [getTasks],
    updateQuery: (updatedTask, oldTasks) =>
    oldTasks &&
    oldTasks.map((task) =>
    task.id === updatedTask.id ? { ...task, ...updatedTask } : task
    ),
    } as OptimisticUpdateDefinition<TaskPayload, Task[]>,
    ],
    }
    )
    // ...
    }

    Database seeding

    When implementing a seed function in TypeScript, you can import a DbSeedFn type via

    import type { DbSeedFn } from "@wasp/dbSeed/types.js"

    and use it to type your seed function like this:

    export const devSeedSimple: DbSeedFn = async (prismaClient) => { ... }

    CRUD operations on entities

    For a specific Entity, you can tell Wasp to automatically instantiate server-side logic (Queries and Actions) for creating, reading, updating and deleting such entities.

    Read more about CRUD operations in Wasp here.

    Using types for CRUD operations overrides

    If you writing the override implementation in Typescript, you'll have access to generated types. The overrides are functions that take the following arguments:

    • args - The arguments of the operation i.e. the data that's sent from the client.
    • context - Context containing the user making the request and the entities object containing the entity that's being operated on.

    You can types for each of the functions you want to override from @wasp/crud/{crud name}. The types that are available are:

    • GetAllQuery
    • GetQuery
    • CreateAction
    • UpdateAction
    • DeleteAction

    If you have a CRUD named Tasks, you would import the types like this:

    import type { GetAllQuery, GetQuery, CreateAction, UpdateAction, DeleteAction } from '@wasp/crud/Tasks'

    // Each of the types is a generic type, so you can use it like this:
    export const getAllOverride: GetAllQuery<Input, Output> = async (args, context) => {
    // ...
    }

    WebSocket full-stack type support

    Defining event names with the matching payload types on the server makes those types exposed automatically on the client. This helps you avoid mistakes when emitting events or handling them.

    Defining the events handler

    On the server, you will get Socket.IO io: Server argument and context for your WebSocket function, which contains all entities you defined in your Wasp app. You can type the webSocketFn function like this:

    src/server/webSocket.ts
    import type { WebSocketDefinition, WaspSocketData } from '@wasp/webSocket'

    // Using the generic WebSocketDefinition type to define the WebSocket function.
    type WebSocketFn = WebSocketDefinition<
    ClientToServerEvents,
    ServerToClientEvents,
    InterServerEvents,
    SocketData
    >

    interface ServerToClientEvents {
    // The type for the payload of the "chatMessage" event.
    chatMessage: (msg: { id: string, username: string, text: string }) => void;
    }

    interface ClientToServerEvents {
    // The type for the payload of the "chatMessage" event.
    chatMessage: (msg: string) => void;
    }

    interface InterServerEvents {}

    interface SocketData extends WaspSocketData {}

    // Use the WebSocketFn to type the webSocketFn function.
    export const webSocketFn: WebSocketFn = (io, context) => {
    io.on('connection', (socket) => {
    socket.on('chatMessage', async (msg) => {
    io.emit('chatMessage', { ... })
    })
    })
    }

    Using the WebSocket on the client

    After you have defined the WebSocket function on the server, you can use it on the client. The useSocket hook will give you the socket instance and the isConnected boolean. The socket instance is typed with the types you defined on the server.

    The useSocketListener hook will give you a type-safe event handler. The event name and its payload type are defined on the server.

    You can additonally use the ClientToServerPayload and ServerToClientPayload helper types to get the payload type for a specific event.

    src/client/ChatPage.tsx
    import React, { useState } from 'react'
    import {
    useSocket,
    useSocketListener,
    ServerToClientPayload,
    ClientToServerPayload,
    } from '@wasp/webSocket'

    export const ChatPage = () => {
    const [messageText, setMessageText] = useState<
    // We are using a helper type to get the payload type for the "chatMessage" event.
    ClientToServerPayload<'chatMessage'>
    >('')

    const [messages, setMessages] = useState<
    // We are using a helper type to get the payload type for the "chatMessage" event.
    ServerToClientPayload<'chatMessage'>[]
    >([])

    // The "socket" instance is typed with the types you defined on the server.
    const { socket, isConnected } = useSocket()

    // This is a type-safe event handler: "chatMessage" event and its payload type
    // are defined on the server.
    useSocketListener('chatMessage', logMessage)

    function logMessage(msg: ServerToClientPayload<'chatMessage'>) {
    // ...
    }

    function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    // This is a type-safe event emitter: "chatMessage" event and its payload type
    // are defined on the server.
    socket.emit('chatMessage', messageText)
    setMessageText('')
    }

    return (
    ...
    )
    }
    - - + + \ No newline at end of file diff --git a/docs/vision.html b/docs/vision.html index 99a332b2b7..94ce3c0423 100644 --- a/docs/vision.html +++ b/docs/vision.html @@ -19,12 +19,12 @@ - - + +
    -

    Vision

    With Wasp, we want to make developing web apps easy and enjoyable, for novices and experts in web development alike.

    Ideal we are striving for is that programming in Wasp feels like describing an app using a human language - like writing a specification document where you describe primarily your requirements and as little implementation details as possible. +

    Vision

    With Wasp, we want to make developing web apps easy and enjoyable, for novices and experts in web development alike.

    Ideal we are striving for is that programming in Wasp feels like describing an app using a human language - like writing a specification document where you describe primarily your requirements and as little implementation details as possible. Creating a new production-ready web app should be easy and deploying it to production should be straightforward.

    That is why we believe Wasp needs to be a programming language (DSL) and not a library - we want to capture all parts of the web app into one integrated system that is perfectly tailored just for that purpose.
    On the other hand, we believe that trying to capture every single detail in one language would not be reasonable. There are solutions out there that work very well for the specific task they aim to solve (React for web components, CSS/HTML for design/markup, JS/TS for logic, ...) and we don't want to replace them with Wasp. @@ -33,7 +33,7 @@ They can be used inline (mixed with Wasp code) or provided via external files.

  • Has hatches (escape mechanisms) that allow you to customize your web app in all the right places, but remain hidden until you need them.
  • Entity (data model) is a first-class citizen - defined via custom Wasp syntax and it integrates very closely with the rest of the features, serving as one of the central concepts around which everything is built.
  • Out of the box support for CRUD UI based on the Entities, to get you quickly going, but also customizable to some level.
  • "Smart" operations (queries and actions) that in most cases automatically figure out when to update, and if not it is easy to define custom logic to compensate for that. User worries about client-server gap as little as possible.
  • Support, directly in Wasp, for declaratively defining simple components and operations.
  • Besides Wasp as a programming language, there will also be a visual builder that generates/edits Wasp code, allowing non-developers to participate in development. Since Wasp is declarative, we imagine such builder to naturally follow from Wasp language.
  • Server side rendering, caching, packaging, security, ... -> all those are taken care of by Wasp. You tell Wasp what you want, and Wasp figures out how to do it.
  • As simple deployment to production/staging as it gets.
  • While it comes with the official implementation(s), Wasp language will not be coupled with the single implementation. Others can provide implementations that compile to different web app stacks.
  • - - + + \ No newline at end of file diff --git a/index.html b/index.html index bffaf6da15..355a5f8d81 100644 --- a/index.html +++ b/index.html @@ -19,8 +19,8 @@ - - + +
    @@ -61,10 +61,10 @@ Full support for TypeScript with auto-generated types that span the whole stack.

    Learn more
    And More!

    Custom API routes, database seeding, optimistic updates, automatic cache invalidation on the client, ... -

    Learn more

    How does it work? 🧐

    Given a simple .wasp configuration file that describes the high-level details of your web app, and .js(x)/.css/..., source files with your unique logic, Wasp compiler generates the full source of your web app in the target stack: front-end, back-end and deployment.

    This unique approach is what makes Wasp "smart" and gives it its super powers!

    Simple config language

    Declaratively describe high-level details of your app.

    Learn more

    Wasp CLI

    All the handy commands at your fingertips.

    Learn more

    React / Node.js / Prisma

    You are still writing 90% of the code in your favorite technologies.

    Arrivederci boilerplate

    Write only the code that matters, let Wasp handle the rest.

    Learn more
    React

    Show, don't tell.

    Take a look at examples - see how things work and get inspired for your next project.

    Waspello 📝

    A Trello clone made with Wasp.

    wasp GitHub profile picturewasp

    Real World App 🐑

    A Medium clone made with Wasp and Material UI.

    wasp GitHub profile picturewasp

    Waspleau 📊

    A simple data dashboard that makes use of Wasp async jobs feature.

    wasp GitHub profile picturewasp

    You're in a good crowd

    Here's what folks using Wasp say about it. Join our Discord for more!

    How does it work? 🧐

    Given a simple .wasp configuration file that describes the high-level details of your web app, and .js(x)/.css/..., source files with your unique logic, Wasp compiler generates the full source of your web app in the target stack: front-end, back-end and deployment.

    This unique approach is what makes Wasp "smart" and gives it its super powers!

    Simple config language

    Declaratively describe high-level details of your app.

    Learn more

    Wasp CLI

    All the handy commands at your fingertips.

    Learn more

    React / Node.js / Prisma

    You are still writing 90% of the code in your favorite technologies.

    Arrivederci boilerplate

    Write only the code that matters, let Wasp handle the rest.

    Learn more
    React

    Show, don't tell.

    Take a look at examples - see how things work and get inspired for your next project.

    Waspello 📝

    A Trello clone made with Wasp.

    wasp GitHub profile picturewasp

    Real World App 🐑

    A Medium clone made with Wasp and Material UI.

    wasp GitHub profile picturewasp

    Waspleau 📊

    A simple data dashboard that makes use of Wasp async jobs feature.

    wasp GitHub profile picturewasp

    Stay up to date 📬

    Be the first to know when we ship new features and updates!

    🚧 Roadmap 🚧

    Work on Wasp never stops: get a glimpse of what is coming next!

    Near-term improvements and features
    • Improve Wasp project structure 
      734
    • Allow custom steps in the build pipeline 
      906
    • Support for SSR / SSG 
      911
    • Automatic generation of API for Operations 
      863
    • Better Prisma support (more features, IDE) 
      641
    • Support for backend testing 
      110
    • Better way to define JS dependencies 
      243
    Advanced Features
    • Top-level data schema 
      642
    • Automatic generation of CRUD UI 
      489
    • Multiple targets (e.g. mobile) 
      1088
    • Multiple servers, serverless
    • Polyglot
    • Multiple frontend libraries
    • Full-stack modules

    Frequently asked questions

    For anything not covered here, join our Discord!

    - - + + \ No newline at end of file diff --git a/search.html b/search.html index ec50dc8f6b..42d27f5cd7 100644 --- a/search.html +++ b/search.html @@ -19,13 +19,13 @@ - - + +

    Search the documentation

    - - + + \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index e3d0fa67ba..d977c31a49 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1 +1 @@ -https://wasp-lang.dev/blogweekly0.5https://wasp-lang.dev/blog/2019/09/01/hello-waspweekly0.5https://wasp-lang.dev/blog/2021/02/23/journey-to-ycombinatorweekly0.5https://wasp-lang.dev/blog/2021/03/02/wasp-alphaweekly0.5https://wasp-lang.dev/blog/2021/04/29/discord-bot-introductionweekly0.5https://wasp-lang.dev/blog/2021/09/01/haskell-forall-tutorialweekly0.5https://wasp-lang.dev/blog/2021/11/21/seed-roundweekly0.5https://wasp-lang.dev/blog/2021/11/22/fundraising-learningsweekly0.5https://wasp-lang.dev/blog/2021/12/02/waspelloweekly0.5https://wasp-lang.dev/blog/2021/12/21/shayne-introweekly0.5https://wasp-lang.dev/blog/2022/01/27/waspleauweekly0.5https://wasp-lang.dev/blog/2022/05/31/filip-introweekly0.5https://wasp-lang.dev/blog/2022/06/01/gitpod-hackathon-guideweekly0.5https://wasp-lang.dev/blog/2022/06/15/jobs-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-futureweekly0.5https://wasp-lang.dev/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joiningweekly0.5https://wasp-lang.dev/blog/2022/08/26/how-and-why-i-got-started-with-haskellweekly0.5https://wasp-lang.dev/blog/2022/09/02/how-to-get-started-with-haskell-in-2022weekly0.5https://wasp-lang.dev/blog/2022/09/05/dev-excuses-app-tutrialweekly0.5https://wasp-lang.dev/blog/2022/09/29/journey-to-1000-gh-starsweekly0.5https://wasp-lang.dev/blog/2022/10/28/farnance-hackathon-winnerweekly0.5https://wasp-lang.dev/blog/2022/11/15/auth-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/16/alpha-testing-program-post-mortemweekly0.5https://wasp-lang.dev/blog/2022/11/16/tailwind-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/17/hacktoberfest-wrap-upweekly0.5https://wasp-lang.dev/blog/2022/11/26/erlis-amicus-usecaseweekly0.5https://wasp-lang.dev/blog/2022/11/26/michael-curry-usecaseweekly0.5https://wasp-lang.dev/blog/2022/11/26/wasp-beta-launch-weekweekly0.5https://wasp-lang.dev/blog/2022/11/28/why-we-chose-prismaweekly0.5https://wasp-lang.dev/blog/2022/11/29/permissions-in-web-appsweekly0.5https://wasp-lang.dev/blog/2022/11/29/typescript-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/29/wasp-betaweekly0.5https://wasp-lang.dev/blog/2022/11/30/optimistic-update-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/12/01/beta-ide-improvementsweekly0.5https://wasp-lang.dev/blog/2022/12/08/fast-fullstack-chatgptweekly0.5https://wasp-lang.dev/blog/2023/01/11/betathon-reviewweekly0.5https://wasp-lang.dev/blog/2023/01/18/wasp-beta-update-decweekly0.5https://wasp-lang.dev/blog/2023/01/31/wasp-beta-launch-reviewweekly0.5https://wasp-lang.dev/blog/2023/02/02/no-best-frameworkweekly0.5https://wasp-lang.dev/blog/2023/02/14/amicus-indiehacker-interviewweekly0.5https://wasp-lang.dev/blog/2023/02/21/junior-developer-misconceptionsweekly0.5https://wasp-lang.dev/blog/2023/03/02/wasp-beta-update-febweekly0.5https://wasp-lang.dev/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hearweekly0.5https://wasp-lang.dev/blog/2023/03/08/building-a-full-stack-app-supabase-vs-waspweekly0.5https://wasp-lang.dev/blog/2023/03/17/new-react-docs-pretend-spas-dont-existweekly0.5https://wasp-lang.dev/blog/2023/04/11/wasp-launch-week-twoweekly0.5https://wasp-lang.dev/blog/2023/04/12/auth-uiweekly0.5https://wasp-lang.dev/blog/2023/04/13/db-start-and-seedweekly0.5https://wasp-lang.dev/blog/2023/04/17/How-I-Built-CoverLetterGPTweekly0.5https://wasp-lang.dev/blog/2023/04/27/wasp-hackathon-twoweekly0.5https://wasp-lang.dev/blog/2023/05/19/hackathon-2-reviewweekly0.5https://wasp-lang.dev/blog/2023/06/07/wasp-beta-update-may-23weekly0.5https://wasp-lang.dev/blog/2023/06/22/wasp-launch-week-threeweekly0.5https://wasp-lang.dev/blog/2023/06/27/build-your-own-twitter-agent-langchainweekly0.5https://wasp-lang.dev/blog/2023/06/28/what-can-you-build-with-waspweekly0.5https://wasp-lang.dev/blog/2023/06/29/new-wasp-lspweekly0.5https://wasp-lang.dev/blog/2023/06/30/tutorial-jamweekly0.5https://wasp-lang.dev/blog/2023/07/10/gpt-web-app-generatorweekly0.5https://wasp-lang.dev/blog/2023/07/17/how-we-built-gpt-web-app-generatorweekly0.5https://wasp-lang.dev/blog/2023/08/01/smol-ai-vs-wasp-aiweekly0.5https://wasp-lang.dev/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescriptweekly0.5https://wasp-lang.dev/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-aiweekly0.5https://wasp-lang.dev/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-callweekly0.5https://wasp-lang.dev/blog/2023/10/04/contributing-open-source-land-a-jobweekly0.5https://wasp-lang.dev/blog/2023/10/12/on-importance-of-naming-in-programmingweekly0.5https://wasp-lang.dev/blog/2023/10/13/wasp-launch-week-fourweekly0.5https://wasp-lang.dev/blog/archiveweekly0.5https://wasp-lang.dev/blog/tagsweekly0.5https://wasp-lang.dev/blog/tags/agentweekly0.5https://wasp-lang.dev/blog/tags/aiweekly0.5https://wasp-lang.dev/blog/tags/authweekly0.5https://wasp-lang.dev/blog/tags/careerweekly0.5https://wasp-lang.dev/blog/tags/chakraweekly0.5https://wasp-lang.dev/blog/tags/chatgptweekly0.5https://wasp-lang.dev/blog/tags/clean-codeweekly0.5https://wasp-lang.dev/blog/tags/cssweekly0.5https://wasp-lang.dev/blog/tags/databaseweekly0.5https://wasp-lang.dev/blog/tags/discordweekly0.5https://wasp-lang.dev/blog/tags/expressweekly0.5https://wasp-lang.dev/blog/tags/featureweekly0.5https://wasp-lang.dev/blog/tags/frameworkweekly0.5https://wasp-lang.dev/blog/tags/full-stackweekly0.5https://wasp-lang.dev/blog/tags/fullstackweekly0.5https://wasp-lang.dev/blog/tags/function-callingweekly0.5https://wasp-lang.dev/blog/tags/generateweekly0.5https://wasp-lang.dev/blog/tags/githubweekly0.5https://wasp-lang.dev/blog/tags/gitpodweekly0.5https://wasp-lang.dev/blog/tags/gptweekly0.5https://wasp-lang.dev/blog/tags/hackweekly0.5https://wasp-lang.dev/blog/tags/hackathonweekly0.5https://wasp-lang.dev/blog/tags/hacktoberfestweekly0.5https://wasp-lang.dev/blog/tags/haskellweekly0.5https://wasp-lang.dev/blog/tags/hiringweekly0.5https://wasp-lang.dev/blog/tags/indie-hackerweekly0.5https://wasp-lang.dev/blog/tags/interviewweekly0.5https://wasp-lang.dev/blog/tags/javascriptweekly0.5https://wasp-lang.dev/blog/tags/jobsweekly0.5https://wasp-lang.dev/blog/tags/junior-developersweekly0.5https://wasp-lang.dev/blog/tags/langchainweekly0.5https://wasp-lang.dev/blog/tags/languageweekly0.5https://wasp-lang.dev/blog/tags/launch-weekweekly0.5https://wasp-lang.dev/blog/tags/memeweekly0.5https://wasp-lang.dev/blog/tags/mlweekly0.5https://wasp-lang.dev/blog/tags/new-hireweekly0.5https://wasp-lang.dev/blog/tags/nodeweekly0.5https://wasp-lang.dev/blog/tags/nodejsweekly0.5https://wasp-lang.dev/blog/tags/open-sourceweekly0.5https://wasp-lang.dev/blog/tags/openaiweekly0.5https://wasp-lang.dev/blog/tags/optimisticweekly0.5https://wasp-lang.dev/blog/tags/pernweekly0.5https://wasp-lang.dev/blog/tags/prdweekly0.5https://wasp-lang.dev/blog/tags/prismaweekly0.5https://wasp-lang.dev/blog/tags/product-requirementweekly0.5https://wasp-lang.dev/blog/tags/product-updateweekly0.5https://wasp-lang.dev/blog/tags/programmingweekly0.5https://wasp-lang.dev/blog/tags/reactweekly0.5https://wasp-lang.dev/blog/tags/real-timeweekly0.5https://wasp-lang.dev/blog/tags/redditweekly0.5https://wasp-lang.dev/blog/tags/saa-sweekly0.5https://wasp-lang.dev/blog/tags/saasweekly0.5https://wasp-lang.dev/blog/tags/showcaseweekly0.5https://wasp-lang.dev/blog/tags/solopreneurweekly0.5https://wasp-lang.dev/blog/tags/startupweekly0.5https://wasp-lang.dev/blog/tags/startupsweekly0.5https://wasp-lang.dev/blog/tags/state-of-jsweekly0.5https://wasp-lang.dev/blog/tags/stripeweekly0.5https://wasp-lang.dev/blog/tags/supabaseweekly0.5https://wasp-lang.dev/blog/tags/tech-careerweekly0.5https://wasp-lang.dev/blog/tags/tutorialweekly0.5https://wasp-lang.dev/blog/tags/typescriptweekly0.5https://wasp-lang.dev/blog/tags/updateweekly0.5https://wasp-lang.dev/blog/tags/updatesweekly0.5https://wasp-lang.dev/blog/tags/waspweekly0.5https://wasp-lang.dev/blog/tags/wasp-aiweekly0.5https://wasp-lang.dev/blog/tags/web-devweekly0.5https://wasp-lang.dev/blog/tags/web-developmentweekly0.5https://wasp-lang.dev/blog/tags/webdevweekly0.5https://wasp-lang.dev/blog/tags/websocketsweekly0.5https://wasp-lang.dev/searchweekly0.5https://wasp-lang.dev/docsweekly0.5https://wasp-lang.dev/docs/advanced/apisweekly0.5https://wasp-lang.dev/docs/advanced/deployment/cliweekly0.5https://wasp-lang.dev/docs/advanced/deployment/manuallyweekly0.5https://wasp-lang.dev/docs/advanced/deployment/overviewweekly0.5https://wasp-lang.dev/docs/advanced/emailweekly0.5https://wasp-lang.dev/docs/advanced/jobsweekly0.5https://wasp-lang.dev/docs/advanced/linksweekly0.5https://wasp-lang.dev/docs/advanced/middleware-configweekly0.5https://wasp-lang.dev/docs/advanced/web-socketsweekly0.5https://wasp-lang.dev/docs/auth/emailweekly0.5https://wasp-lang.dev/docs/auth/overviewweekly0.5https://wasp-lang.dev/docs/auth/social-auth/githubweekly0.5https://wasp-lang.dev/docs/auth/social-auth/googleweekly0.5https://wasp-lang.dev/docs/auth/social-auth/overviewweekly0.5https://wasp-lang.dev/docs/auth/uiweekly0.5https://wasp-lang.dev/docs/auth/username-and-passweekly0.5https://wasp-lang.dev/docs/contactweekly0.5https://wasp-lang.dev/docs/contributingweekly0.5https://wasp-lang.dev/docs/data-model/backendsweekly0.5https://wasp-lang.dev/docs/data-model/crudweekly0.5https://wasp-lang.dev/docs/data-model/entitiesweekly0.5https://wasp-lang.dev/docs/data-model/operations/actionsweekly0.5https://wasp-lang.dev/docs/data-model/operations/overviewweekly0.5https://wasp-lang.dev/docs/data-model/operations/queriesweekly0.5https://wasp-lang.dev/docs/editor-setupweekly0.5https://wasp-lang.dev/docs/examplesweekly0.5https://wasp-lang.dev/docs/general/cliweekly0.5https://wasp-lang.dev/docs/general/languageweekly0.5https://wasp-lang.dev/docs/language/featuresweekly0.5https://wasp-lang.dev/docs/project/client-configweekly0.5https://wasp-lang.dev/docs/project/css-frameworksweekly0.5https://wasp-lang.dev/docs/project/customizing-appweekly0.5https://wasp-lang.dev/docs/project/dependenciesweekly0.5https://wasp-lang.dev/docs/project/env-varsweekly0.5https://wasp-lang.dev/docs/project/server-configweekly0.5https://wasp-lang.dev/docs/project/starter-templatesweekly0.5https://wasp-lang.dev/docs/project/static-assetsweekly0.5https://wasp-lang.dev/docs/project/testingweekly0.5https://wasp-lang.dev/docs/quick-startweekly0.5https://wasp-lang.dev/docs/telemetryweekly0.5https://wasp-lang.dev/docs/tutorial/actionsweekly0.5https://wasp-lang.dev/docs/tutorial/authweekly0.5https://wasp-lang.dev/docs/tutorial/createweekly0.5https://wasp-lang.dev/docs/tutorial/entitiesweekly0.5https://wasp-lang.dev/docs/tutorial/pagesweekly0.5https://wasp-lang.dev/docs/tutorial/project-structureweekly0.5https://wasp-lang.dev/docs/tutorial/queriesweekly0.5https://wasp-lang.dev/docs/tutorials/dev-excuses-appweekly0.5https://wasp-lang.dev/docs/tutorials/dev-excuses-app/01-creating-the-projectweekly0.5https://wasp-lang.dev/docs/tutorials/dev-excuses-app/02-modifying-main-wasp-fileweekly0.5https://wasp-lang.dev/docs/tutorials/dev-excuses-app/03-adding-operationsweekly0.5https://wasp-lang.dev/docs/tutorials/dev-excuses-app/04-updating-main-page-js-fileweekly0.5https://wasp-lang.dev/docs/tutorials/dev-excuses-app/05-perform-migration-and-runweekly0.5https://wasp-lang.dev/docs/typescriptweekly0.5https://wasp-lang.dev/docs/visionweekly0.5https://wasp-lang.dev/weekly0.5 \ No newline at end of file +https://wasp-lang.dev/blogweekly0.5https://wasp-lang.dev/blog/2019/09/01/hello-waspweekly0.5https://wasp-lang.dev/blog/2021/02/23/journey-to-ycombinatorweekly0.5https://wasp-lang.dev/blog/2021/03/02/wasp-alphaweekly0.5https://wasp-lang.dev/blog/2021/04/29/discord-bot-introductionweekly0.5https://wasp-lang.dev/blog/2021/09/01/haskell-forall-tutorialweekly0.5https://wasp-lang.dev/blog/2021/11/21/seed-roundweekly0.5https://wasp-lang.dev/blog/2021/11/22/fundraising-learningsweekly0.5https://wasp-lang.dev/blog/2021/12/02/waspelloweekly0.5https://wasp-lang.dev/blog/2021/12/21/shayne-introweekly0.5https://wasp-lang.dev/blog/2022/01/27/waspleauweekly0.5https://wasp-lang.dev/blog/2022/05/31/filip-introweekly0.5https://wasp-lang.dev/blog/2022/06/01/gitpod-hackathon-guideweekly0.5https://wasp-lang.dev/blog/2022/06/15/jobs-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/06/24/ML-code-gen-vs-coding-by-hand-futureweekly0.5https://wasp-lang.dev/blog/2022/08/15/how-to-communicate-why-your-startup-is-worth-joiningweekly0.5https://wasp-lang.dev/blog/2022/08/26/how-and-why-i-got-started-with-haskellweekly0.5https://wasp-lang.dev/blog/2022/09/02/how-to-get-started-with-haskell-in-2022weekly0.5https://wasp-lang.dev/blog/2022/09/05/dev-excuses-app-tutrialweekly0.5https://wasp-lang.dev/blog/2022/09/29/journey-to-1000-gh-starsweekly0.5https://wasp-lang.dev/blog/2022/10/28/farnance-hackathon-winnerweekly0.5https://wasp-lang.dev/blog/2022/11/15/auth-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/16/alpha-testing-program-post-mortemweekly0.5https://wasp-lang.dev/blog/2022/11/16/tailwind-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/17/hacktoberfest-wrap-upweekly0.5https://wasp-lang.dev/blog/2022/11/26/erlis-amicus-usecaseweekly0.5https://wasp-lang.dev/blog/2022/11/26/michael-curry-usecaseweekly0.5https://wasp-lang.dev/blog/2022/11/26/wasp-beta-launch-weekweekly0.5https://wasp-lang.dev/blog/2022/11/28/why-we-chose-prismaweekly0.5https://wasp-lang.dev/blog/2022/11/29/permissions-in-web-appsweekly0.5https://wasp-lang.dev/blog/2022/11/29/typescript-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/11/29/wasp-betaweekly0.5https://wasp-lang.dev/blog/2022/11/30/optimistic-update-feature-announcementweekly0.5https://wasp-lang.dev/blog/2022/12/01/beta-ide-improvementsweekly0.5https://wasp-lang.dev/blog/2022/12/08/fast-fullstack-chatgptweekly0.5https://wasp-lang.dev/blog/2023/01/11/betathon-reviewweekly0.5https://wasp-lang.dev/blog/2023/01/18/wasp-beta-update-decweekly0.5https://wasp-lang.dev/blog/2023/01/31/wasp-beta-launch-reviewweekly0.5https://wasp-lang.dev/blog/2023/02/02/no-best-frameworkweekly0.5https://wasp-lang.dev/blog/2023/02/14/amicus-indiehacker-interviewweekly0.5https://wasp-lang.dev/blog/2023/02/21/junior-developer-misconceptionsweekly0.5https://wasp-lang.dev/blog/2023/03/02/wasp-beta-update-febweekly0.5https://wasp-lang.dev/blog/2023/03/03/ten-hard-truths-junior-developers-need-to-hearweekly0.5https://wasp-lang.dev/blog/2023/03/08/building-a-full-stack-app-supabase-vs-waspweekly0.5https://wasp-lang.dev/blog/2023/03/17/new-react-docs-pretend-spas-dont-existweekly0.5https://wasp-lang.dev/blog/2023/04/11/wasp-launch-week-twoweekly0.5https://wasp-lang.dev/blog/2023/04/12/auth-uiweekly0.5https://wasp-lang.dev/blog/2023/04/13/db-start-and-seedweekly0.5https://wasp-lang.dev/blog/2023/04/17/How-I-Built-CoverLetterGPTweekly0.5https://wasp-lang.dev/blog/2023/04/27/wasp-hackathon-twoweekly0.5https://wasp-lang.dev/blog/2023/05/19/hackathon-2-reviewweekly0.5https://wasp-lang.dev/blog/2023/06/07/wasp-beta-update-may-23weekly0.5https://wasp-lang.dev/blog/2023/06/22/wasp-launch-week-threeweekly0.5https://wasp-lang.dev/blog/2023/06/27/build-your-own-twitter-agent-langchainweekly0.5https://wasp-lang.dev/blog/2023/06/28/what-can-you-build-with-waspweekly0.5https://wasp-lang.dev/blog/2023/06/29/new-wasp-lspweekly0.5https://wasp-lang.dev/blog/2023/06/30/tutorial-jamweekly0.5https://wasp-lang.dev/blog/2023/07/10/gpt-web-app-generatorweekly0.5https://wasp-lang.dev/blog/2023/07/17/how-we-built-gpt-web-app-generatorweekly0.5https://wasp-lang.dev/blog/2023/08/01/smol-ai-vs-wasp-aiweekly0.5https://wasp-lang.dev/blog/2023/08/09/build-real-time-voting-app-websockets-react-typescriptweekly0.5https://wasp-lang.dev/blog/2023/08/23/using-product-requirement-documents-generate-better-web-apps-with-aiweekly0.5https://wasp-lang.dev/blog/2023/09/17/ai-meme-generator-how-to-use-openai-function-callweekly0.5https://wasp-lang.dev/blog/2023/10/04/contributing-open-source-land-a-jobweekly0.5https://wasp-lang.dev/blog/2023/10/12/on-importance-of-naming-in-programmingweekly0.5https://wasp-lang.dev/blog/2023/10/13/wasp-launch-week-fourweekly0.5https://wasp-lang.dev/blog/archiveweekly0.5https://wasp-lang.dev/blog/tagsweekly0.5https://wasp-lang.dev/blog/tags/agentweekly0.5https://wasp-lang.dev/blog/tags/aiweekly0.5https://wasp-lang.dev/blog/tags/authweekly0.5https://wasp-lang.dev/blog/tags/careerweekly0.5https://wasp-lang.dev/blog/tags/chakraweekly0.5https://wasp-lang.dev/blog/tags/chatgptweekly0.5https://wasp-lang.dev/blog/tags/clean-codeweekly0.5https://wasp-lang.dev/blog/tags/cssweekly0.5https://wasp-lang.dev/blog/tags/databaseweekly0.5https://wasp-lang.dev/blog/tags/discordweekly0.5https://wasp-lang.dev/blog/tags/expressweekly0.5https://wasp-lang.dev/blog/tags/featureweekly0.5https://wasp-lang.dev/blog/tags/frameworkweekly0.5https://wasp-lang.dev/blog/tags/full-stackweekly0.5https://wasp-lang.dev/blog/tags/fullstackweekly0.5https://wasp-lang.dev/blog/tags/function-callingweekly0.5https://wasp-lang.dev/blog/tags/generateweekly0.5https://wasp-lang.dev/blog/tags/githubweekly0.5https://wasp-lang.dev/blog/tags/gitpodweekly0.5https://wasp-lang.dev/blog/tags/gptweekly0.5https://wasp-lang.dev/blog/tags/hackweekly0.5https://wasp-lang.dev/blog/tags/hackathonweekly0.5https://wasp-lang.dev/blog/tags/hacktoberfestweekly0.5https://wasp-lang.dev/blog/tags/haskellweekly0.5https://wasp-lang.dev/blog/tags/hiringweekly0.5https://wasp-lang.dev/blog/tags/indie-hackerweekly0.5https://wasp-lang.dev/blog/tags/interviewweekly0.5https://wasp-lang.dev/blog/tags/javascriptweekly0.5https://wasp-lang.dev/blog/tags/jobsweekly0.5https://wasp-lang.dev/blog/tags/junior-developersweekly0.5https://wasp-lang.dev/blog/tags/langchainweekly0.5https://wasp-lang.dev/blog/tags/languageweekly0.5https://wasp-lang.dev/blog/tags/launch-weekweekly0.5https://wasp-lang.dev/blog/tags/memeweekly0.5https://wasp-lang.dev/blog/tags/mlweekly0.5https://wasp-lang.dev/blog/tags/new-hireweekly0.5https://wasp-lang.dev/blog/tags/nodeweekly0.5https://wasp-lang.dev/blog/tags/nodejsweekly0.5https://wasp-lang.dev/blog/tags/open-sourceweekly0.5https://wasp-lang.dev/blog/tags/openaiweekly0.5https://wasp-lang.dev/blog/tags/optimisticweekly0.5https://wasp-lang.dev/blog/tags/pernweekly0.5https://wasp-lang.dev/blog/tags/prdweekly0.5https://wasp-lang.dev/blog/tags/prismaweekly0.5https://wasp-lang.dev/blog/tags/product-requirementweekly0.5https://wasp-lang.dev/blog/tags/product-updateweekly0.5https://wasp-lang.dev/blog/tags/programmingweekly0.5https://wasp-lang.dev/blog/tags/reactweekly0.5https://wasp-lang.dev/blog/tags/real-timeweekly0.5https://wasp-lang.dev/blog/tags/redditweekly0.5https://wasp-lang.dev/blog/tags/saa-sweekly0.5https://wasp-lang.dev/blog/tags/saasweekly0.5https://wasp-lang.dev/blog/tags/showcaseweekly0.5https://wasp-lang.dev/blog/tags/solopreneurweekly0.5https://wasp-lang.dev/blog/tags/startupweekly0.5https://wasp-lang.dev/blog/tags/startupsweekly0.5https://wasp-lang.dev/blog/tags/state-of-jsweekly0.5https://wasp-lang.dev/blog/tags/stripeweekly0.5https://wasp-lang.dev/blog/tags/supabaseweekly0.5https://wasp-lang.dev/blog/tags/tech-careerweekly0.5https://wasp-lang.dev/blog/tags/tutorialweekly0.5https://wasp-lang.dev/blog/tags/typescriptweekly0.5https://wasp-lang.dev/blog/tags/updateweekly0.5https://wasp-lang.dev/blog/tags/updatesweekly0.5https://wasp-lang.dev/blog/tags/waspweekly0.5https://wasp-lang.dev/blog/tags/wasp-aiweekly0.5https://wasp-lang.dev/blog/tags/web-devweekly0.5https://wasp-lang.dev/blog/tags/web-developmentweekly0.5https://wasp-lang.dev/blog/tags/webdevweekly0.5https://wasp-lang.dev/blog/tags/websocketsweekly0.5https://wasp-lang.dev/searchweekly0.5https://wasp-lang.dev/docsweekly0.5https://wasp-lang.dev/docs/advanced/apisweekly0.5https://wasp-lang.dev/docs/advanced/deployment/cliweekly0.5https://wasp-lang.dev/docs/advanced/deployment/manuallyweekly0.5https://wasp-lang.dev/docs/advanced/deployment/overviewweekly0.5https://wasp-lang.dev/docs/advanced/emailweekly0.5https://wasp-lang.dev/docs/advanced/jobsweekly0.5https://wasp-lang.dev/docs/advanced/linksweekly0.5https://wasp-lang.dev/docs/advanced/middleware-configweekly0.5https://wasp-lang.dev/docs/advanced/web-socketsweekly0.5https://wasp-lang.dev/docs/auth/emailweekly0.5https://wasp-lang.dev/docs/auth/overviewweekly0.5https://wasp-lang.dev/docs/auth/social-auth/githubweekly0.5https://wasp-lang.dev/docs/auth/social-auth/googleweekly0.5https://wasp-lang.dev/docs/auth/social-auth/overviewweekly0.5https://wasp-lang.dev/docs/auth/uiweekly0.5https://wasp-lang.dev/docs/auth/username-and-passweekly0.5https://wasp-lang.dev/docs/contactweekly0.5https://wasp-lang.dev/docs/contributingweekly0.5https://wasp-lang.dev/docs/data-model/backendsweekly0.5https://wasp-lang.dev/docs/data-model/crudweekly0.5https://wasp-lang.dev/docs/data-model/entitiesweekly0.5https://wasp-lang.dev/docs/data-model/operations/actionsweekly0.5https://wasp-lang.dev/docs/data-model/operations/overviewweekly0.5https://wasp-lang.dev/docs/data-model/operations/queriesweekly0.5https://wasp-lang.dev/docs/editor-setupweekly0.5https://wasp-lang.dev/docs/examplesweekly0.5https://wasp-lang.dev/docs/general/cliweekly0.5https://wasp-lang.dev/docs/general/languageweekly0.5https://wasp-lang.dev/docs/language/featuresweekly0.5https://wasp-lang.dev/docs/project/client-configweekly0.5https://wasp-lang.dev/docs/project/css-frameworksweekly0.5https://wasp-lang.dev/docs/project/custom-vite-configweekly0.5https://wasp-lang.dev/docs/project/customizing-appweekly0.5https://wasp-lang.dev/docs/project/dependenciesweekly0.5https://wasp-lang.dev/docs/project/env-varsweekly0.5https://wasp-lang.dev/docs/project/server-configweekly0.5https://wasp-lang.dev/docs/project/starter-templatesweekly0.5https://wasp-lang.dev/docs/project/static-assetsweekly0.5https://wasp-lang.dev/docs/project/testingweekly0.5https://wasp-lang.dev/docs/quick-startweekly0.5https://wasp-lang.dev/docs/telemetryweekly0.5https://wasp-lang.dev/docs/tutorial/actionsweekly0.5https://wasp-lang.dev/docs/tutorial/authweekly0.5https://wasp-lang.dev/docs/tutorial/createweekly0.5https://wasp-lang.dev/docs/tutorial/entitiesweekly0.5https://wasp-lang.dev/docs/tutorial/pagesweekly0.5https://wasp-lang.dev/docs/tutorial/project-structureweekly0.5https://wasp-lang.dev/docs/tutorial/queriesweekly0.5https://wasp-lang.dev/docs/typescriptweekly0.5https://wasp-lang.dev/docs/visionweekly0.5https://wasp-lang.dev/weekly0.5 \ No newline at end of file