Create fullstack book app: CRUD operations w/ tRPC
Tags: webdev, nextjs, trpc, devjournal
Published: 2022-07-13 17:13:01

Hello! In this part of the series, I'd like to talk a little bit about CRUD operations done via tRPC in my made-up book app. That means our book app should be able to **C**reate, **U**pdate, **R**ead and **D**elete records. Also, we'll fetch (read) data from external API. At first, all the operations may feel a bit overwhelming. That's why I prefer to list all of the operations and start crossing them out of the list, one by one.

Here's my list

- create a chapter (C)
- display book detail with all the chapters (R)
- update a chapter (U)
- delete a chapter (D)
 
I think it makes sense to start with the read portion.

## Display book detail with all the chapters
To get a book detail, first, we need to have its ID, in this case, we'll be using the isbn13 code. I've previously mentioned an API I've chosen to work with called [https://api.itbook.store/](https://api.itbook.store/). The API can fetch the newest books by calling [https://api.itbook.store/1.0/new](https://api.itbook.store/1.0/new). You can look at what's being returned. I'll make a type of just some attributes. On top of that, I could've mapped over the result, but this isn't a production site and so I didn't bother. I got my fetch function from [Kent C. Dodds' blog](https://kentcdodds.com/blog/using-fetch-with-type-script).

```
export type Book = {
  title: string;
  subtitle: string | null;
  isbn13: string;
  price: string;
  image: string;
};

export type BookList = {
  error: number;
  total: number;
  page?: number;
  books: Book[];
};

export function fetchBooks(type = "", customConfig = {}): Promise<BookList> {
  const config = {
    method: "GET",
    ...customConfig,
  };

  const url = `${process.env.BOOKS_URL}${type}`;

  return fetch(url, config).then(async (response) => {
    if (response.ok) {
      return await response.json();
    } else {
      const errorMessage = await response.text();
      return Promise.reject(new Error(errorMessage));
    }
  });
}
```

What I've done at first was to create a new router and chained a `query` function that calls `fetchBooks` function above, like this

```
export const booksRouter = createRouter()
  .query("newest", {
    async resolve() {
      const bookList = await fetchBooks("new");

      return bookList.books;
    },
  })
```

Not sure how I feel about it yet, but I don't think this is the best, so either call it with react query

```
const result = useQuery('newest', async () => await fetchBooks('new')
```
or in `getServerSideProps` of page component and pass it as props
```
type Props = {
  books: Books;
};

export const getServerSideProps: GetServerSideProps<Props> = async (
  ctx
) => {
  const book = await fetchBook('new');

  return {
    props: {
      books,
    },
  };
};

const NewestBooks = ({
  books,
}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
  ...
}
```

Now that we have a list of the newest books we can loop over them. Let's display some cards or something, important thing is we have to have a link to an individual book detail page. In general, you'll have something like this

```
books.maps((book) => {
  return (
    <div className="card">
       ...
       <Link href={`/books/${book.isbn13}`}>
         <a>...</a>
       </Link>
    </div>
  )
})
```
Style it however you want. Don't forget to create new page at `pages/books/[isbn13]/index.tsx`. This might look weird, but later on, when working with chapters, I want to have URLs like
```
/books/[isbn13]/chapter/add <- for adding new chapter
and
/books/[isbn13]/chapter/[id] <- for updating existing chapter
```
It probably doesn't make much of a difference, so don't sweat it if you'll have different file structure/routes, as long as it makes sense to you 😄. We're already at creating pages, so let's create pages for adding and updating chapters, like I mentioned previously.


At this point, we have the "newest books" page, "book detail" page, "add chapter" page and "edit chapter" page. In "book detail" page, we'll fetch the book by ISBN code by calling 

```
import { useRouter } from 'next/router'

const BookDetail = () => {
  const router = useRouter()
  const { isbn13 } = router.query
}
```
or in getServerSideProps 

```
export const getServerSideProps: GetServerSideProps<LocalProps> = async (
  ctx
) => {
  const code = ctx?.params?.isbn13;
  ...
};
```

We can fetch a book by isbn13 code by calling

```
export function fetchBook(isbn13 = "", customConfig = {}): Promise<Book> {
  const config = {
    method: "GET",
    ...customConfig,
  };

  const url = `${process.env.BOOKS_URL}books/${isbn13}`;

  return fetch(url, config).then(async (response) => {
    if (response.ok) {
      return await response.json();
    } else {
      const errorMessage = await response.text();
      return Promise.reject(new Error(errorMessage));
    }
  });
}
```
Similarly, like we did fetch the newest books.

```
export const getServerSideProps: GetServerSideProps<LocalProps> = async (
  ctx
) => {
  const book = ctx.params
    ? await fetchBook(ctx?.params?.isbn13 as string)
    : null;

  return {
    props: {
      book,
    },
  };
};
```

> sidenote: if you used `useRouter`, then fetch with react-query

Thanks to this request, the book detail may contain additional book info, but I definitely want list of chapters, that I can edit and delete, and I want to be able to create a new chapter. 

## Create a chapter
> sidenote: when talking about the next chapter we're gonna be creating, lets use term, "callable" pieces of code. When talking about a query, picture using it at the top of some component, on the other hand, when talking about a mutation, picture some kind of a button user will be clicking

Creating a chapter makes sense to do the next, because I want to have some chapters in DB first, which I'll edit or delete later. The scenario is: user comes to the book detail page, where they can either add the book to their "library", or if they've already done so, can add a new chapter.
Let's define, that both of these actions are done by clicking some kind of a button. At this stage, I'm not thinking about which one I'm gonna show or not. I'll be doing testing and I know in which order I have to be clicking buttons. 
Creating mutations for both buttons is a safe bet. I'll show you adding the book first

```
  .mutation("add-book", {
    input: z.object({
      title: z.string(),
      subtitle: z.string().nullish(),
      isbn13: z.string(),
      price: z.string(),
      image: z.string(),
    }),
    async resolve({ ctx, input }) {
      const bookNote = await ctx.prisma.bookNote.create({
        data: {
          ...input,
          authorId: ctx.user?.id || "",
        },
      });

      return bookNote;
    },
  })
```

Do you remember `context` from the previous post? Having a user at each function is super handy, like here. So, we'll create a new book note. Once that's done, we can add a chapter by calling 

```
  .mutation("add-chapter", {
    input: z.object({
      bookID: z.string(),
      payload: z.object({
        title: z.string(),
        text: z.string(),
      }),
    }),
    async resolve({ ctx, input }) {
      const chapter = await ctx.prisma.chapter.create({
        data: {
          ...input.payload,
          bookNoteId: input.bookID,
        },
      });

      return chapter;
    },
  })
```

In both cases, we can generalize, that when creating a new record, we call `create` method of the particular model. There, we're passing a `data` object, that will expect certain values defined in the schema.

You hook to this mutation like this
```
import { trpc } from "utils/trpc"

const exampleMutation = trpc.useMutation(["router.example"]);

...

<button disabled={exampleMutation.isLoading} onClick={exampleMutation.mutate} >Click to mutate</button>
```

Usually, after mutating, one wants to let's say refetch some kind of a query or redirect the user. Just as with react-query, you can use `onSuccess` attribute

```

const exampleMutation = trpc.useMutation(["router.example"], {
  onSuccess: (data) => {...}
});

```

At this point, we have two buttons, but in production, I'd like to switch between them. I'll have to be checking if I already have the book in my "library", and that way I'll decide which button to show

```
  .query("get-book", {
    input: z.object({
      isbn13: z.string(),
    }),
    async resolve({ ctx, input }) {
      const bookNote = await ctx.prisma.bookNote.findFirst({
        where: {
          authorId: String(ctx.user?.id),
          isbn13: input.isbn13,
        },
      });

      return bookNote;
    },
  })
```

Here, we're asking Prisma to give us a record with the given isbn13 and author. We call this query like this

```
import { trpc } from 'utils/trpc'

const exampleQuery = trpc.useQuery(["router.example", $input$]);
```

You could have a different flow of adding chapters. Like

```
  .mutation("add-chapter", {
    input: z.object({
      book: z.object({
        title: z.string(),
        subtitle: z.string().nullish(),
        isbn13: z.string(),
        price: z.string(),
        image: z.string(),
      }),
      payload: z.object({
        title: z.string(),
        text: z.string(),
      }),
    }),
    async resolve({ ctx, input }) {
      const bookNote = await ctx.prisma.bookNote.upsert({
        where: {
          id: input.book.id 
        },
        update: {},
        data: {
          ...input.book,
          authorId: ctx.user?.id || "",
        },
      });

      const chapter = await ctx.prisma.chapter.create({
        data: {
          ...input.payload,
          bookNoteId: input.book.id,
        },
      });

      return chapter;
    },
  })
```

This option uses `upsert`, which can be used as find or create. We're saying to find a book or create it otherwise, which ensures that, when we're adding a chapter, there will be a record with **id** of `input.book.id`. Your choice, I'll go with the first one 😀.

## Update a chapter
Definition of function for updating a chapter is pretty straightforward, when adding a chapter, we were creating a record, which is C out of CRUD. When updating, you guessed it, we'll use the update

```
  .mutation("update-chapter", {
    input: z.object({
      chapterID: z.string(),
      payload: z.object({
        title: z.string(),
        text: z.string(),
      }),
    }),
    async resolve({ ctx, input }) {
      const chapter = await ctx.prisma.chapter.update({
        data: {
          ...input.payload,
        },
        where: {
          id: input.chapterID,
        },
      });

      return chapter;
    },
  })
```

## Delete a chapter
And at the end, we're deleting (literally calling delete).
```
  .mutation("remove-chapter", {
    input: z.object({
      chapterID: z.string(),
    }),
    async resolve({ ctx, input }) {
      await ctx.prisma.chapter.delete({
        where: {
          id: input.chapterID,
        },
      });

      return true;
    },
  })
```

All of the mutations and query can be called the way you want, or the way it suits you best.

## Conclusion
I know, that was intense! But, you've made it! And know you know how to define CRUD functions with tRPC. I hope you can see just how simple it can be.

Thanks for reading, and keep an eye out for the next part. Till then take care 😉