Modern Web Architecture Without a Backend With Prisma + ZenStack
Tags: webdev, fullstack, prisma, database
Published: 2023-02-09 00:55:58

Web development's landscape is ever-changing. We started from bare metal machines serving static HTML pages, to the rise of the LAMP stack, then the MEAN stack, and now the JAM stack.

While at a different level, two trends are going on:

-   **Consolidation of architecture**

    Developers are getting tired of combining many different pieces in an application. They have started to favor less for more: unified programming language, mono-repos, meta frameworks (like Next.js), and even building web apps without explicitly coding a backend.

-   **Renaissance of SQL databases**

    SQL databases probably never really lost any ground to NoSQL; it just became less cool to use for a while from a fashion perspective. Now with more and more people aware that few web apps are indeed "web scale" and relational databases are still the best choice for most use cases, SQL databases are making a comeback.

In this post, I'll share how the combination of [Prisma](https://www.prisma.io/) and [ZenStack](https://zenstack.dev/?utm_campaign=devto&utm_medium=organic&utm_content=prisma_zenstack) correspond to the above two trends and how they can help you build a secure backend with less effort.

## What's Prisma

[Prisma](https://prisma.io/) is an ORM toolkit for Javascript/TypeScript. ORM is a kind of library that lets you access databases without writing SQL. Instead, you write code in the same language as the rest of your app to manipulate the database, and the ORM library will translate it into SQL queries.

![ORM](https://user-images.githubusercontent.com/104139426/217270030-f5acbc6a-345f-4f19-9b8f-23f630f0c152.png)

More specifically, Prisma a schema-first ORM, which means you first define your data model in a schema file, and then Prisma generates the database schema and the corresponding CRUD operations for you. On the contrary, another ORM category is code-first: you define your data model in programming languages like Typescript, and the ORM infers the database schema from it.

Choosing schema-first or code-first is a matter of preference, although a schema-first ORM has the benefit of being more coherent, concise, and easier to read.

![Schema-first vs code-first](https://user-images.githubusercontent.com/104139426/217393709-9d9899ca-cb9f-405b-9c65-75240cd1dacd.png)

## What's ZenStack

[ZenStack](https://zenstack.dev/?utm_campaign=devto&utm_medium=organic&utm_content=prisma_zenstack) is an extension to Prisma that adds a layer of security. It allows you to define access control rules inside your schema file and inject them into Prisma queries at runtime.

![ZModel](https://zenstack.dev/assets/images/banner-code-329856a793ee4a955bc8e37b6f9a4e1c.png)

Furthermore, it provides a RESTful API to access the database and generates client libraries for your frontend code. By combining access control and API generation, ZenStack makes it possible to have a fully secure CRUD backend without manually implementing it.

![ZenStack Architecture](https://zenstack.dev/img/architecture-light.png)

## How Do They Work Together

Prisma and ZenStack make a perfect match for building up a secure backend with a relational database. Here're the general steps for achieving it:

### 1. Define Your Data Model

Use the **ZModel** language (a superset of Prisma schema) to define your models, relations, and access policies. Optionally, if you use Next.js as your full-stack framework, enable the react plugin to generate data access client hooks.

```ts
// schema.zmodel

model Post {
  id        String @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String
  published Boolean @default(false)
  author    User @relation(fields: [authorId], references: [id])
  authorId  String

  // author has full access
  @@allow('all', auth() == author)

  // logged-in users can view published posts
  @@allow('read', auth() != null && published)
}

// generate react hooks under ./src/lib/hook
plugin reactHooks {
  provider = '@zenstackhq/react'
  output = "./src/lib/hook
}

```

### 2. Run Code Generation

Use the `zenstack` CLI to generate several pieces of code:

-   Prisma schema (schema.prisma)
-   Prisma client
-   Access policies for injecting Prisma queries at runtime
-   React hooks for data access

![CLI Animation](https://user-images.githubusercontent.com/104139426/217684008-f4d3228d-dba8-4749-be1e-37a5ad209062.gif)

You can use the generated `schema.prisma` to migrate your database's schema and the Prisma client to access the database without any access control. You'll see how the access policies and React hooks help next.

### 3. Mounting the Data Access APIs

Use a framework-specific package to install a data access API request handler. You'll usually use an "enhanced" Prisma client to initialize the request handler to secure the endpoints. The "enhancement" works by loading the access policies generated in the previous step, intercepting Prisma client's operations, and injecting extra query/mutation conditions.

Here's an example for Next.js:

```ts
// pages/api/model/[...path].ts

// the standard Prisma client
const prisma = new PrismaClient();

// create a Next.js API endpoint handler
export default requestHandler({
    // set up a callback to get a database instance for handling the request
    getPrisma: async (req, res) => {
        // get current user in the session
        const user = await getSessionUser(req, res);

        // return an enhanced Prisma client that enforces access policies
        return withPresets(prisma, { user });
    },
});
```

### 4. Use the Generated Hooks to Access Data

Use the generated hooks to build up UI parts with ease. The hooks talk to the data access API installed in the previous step. Since the access policies already secure the API, the hooks will only return entities that are readable to the current user and reject any unauthorized mutations.

```tsx
// /src/components/posts.tsx

import { usePost } from '../lib/hooks';

const Posts: FC = () => {
    // "usePost" is a generated hooks method
    const { findMany } = usePost();

    // list unpublished posts together with their author's data,
    // the result "posts" only include entities that are readable
    // to the current user
    const posts = findMany({
        where: { published: false },
        include: { author: true },
        orderBy: { updatedAt: 'desc' },
    });

    // entities are accurately typed based on the query structure
    // posts: Array<Post & { author: user }>
    return (
        <ul>
            {posts.map((post) => (
                <li key={post.id}>
                    {post.title} by {post.author.e}
                </li>
            ))}
        </ul>
    );
};
```

## Why Do They Improve Your Productivity?

The combination of Prisma and ZenStack improves your development productivity in several ways:

### 1. A clear picture of your data model

Using a DSL to model your entities, you always have an unambiguous description of your app's data model centralized in a single, easy-to-read file.

### 2. Safeguard close to the database

Security is hard partly because the rules need to be consistently applied across all the related APIs. By modeling security declaratively, you make a safeguard close to the database and can avoid the risk of forgetting to add necessary checks when adding or updating an API. It's also much easier to adjust rules when requirements change because the schema is your single source of truth.

### 3. Powerful and type-safe client library for free

Prisma client's (backend) Typescript API offers excellent type safety. Thanks to ZenStack's client library generation, you can now enjoy the same programming experience right in your frontend code.

## Is It a Good Choice For Me?

Prisma + ZenStack can be a good fit for your project if some of the following situations apply:

-   You desire a simple architecture and want to build a full-stack web app entirely with a meta framework (like Next.js or Remix.run), without a separate backend.
-   Your application has non-trivial security requirements.
-   You're not a SQL guru and would like to avoid complex SQL tasks whenever possible. Otherwise, [Supabase](https://dev.to/zenstack/backend-less-web-development-with-supabase-97l) or [PostgREST](https://dev.to/zenstack/backend-less-web-development-with-postgrest-3ma8) may be a better choice.
-   You want to avoid being locked into a specific database kind or hoster.

## Wrap Up

Prisma and ZenStack make an excellent combination for building up a secure backend with a relational database. I hope you find it useful for your next project.