Menu background

Blitz.js + react-admin = the ultimate MVP back office kit

Dec 8th·6 min read

Building an MVP can be a real challenge. It requires a new mindset (the MVP mindset) both from the business and the development side. It’s mandatory to keep focus; the development cannot get lost in the details. Luckily today, many tools enable fast iteration and out-of-the-box solutions for many of the problems an MVP needs to solve.

In our case, we needed a classic admin dashboard interface to handle CRUD operations as part of our app. We wanted to solve this with the lowest effort possible, so we tried several low-code tools (e.g., retool, appsmith), but after short experimentations, we concluded:

  • these tools have a learning curve as well, and it would be better to utilize our coding skills 👨‍💻
  • although it’s possible to build UI quickly with these tools, you need an API before that 🙄
  • and there are limitations when it comes to entities with complex relationships, parallel development or adding your own custom UI elements ⛔

So we continued searching and found two incredible technologies Blitz.js and React Admin, which make a perfect combination to realize fast iteration over essential business issues and save time on the otherwise painful process of creating your CRUD operations and interfaces.

TL;DR

We created a new React Admin data provider for Blitz.js, enabling super-fast workflow for creating CRUD interfaces.

Here is the repo

Demo app

We recommend reading forward if you’re not familiar with these frameworks or interested in the development details. 👇

First, let’s take a quick look at Blitz.js:

🛠 Blitz.js

Blitz advertises itself as a “batteries-included framework inspired by Ruby on Rails and built on Next.js.”

Batteries-included meaning, it:

  • includes built-in authentication & authorization → saves time ⏱, no need to reinvent the wheel
  • has code scaffolding → pages, queries, mutations, and models can be generated from the Blitz CLI → more time saved, neat ⌛️
  • has a unique “Zero-API” data layer, which lets you import server code directly into your components → see the example, it’s 🤯
  • has full TypeScript support → phenomenal autocomplete & developer experience ⚡️
  • supports Prisma as a database client by default → Prisma is the sexiest Node.js ORM right now, worth checking out if you’re not familiar 👉

Wow, that’s a lot of cool stuff! 😎

Let’s see some concrete examples:

✨ Code scaffolding

Code scaffolding can be performed with the blitz generate CLI command.

The first parameter is the type of file that we’d like to generate. We will be using the crud option, which produces CRUD queries and mutations. For all the types, see the documentation.

The second param is the name of the model.

For example, $ blitz generate crud posts will generate the following files:

app/posts/queries/getPost.ts
app/posts/queries/getPosts.ts
app/posts/mutations/createPost.ts
app/posts/mutations/deletePost.ts
app/posts/mutations/updatePost.ts

Not only the files are created, but a basic implementation is also provided, so it can be used right away: 🚀

import { paginate, resolver } from 'blitz'
import db, { Prisma } from 'db'

interface GetPostsInput
  extends Pick<
    Prisma.PostFindManyArgs,
    'where' | 'orderBy' | 'skip' | 'take'
  > {}

export default resolver.pipe(
  resolver.authorize(),
  async ({ where, orderBy, skip = 0, take = 100 }: GetPostsInput) => {
    const {
      items: posts,
      hasMore,
      nextPage,
      count,
    } = await paginate({
      skip,
      take,
      count: () => db.post.count({ where }),
      query: (paginateArgs) =>
        db.post.findMany({ ...paginateArgs, where, orderBy }),
    })

    return {
      posts,
      nextPage,
      hasMore,
      count,
    }
  }
)

✨ “Zero-API” data layer

After the scaffolding, we have queries and mutations, but how can we use them in a React component? 🤔

Blitz utilizes react-query and provides us with handy hooks: useQuery and useMutation.

import { useQuery } from 'blitz'
import getPost from 'app/projects/queries/getPost'

function App() {
  const [post] = useQuery(getPost, { where: { id: 1 } })
}

What kind of sorcery is this? 🧙‍♂️

Importing server-side code in a React component?

It’s working because Blitz swaps the direct function import with a network call at build time. 🪄

The network call is handled by an auto-generated RPC API. You can read more about it here.

Good, hopefully, you got a taste of Blitz at this point. Now let’s jump onto React Admin.

🌐 React Admin

React Admin is a front-end framework based on React and Material UI for building data-driven applications.

What’s awesome about it is that you can set up whole screens with minimal code.

