Tutorial

Tutorial - 图3caution

Before getting started, know this is beta software. You may encounter bugs or edge cases we haven’t hit yet. If you do, please open an issue! We’re excited to see what you build!

Thanks for trying out Blitz! In this tutorial, we’ll walk you through the creation of a basic voting application.

We’ll assume that you have

Blitz installed already. You can tell if Blitz is installed, and which version you have by running the following command in your terminal:

  1. blitz -v

If Blitz is installed, you should see the version of your installation. If it isn’t, you’ll get an error saying something like “command not found: blitz”.

Creating a project

If this is your first time using Blitz, you’ll have to begin with some initial setup. We provide a command which takes care of all this for you, generating the configuration and code you need to get started.

From the command line,

cd into the directory where you’d like to store your code, and then run the following command:

  1. blitz new mysite

You will be prompted to pick a form library. For this tutorial, select the recommended library

React Final Form. Blitz should now create a mysite directory in your current directory.

Let’s look at what

blitz new created:

  1. mysite├── app ├── components └── ErrorBoundary.tsx ├── layouts └── pages ├── _app.tsx ├── _document.tsx └── index.tsx├── db ├── migrations ├── index.ts └── schema.prisma├── integrations├── node_modules├── public ├── favicon.ico └── logo.png├── utils├── .babelrc.js├── .env├── .eslintrc.js├── .gitignore├── .npmrc├── .prettierignore├── README.md├── blitz.config.js├── package.json├── tsconfig.json└── yarn.lock

These files are:

  • The

    app/ directory is a container for most of your project. This is where you’ll put any pages or API routes.

  • db/ is where your database configuration goes. If you’re writing models or checking migrations, this is where to go.

  • node_modules/ is where your “dependencies” are stored. This directory is updated by your package manager, so you don’t have to worry too much about it.

  • public/ is a directory where you will put any static assets. If you have images, files, or videos which you want to use in your app, this is where to put them.

  • utils/ is a good place to put any shared utility files which you might use across different sections of your app.

  • .babelrc.js, .env, etc. (“dotfiles”) are configuration files for various bits of JavaScript tooling.

  • blitz.config.js is for advanced custom configuration of Blitz. It extends

    next.config.js.

  • package.json contains information about your dependencies and devDependencies. If you’re using a tool like npm or yarn, you won’t have to worry about this much.

  • tsconfig.json is our recommended setup for TypeScript.

The development server

Let’s check that your Blitz project works. Make sure you are in the

mysite directory, if you haven’t already, and run the following command:

  1. blitz start

You’ll see the following output on the command line:

  1. Prepped for launch[ wait ] starting the development server ...[ info ] waiting on http://localhost:3000 ...[ info ] bundled successfully, waiting for typecheck results...[ wait ] compiling ...[ info ] bundled successfully, waiting for typecheck results...[ ready ] compiled successfully - ready on http://localhost:3000

Now that the server’s running, visit

localhost:3000 with your Web browser. You’ll see a welcome page, with the Blitz logo. It worked!

Write your first page

Now that your development environment—a “project”—is set up, you’re ready to start building out the app. First, we’ll create your first page.

Open the file

app/pages/index.tsx and put the following code in it:

  1. const Index = () => ( <div> <h1>Hello, world!</h1> </div>)export default Index

This is the simplest page possible in Blitz. To look at it, go back to your browser and go to http://localhost:3000. You should see your text appear! Try editing the

index.tsx file, and make it your own! When you’re ready, move on to the next section.

Database setup

By default, a Blitz app is created with a local SQLite database. If you’re new to databases, or you’re interested in trying Blitz, this is the easiest choice. Note that when starting your first real project, you may want to use a more scalable database like PostgreSQL, to avoid the pains of switching your database down the road. For more information, see

Database overview. For now, we will continue with the default SQLite database.

Generating content

Blitz provides a handy command called

generate for scaffolding out boilerplate code. We’ll use generate to create two models: Question and Choice. A Question has the text of the question and a list of choices. A Choice has the text of the choice, a vote count, and an associated question. Blitz will automatically generate an id, a creation timestamp, and a last updated timestamp for both models.

First, we’ll generate everything pertaining to the

Question model:

  1. blitz generate all question text:string choices:choice[]

Note: If you are using zsh (the default terminal shell on MacOS), you will need to wrap any fields with arrays with quotation marks (

"") in order for them to be properly interpreted. Like this: "choices:choice[]".

  1. Model for 'question' created successfully:> model Question {> id Int @default(autoincrement()) @id> createdAt DateTime @default(now())> updatedAt DateTime @updatedAt> text String> choices Choice[]> }Now run `blitz prisma migrate dev --preview-feature` to add this model to your databaseCREATE app/questions/pages/questions/index.tsxCREATE app/questions/pages/questions/new.tsxCREATE app/questions/pages/questions/[questionId]/edit.tsxCREATE app/questions/pages/questions/[questionId].tsxCREATE app/questions/components/QuestionForm.tsxCREATE app/questions/queries/getQuestions.tsCREATE app/questions/queries/getQuestion.tsCREATE app/questions/mutations/createQuestion.tsCREATE app/questions/mutations/deleteQuestion.tsCREATE app/questions/mutations/updateQuestion.ts

