T3 stack with app router and supabase

This page summarizes the projects mentioned and recommended in the original post on dev.to

Our great sponsors
  • SurveyJS - Open-Source JSON Form Builder to Create Dynamic Forms Right in Your App
  • WorkOS - The modern identity platform for B2B SaaS
  • InfluxDB - Power Real-Time Data Analytics at Scale
  • t3-blog

  • // app/(main)/profile/new/page.tsx "use client"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { useFieldArray, useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { newProfileSchema } from "@/lib/validators/newProfile"; import { TrashIcon } from "@heroicons/react/24/outline"; import { useRouter } from "next/navigation"; export type NewProfileInput = z.infer; export default function NewProfileForm() { const form = useForm({ resolver: zodResolver(newProfileSchema), defaultValues: { firstName: "", lastName: "", role: undefined, skills: [{ name: "" }], bio: "", github: "", linkedin: "", website: "", }, }); const { fields, append, remove } = useFieldArray({ control: form.control, name: "skills", }); const watchSkills = form.watch("skills"); const router = useRouter(); const onSubmit = async (data: NewProfileInput) => { console.log(data); }; return ( Create Profile Yabba dabba doo ( First name )} /> ( Last name )} /> ( Role Full Stack Frontend Backend Design )} /> ( Skills {fields.map((field, index) => ( {fields.length > 1 && ( remove(index)} /> )} {form.formState.errors.skills?.[index]?.name && ( This can't be empty )} ))} !field.name)} onClick={() => append({ name: "" })} className="max-w-min" > Add Skill )} /> ( Bio )} />

    ( Github Username )} /> ( Linkedin Username )} />
    ( Website )} /> Submit ); }
    Enter fullscreen mode Exit fullscreen mode

    Context and Private Procedure

    Now we have the form but we need to push this data to the database when the user submits it. To do that we need to create a new trpc procedure. Before we actually write the procedure there are a few things we need to add.

    The first step is to add the userId to the trpc context. This will make things a lot quicker because it means we don't have to pass the userId from the client. For example if we want to get the current user's posts, instead of passing the current user id to the server, we can just call the function with no props and it already knows who the user is.

    Navigate to the server/api/trpc.ts file and import the supabase client:

    // server/api/trpc.ts
    
    ...
    import { createClient } from "@/utils/supabase/server";
    import { cookies } from "next/headers";
    ...
    
    Enter fullscreen mode Exit fullscreen mode

    Now inside the createTRPCContext function we need to pass the userId to the context like this:

    // server/api/trpc.ts
    
    ...
    export const createTRPCContext = async (opts: { headers: Headers }) => {
      const supabase = createClient(cookies());
    
      const {
        data: { user },
      } = await supabase.auth.getUser();
    
      return {
        user,
        db,
        ...opts,
      };
    };
    ...
    
    Enter fullscreen mode Exit fullscreen mode

    The next step is to create a new type of procedure called private procedure, which is a procedure that can only be called by authenticated users. This is helpful because it means that we know there is a userId for our queries. If we were to use a public procedure we would have to assert that we know the user is authenticated and handle that on the client which is a lot more complicated. To do this add the following code at the bottom of the file:

    // server/api/trpc.ts
    
    ...
    const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
      if (!ctx.user) {
        throw new TRPCError({
          code: "UNAUTHORIZED",
        });
      }
    
      return next({
        ctx: {
          user: ctx.user,
        },
      });
    });
    
    export const privateProcedure = t.procedure.use(enforceUserIsAuthed);
    ...
    
    Enter fullscreen mode Exit fullscreen mode

    Procedure

    Now we can get started writing the procedure. Go to server/api/routers and you should see a posts.ts file. Delete that file and create a new one called profiles.ts. Inside that file add the following code:

    // server/api/routers/profiles.ts
    
    import { z } from "zod";
    import { createTRPCRouter, privateProcedure } from "@/server/api/trpc";
    
    import { RoleType } from "@prisma/client";
    
    export const profileRouter = createTRPCRouter({
      create: privateProcedure
        .input(
          z.object({
            firstName: z.string(),
            lastName: z.string(),
            role: z.nativeEnum(RoleType),
            skills: z.array(z.string()),
            bio: z.string(),
            github: z.string(),
            linkedin: z.string(),
            website: z.union([z.literal(""), z.string().trim().url()]),
          }),
        )
        .mutation(async ({ ctx, input }) => {
          const user = await ctx.db.profile.create({
            data: {
              id: ctx.user.id,
              email: ctx.user.email!,
              firstName: input.firstName,
              lastName: input.lastName,
              role: input.role,
              skills: input.skills,
              bio: input.bio,
              github: input.github,
              linkedin: input.linkedin,
              website: input.website,
            },
          });
    
          return user;
        }),
    });
    
    Enter fullscreen mode Exit fullscreen mode

    Now we need to expose this router to the client. Go to server/api/root.ts and add the following code:

    // server/api/root.ts
    
    import { profileRouter } from "@/server/api/routers/profiles";
    import { createTRPCRouter } from "@/server/api/trpc";
    
    export const appRouter = createTRPCRouter({
      profiles: profileRouter,
    });
    
    export type AppRouter = typeof appRouter;
    
    Enter fullscreen mode Exit fullscreen mode

    Now we need to call the mutation on submit on form submission. Before we do that though, lets create a utility function to capitalize just the first letter of the string, so all of the names are uniform. In lib/utils.ts add the following function:

    // lib/utils.ts
    
    ...
    
    export function capitalizeFirstLetter(inputString: string) {
      const lowercaseString = inputString.toLowerCase();
    
      const capitalizedString =
        lowercaseString.charAt(0).toUpperCase() + lowercaseString.slice(1);
    
      return capitalizedString;
    }
    
    ...
    
    Enter fullscreen mode Exit fullscreen mode

    Now we can use our mutation. Add the following code to app/(main)/profile/new/page.tsx:

    // app/(main)/profile/new/page.tsx
    
    const router = useRouter();
    
    const { mutate } = api.profiles.create.useMutation({
      onSuccess: () => {
        router.push("/profile");
      },
      onError: (e) => {
        const errorMessage = e.data?.zodError?.fieldErrors.content;
        console.error("Error creating investment:", errorMessage);
      },
    });
    
    const onSubmit = async (data: NewProfileInput) => {
      const skillsList = data.skills.map((skill) => skill.name);
      mutate({
        firstName: capitalizeFirstLetter(data.firstName),
        lastName: capitalizeFirstLetter(data.lastName),
        role: data.role,
        skills: skillsList,
        bio: data.bio,
        github: data.github,
        linkedin: data.linkedin,
        website: data.website,
      });
    };
    
    Enter fullscreen mode Exit fullscreen mode

    Now when you submit the form you should be redirected to a profile page and when you check the table in supabase the new data should be there. Now that we have the data, let's make it so we can view it.

    Querying data

    Go back to server/api/routers/profiles.ts and add the following query below your mutation:

    // server/api/routers/profiles.ts
    
    ...
    getCurrent: privateProcedure.query(async ({ ctx }) => {
      const profile = await ctx.db.profile.findUnique({
        where: { id: ctx.user.id! },
      });
      return profile;
    }),
    ...
    
    Enter fullscreen mode Exit fullscreen mode

    Create a new page in the profile folder and add the following code:

    // app/(main)/profile/page.tsx
    
    import {
      Card,
      CardHeader,
      CardTitle,
      CardDescription,
      CardContent,
      CardFooter,
    } from "@/components/ui/card";
    import { capitalizeFirstLetter } from "@/lib/utils";
    import { api } from "@/trpc/server";
    
    export default async function ProfilePage() {
      const profile = await api.profiles.getCurrent.query();
    
      if (!profile) return null;
    
      return (
        
    {profile.firstName} {profile.firstName} {profile.email}

    Role: {capitalizeFirstLetter(profile.role)}

    Skills:

    {profile.skills.map((skill, index) => (

    {skill}

    ))}

    Bio: {profile.bio}

    Github: {profile.github}

    Linkedin: {profile.linkedin}

    Website: {profile.website}

    ); }
    Enter fullscreen mode Exit fullscreen mode

    This isn't styled very well, its just supposed to show how you can fetch the data on the server side with trpc. Notice that we are importing the api from the server folder not the react folder. This means we cant use hooks like usequery and usemutation because we are in a server component.

    Conclusion

    This app doesn't really do anything, it is just supposed to get you started where everything is configured properly to design a cool web application. You can add to your prisma schema and start doing more complicated queries and mutations with the same principles used for just a simple profile.

    All of the code for this project will be at this repo.

    Thanks for reading!

    P.S. If you are a software developer in college shoot me an email at [email protected]. I am going to start building a website for college students to find other developers in their area to build projects together. If you want to help me build it or use it or provide feedback or whatever I would love to chat.

  • taxonomy

    An open source application built using the new router, server components and everything new in Next.js 13.

  • I am building this app with inspiration from Taxonomy and Acme corp so a lot of the design comes from there.

  • SurveyJS

    Open-Source JSON Form Builder to Create Dynamic Forms Right in Your App. With SurveyJS form UI libraries, you can build and style forms in a fully-integrated drag & drop form builder, render them in your JS app, and store form submission data in any backend, inc. PHP, ASP.NET Core, and Node.js.

    SurveyJS logo
NOTE: The number of mentions on this list indicates mentions on common posts plus user suggested alternatives. Hence, a higher number means a more popular project.

Suggest a related project

Related posts