Develop and Deploy a Blitz JS Application.

Develop and Deploy a Blitz JS Application.

Introduction

Hello everyone, today we will be developing and deploying a notes app using Blitz JS framework.

We will be using the following tools, resources and frameworks on a higher level

  • Blitz JS
  • Tailwind
  • Railway

What is Blitz JS?

Blitz JS is an amazing JavaScript framework that gives you a full stack web application, instead of just a front end or a back end. It is amazingly well type safe, it added the amazing features to the Next JS like authentication, middleware , direct database access and much more.

Advantages that Blitz JS provides out of the box

  • Full stack instead of front end.
  • Data Layer
  • Built In Authentication System
  • Code Scaffolding
  • Recipes
  • New App Development
  • Route Manifest and can use pages folder in which ever folder needed.

Miscellaneous

To reduce the redundant code written every time we use either Code generation or Code Scaffolding techniques.

Code generation is more useful but more restrictive as well, you kinda don’t own your code. But code scaffolding won’t be as useful but you have full ownership of the code you write and generate.

Blitz JS uses Code scaffolding.

Blitz JS has a powerful feature called recipes

Recipes uses Blitz JS Scaffolding to give us many powerful scaffolds.

For example you could install tailwind or chakra-ui or material-ui and many more in just a like click.

Example of installing tailwind in your project with recipes

blitz install tailwind

You could find list of all possible recipes over here, you can create your own recipe too.

blitz/recipes at canary · blitz-js/blitz

Development

Installing Blitz JS

Currently its better to use node version 14.5.0 for many reasons, the main one being the prisma package.

You could use package managers like nvm or fvm to manage node versions locally.

You could install blitz globally using yarn global add blitz command.

You could use your preferred package managers like yarn or pnpm or npm.

Setting up Blitz JS using Railway

We could setup the project in different ways like generating the project through cli, using a starter kit.

We would use railway starter kit so that it helps us reduce the setup time.

This would allocate a postgres db for us and creates required environment variables and saves them in railway and deploys on it.

We can reduce a huge amount of time using this step.

You can head over to

Railway Starters

and select BlitzJS

Railway Starter image

Then enter the application name, choose repository access private or not and then generate and put in a secret.

If you don't know any secret, click CMD + K , then type Generate Secret and paste it in the box.

Wait for it to create and deploy, once it's done you can proceed to the next steps.

Local Setup

You can clone the repository that railway created for us by simply cloning the github repository.

git clone <url>

Very Important steps we need to make before proceeding

  • The railway starter has an old version of blitz cli which doesn’t support many latest and greatest features so please open package.json file and modify the version of blitz to be 0.38.5 (this is latest as of date of writing this article)
  • Railway uses node version 16.5 by default so we have to override it by specifying engine node version.
"engines": {
    "node": "14.15.0"
}
  • In types.ts file make the following change to the import statement.
import { DefaultCtx, SessionContext, SimpleRolesIsAuthorized } from "blitz";
  • In the _app.tsx file Remove the
import { queryCache } from "react-query";

from the imports and add useQueryErrorResetBoundary to existing blitz imports.

import {
  AppProps,
  ErrorComponent,
  useRouter,
  AuthenticationError,
  AuthorizationError,
  ErrorFallbackProps,
  useQueryErrorResetBoundary,
} from "blitz";

Modify the ErrorBoundry to be the following

<ErrorBoundary
      FallbackComponent={RootErrorFallback}
      resetKeys={[router.asPath]}
      onReset={useQueryErrorResetBoundary().reset}
    >
  • Finally modify the blitz.config.js file to blitz.config.ts file and modify it like this
import { BlitzConfig, sessionMiddleware, simpleRolesIsAuthorized } from "blitz";

const config: BlitzConfig = {
  middleware: [
    sessionMiddleware({
      isAuthorized: simpleRolesIsAuthorized,
      cookiePrefix: "google-keep-clone-blitz", // Use your app name here
    }),
  ],
};

module.exports = config;

_Note: I have already made a PR to the railway starters repo, once it gets merged these changes will automatically be reflected_

Once cloned and made the required changes mentioned above you could run install with your preferred package manager.

pnpm install # If you use pnpm

yarn install #If you use yarn

npm install #If you use npm

The output response would look something similar to this

pnpm install pnpm dev