The

generate command with a type of all generates a model and queries, mutation and page files. See the Blitz generate page for a list of available type options.

Now we’ll generate the

Choice model with corresponding queries and mutations. We’ll pass a type of resource this time as we don’t need to generate pages for the Choice model:

  1. blitz generate resource choice text votes:int:default[0] belongsTo:question

Note: Remember to wrap the array field with quotation marks if using zsh. Like this: “votes:init:default[0]“

  1. Model for 'choice' created successfully:> model Choice {> id Int @default(autoincrement()) @id> createdAt DateTime @default(now())> updatedAt DateTime @updatedAt> text String> votes Int @default(0)> question Question @relation(fields: [questionId], references: [id])> questionId Int> }Now run `blitz prisma migrate dev --preview-feature` to add this model to your databaseCREATE app/choices/queries/getChoices.tsCREATE app/choices/queries/getChoice.tsCREATE app/choices/mutations/createChoice.tsCREATE app/choices/mutations/deleteChoice.tsCREATE app/choices/mutations/updateChoice.ts

Note: you can also edit the

db/schema.prisma file directly if needed, either instead of using generate or afterwards if further modifications are needed.

Now, we need to migrate our database. This is a way of telling it that you have edited your schema in some way. Run the below command. When it asks you to enter a migration name, you can enter anything you want (perhaps “init db”):

  1. blitz prisma migrate dev --preview-feature

Playing with the API

Now, let’s hop into the interactive Blitz shell and play around with the free API Blitz gives you. To invoke the Blitz console, use this command:

  1. blitz console

Once you’re in the console, explore the Database API:

  1. # No questions are in the system yet.⚡ > await db.question.findMany()[]# Create a new Question:⚡ > let q = await db.question.create({data: {text: "What's new?"}})undefined# See the entire object:⚡ > q{ id: 1, createdAt: 2020-06-15T15:06:14.959Z, updatedAt: 2020-06-15T15:06:14.959Z, text: "What's new?"}# Or, access individual values on the object:⚡ > q.text"What's new?"⚡ > q.createdAt2020-06-15T15:06:14.959Z# Change values by using the update function:⚡ > q = await db.question.update({where: {id: 1}, data: {text: "What's up?"}}){ id: 1, createdAt: 2020-06-15T15:06:14.959Z, updatedAt: 2020-06-15T15:13:17.394Z, text: "What's up?"}# db.question.findMany() now displays all the questions in the database:⚡ > await db.question.findMany()[ { id: 1, createdAt: 2020-06-15T15:06:14.959Z, updatedAt: 2020-06-15T15:13:17.394Z, text: "What's up?" }]

Fixing generated content

Tutorial - 图4info

Before running the app again, we need to customize some of the content that has been generated. Ultimately, these fixes will not be needed - but for now, we need to work around a couple outstanding issues.

deleteQuestion mutation

Prisma does not yet support “cascading deletes”. In the context of this tutorial, that means it does not currently delete the

Choice data when deleting a Question. We need to temporarily augment the generated deleteQuestion mutation in order to do this manually. Open up app/questions/mutations/deleteQuestion.ts in your text editor and add the following to the top of the function body:

  1. // TODO: remove once Prisma supports cascading deletesawait db.choice.deleteMany({where: {question: {id: where.id}}})

The end result should be as such:

  1. // app/questions/mutations/deleteQuestion.tsexport default async function deleteQuestion({where}: DeleteQuestionInput, ctx: Ctx) { ctx.session.authorize() // TODO: remove once Prisma supports cascading deletes await db.choice.deleteMany({where: {question: {id: where.id}}}) const question = await db.question.delete({where}) return question}

This mutation will now delete the choices associated with the question prior to deleting the question itself.

Question pages

The generated page content does not currently use the actual model attributes you defined during generation. It will soon, but in the meantime, let’s fix the generated pages.

Jump over to

app/questions/pages/questions/index.tsx. Notice that a QuestionsList component has been generated for you:

  1. export const QuestionsList = () => { const [questions] = useQuery(getQuestions, {orderBy: {id: "desc"}}) return ( <ul> {questions.map((question) => ( <li key={question.id}> <Link href="/questions/[questionId]" as={`/questions/${question.id}`}> <a>{question.name}</a> </Link> </li> ))} </ul> )}

This won’t work though! Remember that the

