Generating CMS from Prisma Schema
Tags: prisma, cms, javascript, typescript
Published: 2022-08-05 20:41:00

One of [Prisma](https://www.prisma.io/)'s best features is its TypeScript client generated from Prisma schema. For me, it is a killer feature that differentiates Prisma from other JavaScript ORMs. By using the generated TypeScript client, I don't need to worry about writing custom types no matter how complex my queries are. Even better, Prisma's code generator system is designed to be extensible, so we can make it generate more than just TS client.

Motivated by this flexibility of Prisma generator, I tried to generate a content management system (CMS) from a Prisma schema. This project is still in proof-of-concept stage, but I think it has a potential to be used for building internal admin dashboards.

## Existing Projects
There are already content management systems out there that are built on top of Prisma, so let's take a look at some major ones first:

#### KeystoneJS
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/twvbeh316t01bsc9une7.png)
KeystoneJS is a headless CMS with a built-in ORM. If you design a database schema using Keystone schema API, this tool generates everything from a GraphQL server to admin UIs. It is an amazing productivity booster. It started to support Prisma as a database adapter from 2021 therefore widened the gap between other headless CMS tools and itself.

Although Keystone is a one of the superior frameworks, it is limited in a sense that it cannot be easily attached to exiting projects because Keystone needs to manage the database itself.

#### PalJS
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/neit8pshb255kecv9k6u.png)
PalJS is a GraphQL API + admin UI generation tool built by a solo developer. It covers e2e just like KeystoneJS, but it is much more flexible because PalJS is a Prisma generator! As long as you have a Prisma schema (either `prisma db pull`ed or written yourself), you can attach PalJS to any project.

It is a bit difficult to customize the generated UI because everything is wrapped in components.

## prisma-generator-proto
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7nj6h1k18p5olfr2uo09.png)
So I built a proof-of-concept project called `prisma-generator-proto` to overcome the limitations of existing projects.

#### Making use of Prisma generator interface
Even though some content management systems are standalone services (e.g. Wordpress), others use a CMS for managing data for existing services. Something like Keystone can be good for greenfield projects, but it is challenging to adopt in existing projects. So I wanted `prisma-generator-proto` to be a CMS generated from a Prisma schema for ease of adoption.

#### Providing UI as React hooks
It is definitely convenient to have the entire UI/pages generated like PalJS, most teams need to customize their admin UI as they grow. If you have ever tried to customize components from UI libraries like Material UI, you would know how tricky it is to change behavior of packaged components. So `prisma-generator-proto` only exposes data & operations as React hooks without any styling. Developers can build admin UI by mixing these generated hooks with an UI component library.

#### Prisma generator under the hood
All Prisma generators have a similar mechanism.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0to1ga5pzuy5ptowxnli.png)

#### Prisma schema
It all starts from a file called `schema.prisma`:
```prisma
datasource db {
  url      = env("DATABASE_URL")
  provider = "postgresql"
}

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

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String   @db.VarChar(255)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}
```
As you can see above, there are three main directives in `schema.prisma`:
- `datasource` : Defines how to connect to the database
- `model` : Defines each data model
- `generator` : Configures a generator that consumes this schema file. By default, we generate TypeScript client with `prisma-client-js` generator, and we can declare additional generators using `generator` directive again.

`schema.prisma` file can either be hand-written or be pulled from existing database with `prisma db pull` CLI.

#### prisma generate
`prisma generate` command sequentially runs each generators declared in `schema.prisma`. Prisma takes care of parsing the schema, and each generator receives a parsed result called "DMMF". Each DMMF contains the following information as JS objects:

- A list of all models
- A list of fields within each model
- Type, default value, nullability of each field
- Model associations

Also, Prisma lets each generator where to output the generated files, so a generator's job is just to create some files from DMMF and writing them in the designated output directory.

#### Progress
Here's what `prisma-generator-proto` can do now:
- REST API generation

It generates an Express router containing CRUD routes for all models. It does not generate the Express application itself.

Usage:
```ts
import express from "express";
import { router } from "../__generated__";

const app = express();
app.use(express.json());

// Use the generated CRUD routes!
app.use("/api", router);

app.listen(3001, () => {
  console.log("Listening...");
});
```

- Client code generation

It generates React hooks that calls the generated REST API routes.

Usage:
```tsx
import Form from "../components/Form";
import React from "react";
import Table from "../components/Table";
import styles from "../styles/DataScreen.module.css";
import { useCountry } from "../__generated__/hooks";
const Countries = () => {
  // Do CRUD operations with the generated hooks
  const [countries, addHandler, formState, setFormState] = useCountry();
  return (
    <div className={styles.screen}>
      <Form
        name="Country"
        keys={["name", "population", "continentId", "isUnMember"]}
        onSubmit={addHandler}
        state={formState}
        setState={setFormState}
      />
      <Table
        name="Country"
        list={countries}
        keys={["id", "name", "population", "continentId", "isUnMember"]}
      />
    </div>
  );
};
export default Countries;
```

#### Try it yourself
[prisma-generator-proto](https://www.npmjs.com/package/@prisma-korea/prisma-generator-proto) is available on NPM.

You can find the source code in this [GitHub repository](https://github.com/prisma-korea/prisma-generator-proto).

#### Read more & dig deeper
Although what I made is a simple CMS prototype, there are all sorts of crazy generators out there. Check this list out if you want to see more [community generators](https://www.prisma.io/docs/concepts/components/prisma-schema/generators#community-generators)!