Warp Terminal Sample Output

Let’s add tailwind to the project by leveraging the power of recipes in Blitz JS

blitz install tailwind

Warp Terminal Sample Output

You need to install railway locally as the .env variables we have are from there. So to use the production environment locally we simply need to append railway run before the command to use that environment.

You could check the other commands which are available by running railway help

The output would look something similar to this.

railway help

Warp Terminal Sample Output

You could modify the dev script in package.json file as below so that it will be easy during development.

"dev": "railway run blitz dev"

Database updation

Let’s update the schema.prisma to add Note model and add relations to the User model.

model Note{
  id             Int      @id @default(autoincrement())
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt
  text           String
  userId         Int
  user           User    @relation(fields: [userId], references: [id])
}

Now let’s push the new schema and update our postgres database.

railway run blitz prisma db push --preview-feature

Warp Terminal Sample Output

The output would look something similar to this.

Basic UI

Create a new file at pages/notes.tsx and add the following snippet.

import { BlitzPage } from "blitz";
import React from "react";

const NotesPage: BlitzPage = () => {
  return (
    <>
      <div> Create a note goes here </div>
      <div> My notes go here </div>
    </>
  );
};

export default NotesPage;

Now if you visit [localhost:3000/notes](http://localhost:3000/notes) you would see this page rendered right up!

Queries and Mutations

Now the authentication part, oh Blitz Js provides the entire authentication for us including the error handling, providing hooks to use across the application and many more, we can just skip this step.

In your home page you can create an account using sign up and once you return back to home page you can see your userId. Isn’t that so cool.

Blitz provides an amazing console/playground to run/debug your db queries.

railway run blitz c

Warp Terminal Sample Output

Any file inside the queries folder has magical powers that will help generate the query and many more for us. This queries can be consumed by using useQuery hook.

Similar goes for mutation anything inside mutationsfolder.

ctx.session.$authorize() this is a special line which makes sure the user is logged in, if not redirect to the login page.

It is very very handy and useful utility. Btw if you don't use the line there would be an error in the userId: ctx.session.userId line because userId can also be undefined if the user is not logged in. So this is an example to show how much type safe blitz is

Mutation to create a note

import { Ctx } from "blitz";
import db from "db";

export default async function createNote(
  text: string,
  ctx: Ctx,
  title?: string
) {
  ctx.session.$authorize();

  await db.note.create({
    data: {
      text,
      userId: ctx.session.userId,
    },
  });
}

Query to get all the notes

import { Ctx } from "blitz";
import db from "db";

export async function getUserNotes(ctx: Ctx) {
  ctx.session.$authorize();
  return await db.note.findMany({ where: { userId: ctx.session.userId } });
}

Mutation to delete a note

import { Ctx } from "blitz";
import db from "db";

export default async function deleteNote(id: number, ctx: Ctx) {
  ctx.session.$authorize();

  await db.note.delete({
    where: {
      id,
    },
  });
}

UI

Add Notes

import { BlitzPage, useQuery, invoke } from "blitz";
import React, { Suspense, useState } from "react";

const NoteMain = () => {
  const [text, setText] = useState("");
  return (
    <div className="flex flex-row">
      <div className="w-screen">
        <input
          type="text"
          onChange={(val) => {
            setText(val.target.value);
          }}
          placeholder="Enter note"
          className="form-input px-4 py-3 w-4/5 rounded-full"
        ></input>
      </div>
      <button
        className="w-1/5 p-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full"
        onClick={async () => {
          await invoke(createNote, text);
        }}
      >
        Add
      </button>
    </div>
  );
};

The above component is used to create add note feature to our notes app.

const [text, setText] = useState("") this is the standard useState to save the text before sending it to the server.

await invoke(createNote, text) this will pass text to createNote mutation and run it.

How cool is that!

Display Notes

const NotesDisplay = () => {
  const [notes] = useQuery(getUserNotes, undefined);
  return (
    <div className="flex flex-wrap flex-row">
      {notes.map((note) => (
        <div
          className="flex flex-col justify-around flex-space-between w-1/5 h-32 border-2 border-blue-200 rounded m-2 p-2"
          key={note.id}
        >
          <p className="text-gray-700 text-base">{note.text}</p>
        </div>
      ))}
    </div>
  );
};

This uses the useQuery hook to run the query and save the result in the notes variable.

Once we get the notes we iterate through the array and use some fancy tailwind css styles to display the note.

Delete Note

This is button use to delete the note and this uses same invoke function to run the mutation.

<button
  className="float-right"
  onClick={async () => {
    await invoke(deleteNote, note.id);
  }}
>
  <svg
    xmlns="http://www.w3.org/2000/svg"
    className="h-6 w-6 text-red-500"
    fill="none"
    viewBox="0 0 24 24"
    stroke="currentColor"
  >
    <path
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth={2}
      d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
    />
  </svg>
</button>

This delete button paired with notes display would look like this.

const NotesDisplay = () => {
  const [notes] = useQuery(getUserNotes, undefined);
  return (
    <div className="flex flex-wrap flex-row">
      {notes.map((note) => (
        <div
          className="flex flex-col justify-around flex-space-between w-1/5 h-32 border-2 border-blue-200 rounded m-2 p-2"
          key={note.id}
        >
          <p className="text-gray-700 text-base">{note.text}</p>
          <button
            className="float-right"
            onClick={async () => {
              await invoke(deleteNote, note.id);
            }}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              className="h-6 w-6 text-red-500"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
              />
            </svg>
          </button>
        </div>
      ))}
    </div>
  );
};

Now putting everything in our notes.tsx file that we have created we should be seeing something like this.

import createNote from "app/mutations/createNote";
import deleteNote from "app/mutations/deleteNote";
import getUserNotes from "app/queries/getUserNotes";
import { BlitzPage, useQuery, invoke } from "blitz";
import React, { Suspense, useState } from "react";

const NoteMain = () => {
  const [text, setText] = useState("");
  return (
    <div className="flex flex-row">
      <div className="w-screen">
        <input
          type="text"
          onChange={(val) => {
            setText(val.target.value);
          }}
          placeholder="Enter note"
          className="form-input px-4 py-3 w-4/5 rounded-full"
        ></input>
      </div>
      <button
        className="w-1/5 p-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full"
        onClick={async () => {
          await invoke(createNote, text);
        }}
      >
        Add
      </button>
    </div>
  );
};

const NotesDisplay = () => {
  const [notes] = useQuery(getUserNotes, undefined);
  return (
    <div className="flex flex-wrap flex-row">
      {notes.map((note) => (
        <div
          className="flex flex-col justify-around flex-space-between w-1/5 h-32 border-2 border-blue-200 rounded m-2 p-2"
          key={note.id}
        >
          <p className="text-gray-700 text-base">{note.text}</p>
          <button
            className="float-right"
            onClick={async () => {
              await invoke(deleteNote, note.id);
            }}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              className="h-6 w-6 text-red-500"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
              />
            </svg>
          </button>
        </div>
      ))}
    </div>
  );
};

const NotesPage: BlitzPage = () => {
  return (
    <div className="container m-8 p-8 h-screen w-screen">
      <Suspense fallback={<div>Loading ....</div>}>
        <NoteMain />
        <NotesDisplay />
      </Suspense>
    </div>
  );
};

export default NotesPage;

We use Suspense to show the loading UI and fallback when the queries are yet to be fetched.

We could have split them into multiple Suspense too but this is good for a starter project.

Final Screenshot

Deployment

Since we have used railway starter to setup the project we could just deploy by either running railway up or just push the changes to main branch.

You could look at the response here

Warp Terminal Sample Output

Further reading

You can follow either one of the guides if you have generated blitz application using the blitz cli.

Example of using blitz cli to generate the project

Warp Terminal Sample Output

blitz new google-keep-blitz

Warp Terminal Sample Output

You can use either one of the methods described based on your preferred choice.

Deploy to a Server on Render.com - Blitz.js

Deploy Serverless to Vercel - Blitz.js

Deploy to a Server on Heroku - Blitz.js

Deploy to a Server on Railway - Blitz.js

Resources

You can find the complete code base here

GitHub - Rohithgilla12/google-keep-clone-blitz

For diving deeper into Blitz JS, the link for the documentation is below

Get Started with Blitz

As any thing has a few tradeoffs Blitz JS has a few too, you can look about them in detail over here.

Tradeoffs - Blitz.js

This is a very minimal but fully functional notes application that you have built and deployed in no time.

Thanks

Rohith Gilla