Question model we created above doesn’t have any name field. To fix this, replace question.name with question.text:

  1. export const QuestionsList = () => { const [questions] = useQuery(getQuestions, {orderBy: {id: "desc"}}) return ( <ul> {questions.map((question) => ( <li key={question.id}> <Link href={`/questions/${question.id}`}> <a>{question.text}</a> </Link> </li> ))} </ul> )}

Next, let’s apply a similar fix to

app/questions/pages/questions/new.tsx. In the form submission, replace

  1. const question = await createQuestionMutation({data: {name: "MyName"}})

with

  1. const question = await createQuestionMutation({ data: {text: "Do you love Blitz?", choices: {create: [{text: "Yes!"}]}},})

Finally, we need to fix the edit page. Open

app/questions/pages/questions/[questionId]/edit.tsx and replace

  1. const updated = await updateQuestionMutation({ where: {id: question.id}, data: {name: "MyNewName"},})

with

  1. const updated = await updateQuestionMutation({ where: {id: question.id}, data: {text: "Do you really love Blitz?"},})

Great! Now make sure your app is running. If it isn’t, run

blitz start in your terminal, and visit http://localhost:3000/questions. Blitz comes with authentication and authorization set up by default. You may need to register an account to access the /questions page. Once logged in, play around with your new app a bit! Try creating questions, editing, and deleting them.

Writing a minimal form

You’re doing great so far! The next thing we’ll do is give our form some real inputs. At the moment it’s giving every

Question the same text! Have a look at app/questions/components/QuestionForm.tsx in your editor.

Delete the div that says:

<div>Put your form fields here. But for now, just click submit</div>, and replace it with some inputs:

  1. <input placeholder="Question" /><input placeholder="Choice 1" /><input placeholder="Choice 2" /><input placeholder="Choice 3" />

Finally, we’re going to make sure all that data is submitted. Open up

app/questions/pages/questions/new.tsx in your editor and replace

  1. onSubmit={async () => { try { const question = await createQuestionMutation({ data: { text: "Do you love Blitz?", choices: { create: [{ text: "Yes!" }] } }, }) alert("Success!" + JSON.stringify(question)); router.push("/questions/[questionId]", `/questions/${question.id}`); } catch (error) { alert("Error creating question " + JSON.stringify(error, null, 2)); }}}

with

  1. onSubmit={async (event) => { try { const question = await createQuestionMutation({ data: { text: event.target[0].value, choices: { create: [ {text: event.target[1].value}, {text: event.target[2].value}, {text: event.target[3].value}, ], }, }, }); alert("Success!" + JSON.stringify(question)); router.push("/questions/[questionId]", `/questions/${question.id}`); } catch (error) { alert("Error creating question " + JSON.stringify(error, null, 2)); }}}

In the end, your page should look something like this:

  1. // app/questions/pages/questions/new.tsximport Layout from "app/layouts/Layout"import {Link, useRouter, useMutation, BlitzPage} from "blitz"import createQuestion from "app/questions/mutations/createQuestion"import QuestionForm from "app/questions/components/QuestionForm"const NewQuestionPage: BlitzPage = () => { const router = useRouter() const [createQuestionMutation] = useMutation(createQuestion) return ( <div> <h1>Create New Question</h1> <QuestionForm initialValues={{}} onSubmit={async (event) => { try { const question = await createQuestionMutation({ data: { text: event.target[0].value, choices: { create: [ {text: event.target[1].value}, {text: event.target[2].value}, {text: event.target[3].value}, ], }, }, }) alert("Success!" + JSON.stringify(question)) router.push("/questions/[questionId]", `/questions/${question.id}`) } catch (error) { alert("Error creating question " + JSON.stringify(error, null, 2)) } }} /> <p> <Link href="/questions"> <a>Questions</a> </Link> </p> </div> )}NewQuestionPage.getLayout = (page) => <Layout title={"Create New Question"}>{page}</Layout>export default NewQuestionPage

Listing choices

Time for a breather. Go back to

http://localhost:3000/questions in your browser and look at all the questions you‘ve created. How about we list these questions’ choices here too? First, we need to customize the question queries. In Prisma, you need to manually let the client know that you want to query for nested relations. Change your getQuestion.ts and getQuestions.ts files to look like this:

  1. // app/questions/queries/getQuestion.tsimport {Ctx, NotFoundError} from "blitz"import db, {Prisma} from "db"type GetQuestionInput = Pick<Prisma.FindFirstQuestionArgs, "where">export default async function getQuestion({where /* include */}: GetQuestionInput, ctx: Ctx) { ctx.session.authorize() const question = await db.question.findFirst({where, include: {choices: true}}) if (!question) throw new NotFoundError() return question}
  1. // app/questions/queries/getQuestions.tsimport { Ctx } from "blitz"import db, { Prisma } from "db"type GetQuestionsInput = Pick<Prisma.FindManyQuestionArgs, "where" | "orderBy" | "skip" | "take">export default async function getQuestions( { where, orderBy, skip = 0, take }: GetQuestionsInput, ctx: Ctx) { ctx.session.authorize() const questions = await db.question.findMany({ where, orderBy, take, skip, include:{ choices:true } }) const count = await db.question.count() const hasMore = typeof take === "number" ? skip + take < count : false const nextPage = hasMore ? { take, skip: skip + take! } : null return { questions, nextPage, hasMore, count, }}

