A fullstack framework for the serverless era

The current serverless trend involves practices that I don't think are useful for 95% of the apps that get built out there

Gabriel Chertok avatar
Sep 22, 2020
MonolithPhoto by Johannes Krupinski on Unsplash
E

arlier this year, in a previous article, I push back on going serverless. Not because I think serverless is bad, but because the current serverless trend involves some practices that I don't think are useful for 95% of the apps that get built out there.

If you want to detour here's the previous article I'll wait here drinking my šŸ§‰.

Welcome back šŸ‘‹ As I was saying, I still think the same way. I still feel we need full stack frameworks instead of 15 specialized tools that you can consume from a front-end, and I understand where this pressure of using the right tool for the job comes from, but sometimes a hammer is good enough.

Hopefully, today I can marry these two worlds. Benefiting from serverless infrastructure while developing with a full-stack framework, as if you were writing Django or Ruby on Rails. Let's explore Blitz.js.

Enter Blitz.js

Blitz.js is a full-stack framework adapted for the serverless era. It carries all the benefits of serverless ready frameworks like Next.js -it's built on top of it- while adopting features like a data layer or a set of reasonable defaults.

Blitz.js is built on top of Next.js supporting most, if not all, Next.js features such as React for the view layer, Server Side Rendering (SSR), Static Site Generation (SSG), and the new Incremental Site Generation (ISG), but I feel the exciting parts are in the differences.

Serverless era?

Currently, full-stack frameworks can't run on platforms like AWS lambda or Vercel. These platforms can support different languages like ruby, Java, or PHP, but the full-stack frameworks' programming model doesn't play nicely with the constraints FaaS exposes.

Blitz.js embraces the FaaS constraints. You have no controllers, but stateless functions that can be executed as a long-running nodejs process or invoked as a lambda function.

Typescript

By default, Blitz.js wants you to use Typescript: you can opt-out, but I wouldn't recommend it. TypeScript is a solid language, and the framework generators and all the internals are written in this language.

Code organization

While Next.js doesn't hold too many opinions, maybe non outside how to do routing, Blitz.js does.

First, it encourages you to group files by functionality and not by role. If you have worked with a full-stack framework before, you know a big part of the framework's responsibility is to make these decisions for you.

ā”œā”€ā”€ app
ā”‚Ā Ā  ā”œā”€ā”€ components
ā”‚Ā Ā  ā”œā”€ā”€ layouts
ā”‚Ā Ā  ā”œā”€ā”€ pages
ā”‚Ā Ā  ā”‚Ā Ā  ā”œā”€ā”€ _app.tsx
ā”‚Ā Ā  ā”‚Ā Ā  ā”œā”€ā”€ _document.tsx
ā”‚Ā Ā  ā”‚Ā Ā  ā””ā”€ā”€ index.tsx
ā”‚Ā Ā  ā”œā”€ā”€ products
ā”‚Ā Ā  ā”‚Ā Ā  ā”œā”€ā”€ components
ā”‚Ā Ā  ā”‚Ā Ā  ā”‚Ā Ā  ā””ā”€ā”€ ProductForm.tsx
ā”‚Ā Ā  ā”‚Ā Ā  ā”œā”€ā”€ mutations
ā”‚Ā Ā  ā”‚Ā Ā  ā”‚Ā Ā  ā”œā”€ā”€ createProduct.ts
ā”‚Ā Ā  ā”‚Ā Ā  ā”‚Ā Ā  ā”œā”€ā”€ deleteProduct.ts
ā”‚Ā Ā  ā”‚Ā Ā  ā”‚Ā Ā  ā””ā”€ā”€ updateProduct.ts
ā”‚Ā Ā  ā”‚Ā Ā  ā”œā”€ā”€ pages
ā”‚Ā Ā  ā”‚Ā Ā  ā”‚Ā Ā  ā””ā”€ā”€ products
ā”‚Ā Ā  ā”‚Ā Ā  ā””ā”€ā”€ queries
ā”‚Ā Ā  ā”‚Ā Ā      ā”œā”€ā”€ getProduct.ts
ā”‚Ā Ā  ā”‚Ā Ā      ā””ā”€ā”€ getProducts.ts
ā”‚Ā Ā  ā””ā”€ā”€ queries
ā”‚Ā Ā      ā””ā”€ā”€ getReferer.ts
...

Routes

Here you see how products and app have both a pages directory. At runtime, all these routes are smashed together.

Queries & mutations

Besides pages, we see other types of files, such as queries and mutations. Let's explain those.

Queries and mutations are what you would expect, a way to query and store data from/to your database. While it's not restricted to the DB layer, it's probably the most common scenario.

Blitz.js uses Prisma 2, a framework to abstract the interactions with the database, and it's used like this:

import db from "db"

type GetCompaniesInput = {
  where?: FindManyCompanyArgs["where"]
}

export default async function getCompanies(
  { orderBy = { createdAt: "asc" } }: GetCompaniesInput,
  _ = {}
) {
  const companies = await db.company.findMany({
    orderBy,
  })
  return companies
}

Queries -and mutations- are not API endpoints, but regular TS functions that you can import from your components and call. This is a novel concept I haven't seen in any other frameworks, called Zero-API.

The idea behind the Zero-API is to allow you to call a function from a React component, while swapping that call at compile time for an API request. This results in a simpler programming model. Importing and calling vs. dealing with endpoints, with the added benefit of TS type checking inputs and results. The framework makes the heavy lift for us at build time.

export const Companies = () => {
  const [companies] = useQuery(getCompanies, {})
  return (
    <>
      <h1 className="font-bold text-4xl mb-8">Companies</h1>
      {companies.map((company) => {
        return <Company key={company.id} {...company} />
      })}
    </>
  )
}

Queries are called from the front-end with a useQuery hook. For mutations, no hook is needed you can just await the mutation response. Also, types are carried over from the hook to your variables.

Prisma 2

We talked about Prisma 2 when discussing queries and mutations, but it deserves a bit more explanation. At its core, Prisma is a set of packages that allows you to interact with relational databases using node or TypeScript.

If you choose TypeScript as Blitz does this, you get complete type safety for your models and DB operations, since Prisma will generate not only model types but types for querying and mutating the resource.

Alt Text

The way Prisma works is by having a schema file with a custom DSL. This schema is similar to the one you can find in Rails, but instead of being the result of applying migrations it operates as the source of truth, and migrations are generated from this file.

datasource db {
  provider = ["sqlite", "postgres"]
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

// --------------------------------------

model Company {
  id               Int      @default(autoincrement()) @id
  createdAt        DateTime @default(now())
  updatedAt        DateTime @updatedAt
  name             String
  description      String
  logo             String
  url              String   @default("")
  hasOffices       Boolean
  allowsFullRemote Boolean
}

After you run the blitz db migrate command, Prisma will generate a migration -a snapshot of the actual schema- and a Prisma client. A Prisma client is the package we use to interact with the DB and has the generated types for our schema.

CLI

Most of the things I talked about here can be created though the Blitz CLI. Currently, it has almost everything you need to start working with the framework such as blitz new {PROJECT NAME} or blitz generate to generate models, scaffolds pages and more, as well as the blitz db command to interact with Prisma using the same CLI.

Final words

There are many more things I wish I had covered in this review, such as the new upcoming seed command, the built-in authentication or the recipes.

I will be writing more about Blitz since I'm using it to rebuild remote.uy, so hopefully, I can cover more ground and learn since I'm not an expert on the subject, and the framework is rapidly evolving.

If you liked the framework, give it a try, and join the Slack community where most of the action takes place.