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
and select BlitzJS
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 openpackage.json
file and modify the version of blitz to be0.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 specifyingengine
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 toblitz.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
Let’s add tailwind to the project by leveraging the power of recipes
in Blitz JS
blitz install tailwind
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
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
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
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 mutations
folder.
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.
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
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
blitz new google-keep-blitz
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
As any thing has a few tradeoffs Blitz JS has a few too, you can look about them in detail over here.
This is a very minimal but fully functional notes application that you have built and deployed in no time.
Thanks
Rohith Gilla