How to Build a Production-Ready Todo App in One Next.js Project With ZenStack
Tags: nextjs, webdev, tutorial, typescript
Published: 2022-11-10 09:06:24

**Notice**: ZenStack has recently released its 1.0 version, and this post was written based on the 0.5 version.  Although the basic concept and logic has been the same, some of the code in this post is not applicable in the 1.0 version anymore. I suggest you check out the new tutorial I wrote in:
{% embed https://dev.to/zenstack/how-to-build-a-collaborative-saas-product-using-nextjs-and-zenstacks-access-control-policy-1nc1 %}

---


Next.js did a great job in "**bringing the power of full-stack to the front end"**as their slogan on the website. However, you(or your collaborator) probably still need to spend significant energy designing and building up your app's server-side part. Things that make you stressed include:

- What kind of API to use? RESTful or GraphQL?
- How to model your data and map the model to both source code and database (ORM)?
- How to implement CRUD operations? Manually construct it or use a generator?
- How to evolve your data model?
- How to authenticate users and authorize their requests?

[**ZenStack**](https://zenstack.dev/?utm_campaign=devto&utm_medium=organic&utm_content=Todo_tutorial) aims to simplify and resolve these tasks from a front-end perspective, so we can move one step further to "**bringing the power of full-stack to the front end."**

This Tutorial will show you how to create a simple collaborative Todo web app below with the ZenStack library using Next.js and PostgreSQL database step by step.

The app would have below features:

- Register/Sign-in with the email and password.
- Create/Delete a Todo list, either public or private. Public means others could see it.
- Create/Delete/Complete a Todo under a Todo list.

Here is an example of final result:

[https://zenstack-nextjs-todo-demo.vercel.app](https://zenstack-nextjs-todo-demo.vercel.app/) ([source](https://github.com/zenstackhq/zenstack-nextjs-todo-demo/tree/todo))

##Join the Conversation
If you have questions about anything related to this tutorial, you're welcome to ask our community on [Discord](https://go.zenstack.dev/chat ).

It is a long article divided into four chapters. I will make sure you have something fun to play during the quarter break😉:
1. Set Up The Project
2. User Register Sign-in
3. Todo List
4. Todo

Let's move on!

# 1. Set Up The Project

1. Create a template application using the below command:
    
    ```bash
    npx create-next-app todo --use-npm -e https://github.com/zenstackhq/nextjs-auth-postgres-template
    ```
    

2. Install [ZenStack VS code extension](https://marketplace.visualstudio.com/items?itemName=zenstack.zenstack). So When you write your **ZModel**, you can get the syntax highlighting, linting, code completion, formatting, and jump-to-definition in VScode same as writing Typescript:

![VS code extension](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dlmlkvgi77ow76hnlpk0.png)

Let me show you what the **ZModel** is. 

# 2. User Register Sign-in

## ZModel

ZModel is the core part of the ZenStack library. You can think of it as the "Class" in any Object-oriented programming(OOP), including Typescript we are using now, with the main important characteristic: **the instance of it will be persisted in the database.**

If you are familiar with [Object-Relational Mapping](https://en.wikipedia.org/wiki/Object-relational_mapping)(ORM), you might wonder if this is the front-end version of ORM. Yes, but it's only part of it. Besides the data structure usually defined by ORM, ZModel also contains data access policies just like the logic of the Class.

Therefore, the first and foremost job of using ZenStack is to define the Model appropriately. After that, you can 99% focus on front-end development like you used to do. So let's start with the first Model.

All the models should be defined in the schema file **`zenstack/schema.zmodel`** under the root of your project.  It should have been generated by the template,  open it you can find the below snippet at the top:

```tsx
datasource db {
    provider = 'postgresql'
    url = env('DATABASE_URL')
}
```

The datasource at the top determines how ZenStack will connect the database.  We will use Postgres as our database.  So what you need to do is to add your database connection URL in the .env file as below:

```bash
DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@[YOUR-URL]/postgres"
```

If you don't have a Postgres database, the simple way to get one is to get a docker instance or a free one from [Supabase.](https://supabase.com) 

Note: if you want to play around in your local machine without getting a Postgres instance, you can also use [SQLite](https://www.sqlite.org/index.html), a file-based database. You can change the datasource as below:

```tsx
datasource db {
    provider="sqlite"
    url="file:./dev.db"
}
```

## User ZModel

The rest part of the schema file is the definition of the User ZModel:

```tsx
/*
 * User model
 */
model User {
  id String @id @default(uuid())
  email String @unique
  emailVerified DateTime?
  // @password indicates the field is a password and its
  // value should be hashed (with bcrypt) before storing
  // @omit indicates the field should not be returned on read
  password String @password @omit
  name String?

  // everybody can signup
  @@allow('create', true)
  // can only be updated and deleted by self
  @@allow('read,update,delete', auth() == this)
  }
```

The Model consists of two parts:

- Data structure.  It is defined by the individual field. Each field should have a name, type, and optional attributes. The name and type have no different from any other language. The optional attribute adds a constraint to the field. Let's go through them one by one:
    - `@id`. It is used to identify an individual record uniquely. Every Model must have an ID. Internally It is mapped as the primary key of the table in the database.
    - `@default(cuid())`.  It is used to give a default value for a field so that you don’t have to give the value for it when creating it. The `cuid()` is to generate a globally unique identifier that has a better lookup performance than `uuid()`. It is usually used together with `@id` to generate business independent id.
    - `@unique`.  It defines a unique constraint for the email field. If you are trying to create an instance with an email that has existed, it will throw an error.
    - `@password`. It indicates the field is a password, and its value should be hashed (with bcrypt) before storing.
    - `@omit` . It indicates the field should not be returned on read, which means it should only be accessed from the backend, like secret, password, etc.
    
This part is what an ORM provides. Internally, it would be converted into the [Prisma](https://www.prisma.io) schema. Now it is fully compatible with Prisma schema. So to get the complete list of what you can use, please take a reference with the [Prisma schema document.](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference) 
    

- Access policy. As mentioned above, ZenStack provides a data access policy over the traditional ORM. Let's take a look at the two policies used here:
    - `@@allow('create', true)`.  It means you can always create a new user, no matter your identity. We need to specify it explicitly because, by default, all the operation is forbidden from the front end. You need to open it explicitly.
    - `@@allow('read,update,delete', auth() == this)`.  It means only the current user instance could be read/written by himself.  ZModel allows you to reference the current login user via `auth()` function in access policy expressions.  So when the front-end sends the query to the backend through the API provided by ZenStack library,  it will first go through the guard code generated by ZenStack in the backend to ensure it would only access the data comply with the policy.  Therefore front-end code could safely call the API to implement the business logic without worrying about access control.
    

You can find the complete ZModel language definition [here](https://github.com/zenstackhq/zenstack/blob/main/docs/get-started/learning-the-zmodel-language.md).

## Authentication

Almost every modern application has authentication now, which the access policy is based on as the `auth()` function you have seen.  To simplify the task, ZenStack has integrated with the open source authentication library [NextAuth](https://next-auth.js.org/), which we will use in this demo.  

### NextAuth

**If you are familiar with the NextAuth, you can skip this part.** 

[NextAuth](https://next-auth.js.org/) has vast coverage of providers ranging from Google, Github, Facebook, Apple, Slack, Twitter, etc. We will use the basic email/password login to avoid external dependency.

Below I've included a description of how to use NextAuth in the Next project. You can get more detailed instructions from the [official document of NextAuth](https://next-auth.js.org/getting-started/example).

- Create a file called `[...nextauth].js`in `pages/api/auth`This contains the dynamic route handler for NextAuth.js which will also contain all of your global NextAuth.js configurations.
    
    ```tsx
    export const authOptions: NextAuthOptions = {
      session: {
        strategy: "jwt",
      },
    
      providers: [
        CredentialsProvider({
          credentials: {
            email: {
              label: "Email Address",
              type: "email",
              placeholder: "Your email address",
            },
            password: {
              label: "Password",
              type: "password",
              placeholder: "Your password",
            },
          },
        }),
      ],
     callbacks: {
      async session({ session, token, user }) {
      //Send properties to the client, like an access_token from a provider.
        session.accessToken = token.accessToken
        return session
      }
    }
    };
    
    export default NextAuth(authOptions);
    ```
    
    The CredentialsProvider is used for the email/password login and uses JWT token to store the session information in the client.
    
    `session()`callback function is used to add additional information to the client when calling `useSession()` below.
    
- Get session data from the front-end
    
    NextAuth provides a `useSession()` React Hook to check if someone is signed in like below:
    
    ```tsx
    const { data: session } = useSession()
      if (session) {
        return (
          <>
            Signed in as {session.user.email} <br />
            <button onClick={() => signOut()}>Sign out</button>
          </>
        )
      }
      return (
        <>
          Not signed in <br />
          <button onClick={() => signIn()}>Sign in</button>
        </>
      )
    ```
    
    To be able to use it, first, you'll need to expose the session context,[`<SessionProvider/>`](https://next-auth.js.org/getting-started/client#sessionprovider), at the top level of your application:
    
    ```tsx
    import { SessionProvider } from "next-auth/react"
    export default function App({
      Component,
      pageProps: { session, ...pageProps },
    }) {
      return (
        <SessionProvider session={session}>
          <Component {...pageProps} />
        </SessionProvider>
      )
    }
    ```
    
- Get session data from the back-end
    
    You can use the [`unstable_getServerSession()`](https://next-auth.js.org/configuration/nextjs#unstable_getserversession)method to retrieve the session data like the below:
    
    ```tsx
    import { unstable_getServerSession } from "next-auth/next"
    import { authOptions } from "./auth/[...nextauth]"
    export default async (req, res) => {
      const session = await unstable_getServerSession(req, res, authOptions)
      if (session) {
        res.send({
          content:
            "This is protected content. You can access this content because you are signed in.",
        })
      } else {
        res.send({
          error: "You must be signed in to view the protected content on this page.",
        })
      }
    }
    ```
    
- Persist user data in the database
    
    NextAuth support creating an **Adapter** to connect your application to whatever database or backend system.  ZenStack has implemented its own adapter,  you will see how to use it below. 
    

### ZenStack Adapter

Let's see how to hook up with the ZenStack Adapter in our demo.

1. The first thing required is to have a user entity defined to include [specific fields.](https://next-auth.js.org/adapters/models) The user ZModel defined above meets the requirement, so there is nothing you need to change.  The Adapter is hooked in `pages/api/auth/**[...nextauth].js`**  
    
    ```tsx
    import NextAuth, { NextAuthOptions, User } from "next-auth";
    import CredentialsProvider from "next-auth/providers/credentials";
    import { authorize, NextAuthAdapter as Adapter } from "@zenstackhq/runtime/auth";
    import service from "@zenstackhq/runtime";
    
    export const authOptions: NextAuthOptions = {
      // use the ZenStack next-auth adapter for user identity persistence
      adapter: Adapter(service),
    
      session: {
        strategy: "jwt",
      },
    
      providers: [
        CredentialsProvider({
          credentials: {
            email: {
              label: "Email Address",
              type: "email",
              placeholder: "Your email address",
            },
            password: {
              label: "Password",
              type: "password",
              placeholder: "Your password",
            },
          },
          // use the "authorize" helper generated by ZenStack to authenticate user login
          authorize: authorize(service),
        }),
      ],
    
      callbacks: {
        async session({ session, token }) {
          // unbox the user entity from session and get userId from JWT token
          return {
            ...session,
            user: {
              ...session.user,
              id: token.sub!,
            },
          };
        },
      },
    };
    
    export default NextAuth(authOptions);using 
    ```
    
    `authorize()` function generated by ZenStack would actually check the email/password provided by the client to see whether sign-in is allowed when calling `signIn()` provided by NextAuth. 
    

1. The value returned by `auth()`is provided via the `getServerUser`hook function when mounting ZenStack APIs in `pages/api/zenstack/[…path].ts` :
    
    ```tsx
    import { NextApiRequest, NextApiResponse } from "next";
    import { type RequestHandlerOptions, requestHandler } from "@zenstackhq/runtime/server";
    import { authOptions } from "../auth/[...nextauth]";
    import { unstable_getServerSession } from "next-auth";
    import service from "@zenstackhq/runtime";
    
    const options: RequestHandlerOptions = {
      async getServerUser(req: NextApiRequest, res: NextApiResponse) {
        const session = await unstable_getServerSession(req, res, authOptions);
        return session?.user;
      },
    };
    export default requestHandler(service, options);
    ```
    

1. Add [`<SessionProvider/>`](https://next-auth.js.org/getting-started/client#sessionprovider)
    
    ```tsx
    import { SessionProvider } from "next-auth/react";
    import type { AppProps } from "next/app";
    import type { Session } from "next-auth";
    
    function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps<{ session: Session }>) {
      return (
        <SessionProvider session={session}>
          <div className="flex-grow h-100">
            <Component {...pageProps} />
          </div>
        </SessionProvider>
      );
    }
    
    export default MyApp;
    ```
    
2. Use `useSession()` to see whether the user is successfully checked in. 
    
    ```tsx
    import React from "react";
    import { signIn, signOut, useSession } from "next-auth/react";
    
    export default function Home() {
      const { status, data } = useSession();
    
      if (status === "loading") {
        return <p>Loading...</p>;
      } else if (status === "unauthenticated") {
        signIn();
        return <></>;
      } else {
        const authUser = data?.user;
        return (
          <>
            {authUser && (
              <div className="mt-8 text-center flex flex-col items-center w-full">
                <h1 className="text-2xl text-gray-800">Hello World!</h1>
                <button onClick={() => signOut()}>logout</button>
              </div>
            )}
          </>
        );
      }
    }
    ```
    

### Sign up

With sign-in taken care of by the Adapter,  we need to implement the signup function, which means we need to be able to create a user entity. 

As mentioned above, ZenStack would generate React Hook for each ZModel type.  So for User, the `userUser()`  hook is generated in @zenstackhq/runtime/hooks

```tsx
export declare function useUser(): {
    create: <T extends P.UserCreateArgs>(args: P.UserCreateArgs) => Promise<P.CheckSelect<T, User, P.UserGetPayload<T, keyof T>>>;
    find: <T_1 extends P.UserFindManyArgs>(args?: P.SelectSubset<T_1, P.UserFindManyArgs> | undefined) => SWRResponse<P.CheckSelect<T_1, User[], P.UserGetPayload<T_1, keyof T_1>[]>, any>;
    get: <T_2 extends P.Subset<P.UserFindFirstArgs, "select" | "include">>(id: String, args?: P.SelectSubset<T_2, P.Subset<P.UserFindFirstArgs, "select" | "include">> | undefined) => SWRResponse<P.CheckSelect<T_2, User, P.UserGetPayload<T_2, keyof T_2>>, any>;
    update: <T_3 extends Omit<P.UserUpdateArgs, "where">>(id: String, args: Omit<P.UserUpdateArgs, 'where'>) => Promise<P.CheckSelect<T_3, User, P.UserGetPayload<T_3, keyof T_3>>>;
    del: <T_4 extends Omit<P.UserDeleteArgs, "where">>(id: String, args?: Omit<P.UserDeleteArgs, 'where'>) => Promise<P.CheckSelect<T_4, User, P.UserGetPayload<T_4, keyof T_4>>>;
};
```

You can call these CRUD functions to access the data without explicitly writing HTTP requests.  

So let's create a signup page in `page/signup.tsx` 

```tsx
import React, { useState } from "react";
import Router from "next/router";
import { useUser } from "@zenstackhq/runtime/hooks";
import { signIn } from "next-auth/react";

const SignUp: React.FC = () => {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const { create: signup } = useUser();

  const submitData = async (e: React.SyntheticEvent) => {
    e.preventDefault();
    try {
      await signup({
        data: {
          email,
          name,
          password,
        },
      });

      const signInResult = await signIn("credentials", {
        redirect: false,
        email,
        password,
      });
      if (signInResult?.ok) {
        await Router.push("/");
      } else {
        console.error("Signin failed:", signInResult?.error);
      }
    } catch (error) {
      console.error(error);
      alert("This email has been registered");
    }
  };

  return (
    <>
      <div className="page">
        <form onSubmit={submitData}>
          <h1>Signup user</h1>
          <input autoFocus onChange={(e) => setName(e.target.value)} placeholder="Name" type="text" value={name} />
          <input onChange={(e) => setEmail(e.target.value)} placeholder="Email address" type="text" value={email} />
          <input
            onChange={(e) => setPassword(e.target.value)}
            placeholder="Your password"
            type="password"
            value={password}
          />
          <input disabled={!name || !email || !password} type="submit" value="Signup" />
          <a className="back" href="#" onClick={() => Router.push("/")}>
            or Cancel
          </a>
        </form>
        <p>
          Already have an account?{" "}
          <a
            className="signin"
            onClick={() =>
              signIn(undefined, {
                callbackUrl: "/",
              })
            }
          >
            Signin now
          </a>
          .
        </p>
      </div>
      <style jsx>{`
        .page {
          background: white;
          padding: 3rem;
          display: flex;
          flex-direction: column;
          justify-content: center;
        }
        input[type="text"],
        input[type="password"] {
          width: 100%;
          padding: 0.5rem;
          margin: 0.5rem 0;
          border-radius: 0.25rem;
          border: 0.125rem solid rgba(0, 0, 0, 0.2);
        }
        input[type="submit"] {
          background: #ececec;
          border: 0;
          padding: 1rem 2rem;
          cursor: pointer;
        }
        .signin {
          cursor: pointer;
          text-decoration: underline;
        }
        .back {
          margin-left: 1rem;
        }
      `}</style>
    </>
  );
};

export default SignUp;
```

The `submitData` function shows that signup is actually simply calling the `create` function of `userUser()` hook provided by ZenStack.  Since`@password` attribute on the `password` property will hash the password in the back end, so we could only pass the plain text here.

If the signup succeeds, it will automatically call the `signIn` of NextAuth to get the user signed in and then go to the home page.

## Generate From Schema

Once you have the schema ready, there are two things you need to do before running the app.

Firstly, we need to generate all the code stubs from the ZModel schema by running:

```bash
npm run generate
```

You should see the output below :

```bash
✔️ Prisma schema and query guard generated
  ✔️ ZenStack service generated
  ✔️ React hooks generated
  ✔️ Next-auth adapter generated
  ✔️ Typescript source files transpiled
👻 All generators completed successfully! 
```

- The Prisma schema named 'schema.prisma' is generated beside your ZModel file.
- The React Hook to access User is generated under  @zenstackhq/runtime/hooks.
- Next-auth adapter is generated @zenstackhq/runtime/auth
- Typescript type definition of User is generated under @zenstackhq/runtime/types

Secondly, We need to sync the database with the schema. If you haven't added `DATABASE_URL` in the .env, you can add it now. Then run:

```bash
npm run db:push
```

You should see the output below:

```bash
🚀  Your database is now in sync with your Prisma schema. Done in 2.56s
```

Check your database. There should be a User table generated.

## Run the Code

The template project has all it needs to run for the complete user registration and sign-in. So now you can run it as a standard Next.js app by:

```bash
npm run build & npm run dev
```

Then visit [http://localhost:3000/singup](http://localhost:3000/singup), you should be able to see the signup form:

![Signup Form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wy6tcu2z6th37w4rvskq.png)


After signup with your account information,  you will see the familiar message: 


![Hello world](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q2c4e5ptr1jzzczemq2d.png)

Now run the below command:

```bash
npm run db:browse
```

It will open a page in [http://localhost:5555/](http://localhost:5555/) and show you the data stored in the database. You should be able to see your user record:


![Database record](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xujie6xa5k37hc1i1718.png)

## Query User Data

We have seen how to create the user entity in the signup chapter above. Next, let's see how to query it.

Similar with `create`,  it uses the `get` hook of `useUser`. So you need to pass the user id getting from the `useSession`. So Let’s open `index.tsx` and change it to below:

```tsx
import React from "react";
import { signIn, signOut, useSession } from "next-auth/react";
import { useUser } from "@zenstackhq/runtime/hooks";

export default function Home() {
  const { status, data } = useSession();
  const { get } = useUser();
  const { data: user } = get(data?.user.id!);

  if (status === "loading") {
    return <p>Loading...</p>;
  } else if (status === "unauthenticated") {
    signIn();
    return <></>;
  } else {
    return (
      <>
        {user && (
          <div className="mt-8 text-center flex flex-col items-center w-full">
            <h1 className="text-2xl text-gray-800">Welcome {user.name || user.email}!</h1>
            <button onClick={() => signOut()}>logout</button>
          </div>
        )}
      </>
    );
  }
}
```

Make sure the server is still running, and revisit [http://localhost:3000](http://localhost:3000/). You should see your user name displayed on the screen:


![welcome message](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r6itm0i44037r7qo4dao.png)

If you open the Network in the developer tool panel, you can find the actual HTTP request to get data with the URL looks like below:

[http://localhost:3000/api/zenstack/data/User/cl9tqaibt0000t2sskmmw7g3r](http://localhost:3000/api/zenstack/data/User/cl9tqaibt0000t2sskmmw7g3r)

`cl9tqaibt0000t2sskmmw7g3r` is the id of the user ZModel 

## Polishing Layout

So far, we have used minimal code to illustrate how the ZenStack library works for you. Before we move on, let's polish our page styling to make it look natural.

We choose to [daisyUI](https://daisyui.com/) component with [TailWindCSS](https://tailwindcss.com/) utility. We will create a Navigation bar for it. As it's pure front-end Layout work, we will not dive into the detail in this tutorial. 

The page looks like this now: 

![Polished home page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y2u8u0ils2vqoleldkyj.png)

We also replace the default login page with a custom one implemented using [Chakra UI](https://chakra-ui.com/) components. Again, you can look at how to do that from the [official NextAuth document](https://next-auth.js.org/configuration/pages#credentials-sign-in).

Moreover, we combined it with the signup page together, so it looks like this now:

![Combined singin/up](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ikpmi0k3ejoespey6503.png)

You can check out the finished code under:

[https://github.com/zenstackhq/zenstack-nextjs-todo-demo/tree/signin](https://github.com/zenstackhq/zenstack-nextjs-todo-demo/tree/signin)

# 3. Todo List

After signing in, the user would first create a Todo list. So let's start to implement a Todo list. Like what we did for the user, it should always start with defining the ZModel. There are two parts of a ZModel: data structure and access policy.

## Data Structure

To be simple, let's use **List** as the name for this Model. So let's consider what data we need for a Todo list entity.

- A unique id
    
    ```tsx
    id String @id @default(uuid())
    ```
    
- Create/Update time
    
    ```tsx
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    ```
    
    We can use `now()` to let the library generate it automatically when creating so that we don’t have to specify the value ourselves.  The `@updateAt` will automatically set the field value when this entity is updated. 
    
- A title
    
    ```tsx
    title String
    ```
    
- A private flag.  If you remember our Todo list could be either public or private
    
    ```tsx
    Boolean @default(false)
    ```
    
- Owner.  To whom this Todo list belongs.
    
    This comes to an essential concept **Relations**. A relation is a connection between two models in the ZModel schema. In our case, the relation between User and Todo list is that a Todo list must belong to a User; On the other hand, a User could have many Todo lists. This is called One-to-Many relation. The other two One-to-One and Many-to-Many relations will not be covered in this tutorial.
    
    Relation is the predefined constraint between the Model to make, which is usually implemented at the database level. So data consistency would be guaranteed, meaning you can't write the inconsistent data like a non-existing user to be the owner of a Todo list.
    
    Since the relation is between two models, defining it also involves two models. 
    
    ### Todo list ZModel
    
    ```tsx
    ownerId String
    owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
    ```
    
    - ownerId.  The value of this field equals to the id of the User entity it belongs to. This is what actually persisted in the database.  This field is referenced by the `@relation` attribute.
    - owner.  This is the object reference to the User entity.   It would not be persisted in the database.  The value would be set by the ZenStack library through the `fields` and `references` parameters of `@relation` attribute.
        - `fields` tells which fields in the current model store the unique id of the referenced entity.  In our case, it is the ownerId above.
        - `references` tells which fields are the unique id in the referenced Model. In our case, it is the id of the User ZModel.
        - `onDelete` tells what we should do about this record if the referenced record is deleted.  `Cascade` means if a user is deleted, then all the Todo list  belonging to him would be deleted automatically too.
    
    ### User ZModel
    
    This part is simple. We only need to add the reference to all the Todo Lists belonging to this user.  So please add the blew field in the User Model:
    
    ```tsx
    model User {
    ...
    lists List[]
    ...
    }
    ```
    
    The benefit of this reversed reference is that when you query the user, you can also get all the Todo lists belonging to this user.
    
    Internally the relation is handled by Prisma too, so if you want to know more about it, please check out the [Prisma](https://www.prisma.io/docs/concepts/components/prisma-schema/relations) document. 
    

The complete Todo list Model data structure would be:

```tsx
model List {
    id String @id @default(uuid())
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    title String
    private Boolean @default(false)
    ownerId String
    owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
}
```

## Access Policy

### ToDo List

The access policy depends on the functionality from the business side. So let's go through it one by one:

Firstly, two standard rules probably apply to most cases in web applications:

- You can't access it without login

```tsx
// require login
@@deny('all', auth() == null)
```

- The owner has the full access. Remember we have the owner reference field, so we can use it to compare with the User entity returned by `auth()`.  Internally it will check the equality using id.

```tsx
// owner has full access
@@allow('all', auth() == owner)
```

Then there is a related business functionality: if the Todo list is public, it could also be seen by others without modification. 

```tsx
// can be read by anyone if it is public 
@@allow('read', !private)
```

The logic of permitting/rejecting an operation is as follows:

- By default, all operations are rejected if there isn't any @@allow rule in a model
- The operation is rejected if any of the conditions in @@deny rules evaluate to `true`
- Otherwise, the operation is permitted if any of the conditions in @@allow rules evaluate to `true`
- Otherwise, the operation is rejected

### User

If you can see other's Todo list, you would also need to know to whom it belongs. However, the current policy of user Model only allow read by self, so we need to change it:

```tsx
  // everybody can signup
  @@allow('create', true)
  // can be read by other users
  @@allow('read', auth() != null)
  // can only be updated and deleted by self
  @@allow('update,delete', auth() == this)
```

## Generate From Schema

This is the same step as what we did for the User registration steps by running the below two commands:

```bash
// generate code stub
npm run generate
// sync with database
npm run db:push
```

## Write Front-End Code

With the schema ready, let's focus on the front-end implementation. Firstly, Let's add two more dependencies we will use:

```bash
npm i moment
npm i @heroicons/react
```

### Todo list Component

Then let's create the Todo list UI component in `components/TodoList.tsx` with the below props:

```bash
import { List, User } from "@zenstackhq/runtime/types";

type Props = {
  value: List & { owner: User };
};
```

As this is a Todo list, it needs a Todo list entity to render. ZenStack will generate the corresponding typescript type definition for every Model under `@zenstackhq/runtime/types`. So we can directly use it here.

One thing to note is that although the ZenStack library can set the reference type in the query result, the type definition doesn't have that field. So you need to specify it to pass the type checking explicitly.

Next Let’s implement the delete function:

```bash
import { useList } from "@zenstackhq/runtime/hooks";

export default function TodoList({ value, deleted }: Props) {
  const router = useRouter();

  const { del } = useList();

  const deleteList = async () => {
    if (confirm("Are you sure to delete this list?")) {
      try {
        await del(value.id);
      } catch (error: any) {
        if (error.status == 403) {
          alert("You are now allowed to do so");
        }
     
    }
  };
}
```

To execute the delete operation,  all you need to do is call  `del` hooks provided by ZenStack library and pass the id of the list.  Since the hooks is guarded by the access policy, you should always handle the exception when the policy check fails. 

Since the Todo list entity has the createdAt/updatedAt field, we would like to show the date on the list. So, let's create a TimeInfo component for it:

```bash
import moment from "moment";

type Props = {
  value: { createdAt: Date; updatedAt: Date };
};

export default function TimeInfo({ value }: Props) {
  return (
    <p className="text-sm text-gray-500">
      {value.createdAt === value.updatedAt
        ? `Created ${moment(value.createdAt).fromNow()}`
        : `Updated ${moment(value.updatedAt).fromNow()}`}
    </p>
  );
}
```

Also, we use [picsum.photos](https://picsum.photos/) to show image covers. To use it, add the below in the next.config.js:

```bash
	images: {
    domains: ["picsum.photos"],
  },
```

The complete code for Todo list component is below:

```bash
import Image from "next/image";
import { List, User } from "@zenstackhq/runtime/types";
import { customAlphabet } from "nanoid";
import { LockClosedIcon, TrashIcon } from "@heroicons/react/24/outline";
import Avatar from "./Avatar";
import Link from "next/link";
import { useRouter } from "next/router";
import { useList } from "@zenstackhq/runtime/hooks";
import TimeInfo from "./TimeInfo";

type Props = {
  value: List & { owner: User };
  deleted?: (value: List) => void;
};

export default function TodoList({ value, deleted }: Props) {
  const router = useRouter();

  const { del } = useList();

  const deleteList = async () => {
    if (confirm("Are you sure to delete this list?")) {
      try {
        await del(value.id);
      } catch (error: any) {
        if (error.status == 403) {
          alert("You are not allowed to do so");
        }
      }
      if (deleted) {
        deleted(value);
      }
    }
  };

  return (
    <div className="card w-80 bg-base-100 shadow-xl cursor-pointer hover:bg-gray-50">
      <a>
        <figure>
          <Image
            src={`https://picsum.photos/300/200?r=${value.id}`}
            width={320}
            height={200}
            alt="Cover"
          />
        </figure>
      </a>
      <div className="card-body">
        <Link href={`${router.asPath}${value.id}`}>
          <a>
            <h2 className="card-title line-clamp-1">{value.title || "Missing Title"}</h2>
          </a>
        </Link>
        <div className="card-actions flex w-full justify-between">
          <div>
            <TimeInfo value={value} />
          </div>
          <div className="flex space-x-2">
            <Avatar user={value.owner} size={18} />
            {value.private && (
              <div className="tooltip" data-tip="Private">
                <LockClosedIcon className="w-4 h-4 text-gray-500" />
              </div>
            )}
            <TrashIcon
              className="w-4 h-4 text-gray-500 cursor-pointer"
              onClick={() => {
                deleteList();
              }}
            />
          </div>
        </div>
      </div>
    </div>
  );
}
```

### Create Todo list

You can use the `create` hooks to create a Todo list. Since both createdAt/updated will be handled automatically, we don't need to specify it

```tsx
const { create } = useList();

await create({
        data: {
          title,
          ownerId: user!.id,
          private: _private,
        },
      });
```

### Get all the Todo list

You can use the `find` hooks to retrieve all the Todo list the current user could see:

```tsx
const { find } = useList();

const { data: lists } = find({
  include: {
    owner: true,
  },
  orderBy: {
    updatedAt: "desc",
  },
});
```

By default, the related entities wouldn’t be retrieved for performance issues. If you want it in the result, you need to specify it in the `include` property.

Let's replace the `index.tsx` file with the below content to include the two features above:

```tsx
import { UserContext } from "@lib/context";
import { ChangeEvent, FormEvent, useContext, useState } from "react";
import { useList } from "@zenstackhq/runtime/hooks";
import TodoList from "components/TodoList";

function CreateDialog() {
  const user = useContext(UserContext);

  const [modalOpen, setModalOpen] = useState(false);
  const [title, setTitle] = useState("");
  const [_private, setPrivate] = useState(false);

  const { create } = useList();

  const onSubmit = async (event: FormEvent) => {
    event.preventDefault();

    try {
      await create({
        data: {
          title,
          ownerId: user!.id,
          private: _private,
        },
      });
    } catch (err) {
      alert(`Failed to create list: ${err}`);
      return;
    }

    // reset states
    setTitle("");
    setPrivate(false);

    // close modal
    setModalOpen(false);
  };

  return (
    <>
      <input
        type="checkbox"
        id="create-list-modal"
        className="modal-toggle"
        checked={modalOpen}
        onChange={(e: ChangeEvent<HTMLInputElement>) => {
          setModalOpen(e.currentTarget.checked);
        }}
      />
      <div className="modal">
        <div className="modal-box">
          <h3 className="font-bold text-xl mb-8">Create a Todo list</h3>
          <form onSubmit={onSubmit}>
            <div className="flex flex-col space-y-4">
              <div className="flex items-center">
                <label htmlFor="title" className="text-lg inline-block w-20">
                  Title
                </label>
                <input
                  id="title"
                  type="text"
                  required
                  placeholder="Title of your list"
                  className="input input-bordered w-full max-w-xs mt-2"
                  value={title}
                  onChange={(e: FormEvent<HTMLInputElement>) => setTitle(e.currentTarget.value)}
                />
              </div>
              <div className="flex items-center">
                <label htmlFor="private" className="text-lg inline-block w-20">
                  Private
                </label>
                <input
                  id="private"
                  type="checkbox"
                  className="checkbox"
                  onChange={(e: FormEvent<HTMLInputElement>) => setPrivate(e.currentTarget.checked)}
                />
              </div>
            </div>
            <div className="modal-action">
              <input className="btn btn-primary" type="submit" value="Create" />
              <label htmlFor="create-list-modal" className="btn btn-outline">
                Cancel
              </label>
            </div>
          </form>
        </div>
      </div>
    </>
  );
}

export default function Home() {
  const { find } = useList();

  const { data: lists } = find({
    include: {
      owner: true,
    },
    orderBy: {
      updatedAt: "desc",
    },
  });

  return (
    <>
      <div className="p-8">
        <div className="w-full flex flex-col md:flex-row mb-8 space-y-4 md:space-y-0 md:space-x-4">
          <label htmlFor="create-list-modal" className="btn btn-primary btn-wide modal-button">
            Create a list
          </label>
        </div>

        <ul className="flex flex-wrap gap-6">
          {lists?.map((list) => (
            <li key={list.id}>
              <TodoList value={list} />
            </li>
          ))}
        </ul>

        <CreateDialog />
      </div>
    </>
  );
}
```

The complete code of this version is under:

[https://github.com/zenstackhq/zenstack-nextjs-todo-demo/tree/list](https://github.com/zenstackhq/zenstack-nextjs-todo-demo/tree/list)

## Run it

Let’s log in with User A and create one public list and one private list like below:

![Todo list result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gqyoscnzbnqhktom55eq.png)

Then let’s login with another User B,  then you could only see the Public list


![Todo list another user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7iz0wmsofi01x24bt1z0.png)

And If you try to delete this by user B you will see the error below:


![Delete error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qdrdu3zf54spcipv54ky.png)

Isn't it cool that all these things would be handled by the access policy that you don't need to worry about in your code? 😎

# 4. Todo

Let's implement the last part of this tutorial, the Todo. Let's begin with the ZModel, as always:

## Data Structure

The data structure is like below:

```tsx
model Todo {
    id String @id @default(uuid())
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
    ownerId String
    list List @relation(fields: [listId], references: [id], onDelete: Cascade)
    listId String
    title String
    completedAt DateTime?
}
```

With the experience of Todo list,  there is nothing new for you except it has two reference field owner and list. 

## Access Policy

The access policy is like below:

```tsx
// require login
@@deny('all', auth() == null)
// list owner has full access 
@@allow('all', list.owner == auth())
// can be read by anyone if is public 
@@allow('read', !list.private)
```

1. The first one is the same as Todo.
2. The second one means the owner of the list this Todo belongs to has the full access, which is done by referencing the list's field `list.owner`.
Why don't we use `owner == auth()`, like Todo? The problem is that then you could create a Todo under another person's public list.
3. It means if the list is public, then the Todo under it should be public too.

## Write Your Own Code

Since you have been here,  how about writing the Todo implementation by your own. Use this as a test to see how much you have learned about using the ZenStack library.  💪

Don't worry if you got blocked by something. You can always take a reference of our implementation below:

[https://github.com/zenstackhq/zenstack-nextjs-todo-demo/tree/todo](https://github.com/zenstackhq/zenstack-nextjs-todo-demo/tree/todo) 

# Finally

Congratulations on finishing this long journey! [ZenStack](https://zenstack.dev/?utm_campaign=devto&utm_medium=organic&utm_content=Todo_tutorial) is an open-source project in its infant stage. If you think it's useful, let's raise it together. Let us know your thought on [Twitter](https://twitter.com/intent/tweet?text=Wow%20%40zenstackhq), [Discord](https://go.zenstack.dev/chat), [GitHub](https://go.zenstack.dev), or whatever you like.

Coming next would be episode 2 which I will show you how to add space/organization to make it like an actual SAAS product