Now hop back to our main questions page (

app/questions/pages/questions/index.tsx)in your editor, and we can list the choices of each question. And add this code beneath the Link in our QuestionsList:

  1. <ul> {question.choices.map((choice) => ( <li key={choice.id}> {choice.text} - {choice.votes} votes </li> ))}</ul>

Magic! Let’s do one more thing–let people vote on these questions!

Open

app/questions/pages/questions/[questionId].tsx in your editor. First, we’re going to improve this page somewhat.

  1. Replace

    <h1>Question {question.id}</h1> with <h1>{question.text}</h1>.

  2. Delete the pre element, and copy in our choices list which we wrote before:

  1. <ul> {question.choices.map((choice) => ( <li key={choice.id}> {choice.text} - {choice.votes} votes </li> ))}</ul>

If you go back to your browser, your page should now look something like this!

Screenshot

Now it’s time to add the vote button. In our

li, add a button like so:

  1. <li key={choice.id}> {choice.text} - {choice.votes} votes <button>Vote</button></li>

Then, import the

updateChoice mutation (generated at the beginning) and create a handleVote function in our page:

  1. import updateChoice from "app/choices/mutations/updateChoice"...const [updateChoiceMutation] = useMutation(updateChoice)const handleVote = async (id, votes) => { try { const updated = await updateChoiceMutation({ where: {id}, data: {votes: votes + 1}, }) refetch() alert("Success!" + JSON.stringify(updated)) } catch (error) { alert("Error creating question " + JSON.stringify(error, null, 2)) }}return (...

Finally, we’ll tell our new

button to call that function!

  1. <button onClick={() => handleVote(choice.id, choice.votes)}>Vote</button>

In order to call the

refetch() function in handleVote, we need to destructure it from the existing useQuery. In summary, this is what the file should now look like:

  1. // app/questions/pages/questions/[questionId].tsximport {Suspense} from "react"import Layout from "app/layouts/Layout"import {Link, useRouter, useQuery, useParam, BlitzPage, useMutation} from "blitz"import getQuestion from "app/questions/queries/getQuestion"import deleteQuestion from "app/questions/mutations/deleteQuestion"import updateChoice from "app/choices/mutations/updateChoice" export const Question = () => { const router = useRouter() const questionId = useParam("questionId", "number") const [question, {refetch}] = useQuery(getQuestion, {where: {id: questionId}}) const [deleteQuestionMutation] = useMutation(deleteQuestion) const [updateChoiceMutation] = useMutation(updateChoice) const handleVote = async (id, votes) => { try { const updated = await updateChoiceMutation({ where: {id}, data: {votes: votes + 1}, }) refetch() alert("Success!" + JSON.stringify(updated)) } catch (error) { alert("Error creating question " + JSON.stringify(error, null, 2)) } } return ( <div> <h1>{question.text}</h1> <ul> {question.choices.map((choice) => ( <li key={choice.id}> {choice.text} - {choice.votes} votes <button onClick={() => handleVote(choice.id, choice.votes)}>Vote</button> </li> ))} </ul> <Link href={`/questions/${question.id}/edit`}> <a>Edit</a> </Link> <button type="button" onClick={async () => { if (window.confirm("This will be deleted")) { await deleteQuestionMutation({where: {id: question.id}}) router.push("/questions") } }} > Delete </button> </div> )}const ShowQuestionPage: BlitzPage = () => { return ( <div> <p> <Link href="/questions"> <a>Questions</a> </Link> </p> <Suspense fallback={<div>Loading...</div>}> <Question /> </Suspense> </div> )}ShowQuestionPage.getLayout = (page) => <Layout title={"Question"}>{page}</Layout>export default ShowQuestionPage

Conclusion

🥳 Congrats! You created your very own Blitz app! Have fun playing around with it, or sharing it with your friends. Now that you’ve finished this tutorial, why not try making your voting app even better? You could try:

  • Adding styling
  • Showing some more statistics about votes
  • Deploying live on Render or Vercel

If you want to share your project with the world wide Blitz community there is no better place to do that than on Discord.

Visit

discord.blitzjs.com. Then, post the link to the #built-with-blitz channel to share it with everyone!