Hikari

Example - Creating and editing posts using tRPC

Using tRPC in Your Application

1. Defining Routes and Mutations

In your server/api/routers/posts.ts, you define routes using createTRPCRouter. Each route can be a query or a mutation. For example, to create a post, you define a mutation like this:

import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { createClient } from "@/utils/supabase/server";
import { TRPCError } from "@trpc/server";
 
export const postsRouter = createTRPCRouter({
  create: protectedProcedure
    .input(z.object({ title: z.string().min(1), content: z.string().nullable() }))
    .mutation(async ({ input, ctx }) => {
      const { data, error } = await createClient()
        .from('posts')
        .insert({
          user_id: ctx.user.id,
          title: input.title,
          content: input.content,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        })
        .select()
        .single();
 
      if (error) {
        console.error("Error creating post:", error);
        throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: error.message });
      }
      return data;
    }),
});

If you have already noticed, we use a protectedProcedure in the above example. This is because we use the auth.session() middleware in the server/api/trpc/context.ts file. This is a simple check to ensure that the user is authenticated before they can create or edit a post.

If you are using any database operation that requires authentication, you will need to use the protectedProcedure wrapper.

To edit a post, you would use a similar mutation with the necessary input parameters:

  update: protectedProcedure
    .input(z.object({ id: z.number(), title: z.string().min(1), content: z.string().nullable() }))
    .mutation(async ({ input, ctx }) => {
      const { data, error } = await createClient()
        .from('posts')
        .update({ 
          title: input.title, 
          content: input.content, 
          updated_at: new Date().toISOString() 
        })
        .eq('id', input.id)
        .eq('user_id', ctx.user.id)
        .select()
        .single();
 
      if (error) {
        console.error("Error updating post:", error);
        throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: error.message });
      }
      return data;
    }),

And finally, to get all posts, you would use a query:

  getAll: protectedProcedure
    .query(async ({ ctx }) => {
      const { data, error } = await createClient()
        .from('posts')
        .select('*')
        .eq('user_id', ctx.user.id)
        .order('created_at', { ascending: false });
 
      if (error) {
        console.error("Error fetching posts:", error);
        throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: error.message });
      }
      
      return data ?? [];
    }),

The structure of building API's with tRPC is simple. If you want to see the rest of the code for the posts router, you can view it here.

2. Calling tRPC in the Client

In your client component, such as components/posts.tsx, you can call these routes using hooks provided by tRPC. For example, to fetch all posts, you would use:

import { useQuery } from "@tanstack/react-query";
import { trpc } from "@/utils/trpc";
 
// Getting the posts
 
const { data: posts, isLoading, error: fetchError } = api.posts.getAll.useQuery()
 
// Creating a post
const createPost = api.posts.create.useMutation({
  onSuccess: async () => {
    await utils.posts.getAll.invalidate()
    setNewPost({ title: '', content: '' })
    toast({
      title: "Success",
      description: "Post created successfully!",
    })
  },
})
 
// Updating a post
const updatePost = api.posts.update.useMutation({
  onSuccess: async () => {
    await utils.posts.getAll.invalidate()
    setEditingPost(null)
    toast({
      title: "Success",
      description: "Post updated successfully!",
    })
  },
})
 
// Deleting a post
const deletePost = api.posts.delete.useMutation({
  onSuccess: async () => {
    await utils.posts.getAll.invalidate()
    toast({
      title: "Success",
      description: "Post deleted successfully!",
    })
  },
})

3. Benefits of Using tRPC

tRPC streamlines the development process by allowing you to define your API routes and types in one place, reducing the need for boilerplate code. This leads to quicker development cycles and a more cohesive codebase compared to traditional REST APIs. With tRPC, you can easily manage your API calls and ensure type safety across your application.

Important Note

A new SQL file for the posts has been added. If anyone from a previous version of Hikari wants to try it locally, they need to run this SQL file to set up the necessary database structure.

On this page