import React from 'react'
import { Admin, Resource, ListGuesser, EditGuesser } from 'react-admin'
import dataProvider from './myDataProvider'

const ReactAdmin = () => {
  return (
    <Admin dataProvider={dataProvider}>
      <Resource name="posts" list={ListGuesser} edit={EditGuesser} />
    </Admin>
  )
}

export default ReactAdmin

You can define your project’s entities as Resources.

React Admin has intelligent Guesser components (like ListGuesser), which can generate whole components based on the API response, saving you from many keystrokes. 💪

Guesser example

You just copy the console output, create your PostList component, and then customize it to your own needs.

The backend can be connected through data providers, which are adapters that tell react-admin how to perform its set of operations for a given resource.

There are several data provider implementations to choose from.

First, we tried to set things up through ra-data-prisma, with a generated GraphQL backend. (using typegraphql-prisma) However, it turned out that this package wasn’t well maintained, and we ran into several compatibility problems. It was a frustrating experience. 😫

That’s when we realized it would be much more efficient to utilize Blitz.js’ internal RPC API layer and the code scaffolding mechanism that we already had. So we decided to create our data-provider package, which turned out to be a lot of fun! 🎉

🔗 Connecting Blitz + React Admin

A data provider needs to implement the following interface:

const dataProvider = () => ({
  getList: (resource, params) => Promise,
  getOne: (resource, params) => Promise,
  getMany: (resource, params) => Promise,
  getManyReference: (resource, params) => Promise,
  create: (resource, params) => Promise,
  update: (resource, params) => Promise,
  updateMany: (resource, params) => Promise,
  delete: (resource, params) => Promise,
  deleteMany: (resource, params) => Promise,
})

Where resource is the name of entities (e.g., “posts”) and params are all the different inputs required to configure the given dataProvider method.

First we thought we need to make RPC API calls in these methods, because React hooks (useQuery and useMutation) can only be used inside components. Fortunately, Blitz exposes an invoke method which can be used for imperatively calling queries or mutations. 🙏

As Blitz enforces a rather strict folder structure, we could build a path from the resource and the specific method, and then dynamically import it.

const getHandlerModule = async ({
  resource,
  method,
  plural,
}: GetHandlerModuleParams) => {
  const entityName = getEntityNameFromResource(resource)
  const folder = method === QueryMethod.Get ? 'queries' : 'mutations'
  return import(
    `app/${resource}/${folder}/${method}${
      plural ? getPluralEntityName(entityName) : entityName
    }`
  )
}

export const getHandler = async ({
  resource,
  method = QueryMethod.Get,
  plural = false,
  invoke,
}: GetHandlerParams) => {
  if (!isPlural(resource)) {
    throw new Error(`Resource '${resource}' MUST be plural!`)
  }

  const handlerModule = await getHandlerModule({ resource, method, plural })

  return async (params: any) => invoke(handlerModule.default, params)
}

The last thing we needed was to map the params to match the handler’s signature, call the correct handler with invoke and pass the mapped params to them.

As an example, here is the getOne method’s implementation:

...
getOne: async (resource, params) => {
  const id = params.id as string;
  const handler = await getHandler({ resource, invoke });
  const data = await handler({ id: parseInt(id) });
  return {
    data,
  };
},
...

Example usage of the package from our demo repo:

import React from 'react'
import { Admin, Resource } from 'react-admin'
import { invoke } from 'blitz'
import blitzDataProvider from '@theapexlab/ra-data-blitz'
import { PostList } from './PostList'
import { UserEdit } from './UserEdit'
import { UserList } from './UserList'
import { PostEdit } from './PostEdit'
import { UserCreate } from './UserCreate'
import { PostCreate } from './PostCreate'

const ReactAdmin = () => {
  return (
    <Admin dataProvider={blitzDataProvider({ invoke })}>
      <Resource
        name="users"
        list={UserList}
        edit={UserEdit}
        create={UserCreate}
      />
      <Resource
        name="posts"
        list={PostList}
        edit={PostEdit}
        create={PostCreate}
      />
    </Admin>
  )
}

export default ReactAdmin

🏁 Summary

Working with these technologies was a fun experience 🦄, despite the initial difficulties.

The unity of Blitz.js and React Admin is truly something else.

We hope that you found our findings as exciting as it was for us to create and could learn something new. 🤓

See you next time! 🖖

Background waves

Are you interested in working with us?

Get in touch

get in touch theme image