Skip to content

Instantly share code, notes, and snippets.

@omer-os
Last active April 9, 2024 14:47
Show Gist options
  • Save omer-os/6addbd606902bf681da3dd58a284b115 to your computer and use it in GitHub Desktop.
Save omer-os/6addbd606902bf681da3dd58a284b115 to your computer and use it in GitHub Desktop.

Trpc and Graphql

In my journey to refine the tech stack for future projects, I discovered the T3 Stack, diving into its components and advantages. This venture builds on my experiences from a recent shift from GraphQL to tRPC in a project, which significantly enhanced my development process and simplified the project architecture. Here, I share reflections and lessons from this transition, emphasizing that while my views are shaped by personal experience, they are not definitive judgements but rather observations of the challenges and benefits encountered.

GraphQL: The Challenges

  • Complex Setup: The integration of GraphQL with TypeScript on the frontend requires a suite of additional tools — notably, codegen for translating GraphQL schemas to TypeScript types and urql (alongside its extensions) for handling mutations and queries. This not only complicates the initial setup but also burdens the development experience (DX) with the need for ongoing updates to types in response to schema modifications. The complexity extends beyond just frontend concerns; setting up and managing a GraphQL backend is arguably more intricate, dealing with schema definitions, resolvers, and the orchestration of a performant server. This multifaceted setup can significantly slow down development cycles across the entire stack. In stark contrast, tRPC offers an out-of-the-box solution for type generation, simplifying the development process by eliminating the need for these additional tools and providing a more streamlined experience for both frontend and backend development.

  • Overly Flexible Schema: While GraphQL's flexibility in schema design is beneficial, it often results in returned types that are overly complex, including unnecessary elements such as nodes and edges. This complexity can lead to difficulties in effectively using the data on the frontend.

    type ActivityConnection {
      edges: [ActivityEdge!]
      pageInfo: PageInfo!
    }
  • Schema Exploration and Query/Mutation Complexity: The process of exploring a large schema.graphql file, such as the one found in the Qasah dashboard project which spans 789 lines, can be daunting. Writing queries and mutations within this framework is not only time-consuming but often requires reliance on GraphQL extensions which are notoriously buggy and unreliable. This setup complicates the development process and detracts from type safety, making it less intuitive than it should be. here are some defninitions for qasah
    image why do i need to write those definitions as front end? its making my work harder and unmanageable.

  • Caching: The need for verbose configurations to implement caching strategies for each query and mutation in GraphQL contrasts sharply with tRPC's simpler and more direct approach. tRPC's design inherently reduces the need for such configurations, simplifying state management across the application. heres the file i have to write cache updates on whenever i make a mutation, example:

    Mutation: {
      setUserSelectedStaff: (
        result: SetUserSelectedStaffMutation,
        args: SetUserSelectedStaffMutationVariables,
        cache,
        info
      ) => {
        cache.updateQuery<GetMyUserQuery, GetMyUserQueryVariables>(
          { query: GetMyUserDocument },
          (data) => {
            if (data && data.getMyUser) {
              data.getMyUser.currentSelectedStaff =
                result.setUserSelectedStaff.staff;
              return data;
            } else {
              return null;
            }
          }
        );
      },
  • Writing Queries and Mutations: The task of writing and maintaining queries and mutations in GraphQL is significantly laborious. I spent a considerable amount of time crafting these, and the dependency on extensions that frequently fail to work as expected only exacerbates the issue. This aspect of GraphQL slowing down development velocity so much.

  • Developer Experience with Codegen: Utilizing tools like codegen for generating TypeScript types from GraphQL schemas introduces an additional layer of complexity and can make the development experience sluggish. The process of waiting for codegen to run every time the schema or queries are updated interrupts the development flow, creating unnecessary delays.

  • Backend Implementation Speed: Implementing even simple functionalities in GraphQL being time-consuming, with something as straightforward as a product API taking weeks to design and implement. This slowness is exacerbated by the frontend's reliance on the backend for any new query or mutation, often leading to lengthy waits for implementation. Conversely, tRPC's approachable codebase enables even frontend developers to understand and potentially add new queries or mutations quickly, significantly speeding up development cycles. i mean heres how you write an api route in trpc :

export const postRouter = createTRPCRouter({
 
  // publicProcedure means this procedure is accessible to anyone without auth
  hello: publicProcedure
    .input(z.object({ text: z.string() }))
    .query(({ ctx, input }) => {
      
      // ctx : get user session, details like name, email, id etc... and roles if you have one
      return {
        greeting: `Hello ${input.text}`,
      };
    }),

  // protectedProcedure means this procedure is accessible to anyone without auth
  getSecretMessage: protectedProcedure.query(() => {
    return "you can now see this secret message!";
  }),
});

tRPC: Enhancing Developer Experience

  • Automatic Type Generation: tRPC excels by generating TypeScript types automatically during the app-building process, negating the need for external tools like codegen. This seamless integration accelerates development and guarantees that types are always synchronized with the backend implementation, ensuring a smooth and consistent developer experience.

  • Unified Development Workflow: For teams with distinct frontend and backend developers working within the same codebase and environment, tRPC offers a unified workflow that drastically simplifies collaboration. Both sides can work more cohesively, as tRPC's architecture inherently promotes a shared understanding of the code, reducing the overhead typically associated with aligning frontend and backend expectations. This coherence is especially beneficial in agile development settings, where rapid iterations and close collaboration are key.

  • Simplified Schema Management: By reducing the complexity typically associated with schema management and usage, tRPC allows developers to focus more on feature development rather than wrestling with data structure intricacies. This leads to a more efficient development process, where adding or modifying features is streamlined, and less time is spent on boilerplate code.

  • Enhanced Collaboration and Speed: tRPC's approachable nature means frontend developers can easily understand backend logic and vice versa. This mutual understanding fosters a more collaborative environment where developers can swiftly add new queries or mutations as needed without waiting on their counterparts. As a result, feature development and bug fixes can be implemented faster, making the entire development cycle more efficient.


In my recent project, I embarked on a journey to find the ideal tech stack, prioritizing a seamless structure for frontend and backend within a unified codebase. My quest led me to the T3 Stack, a revelation I stumbled upon through a developer's comment on Reddit, highlighting significant speed enhancements post-adoption.

The T3 Stack, touted as "The best way to start a full-stack, typesafe Next.js app" create.t3.gg. It intrigued me with its comprehensive yet straightforward approach, promising an efficient development workflow.

Key Components of the T3 Stack:

  • Core Technologies: Built on the pillars of Next.js, TypeScript, and Tailwind CSS, offering a robust foundation for modern web development.

  • Authentication: NextAuth is integrated for authentication, providing a versatile and manageable solution. Its wide range of adapters and providers facilitates easy customization, aligning perfectly with various project requirements. More details can be explored at NextAuth.js documentation.

  • Route Handling with tRPC: Enhances backend route handling, ensuring typesafe APIs that are easy to consume and maintain.

  • UI Development with Shadcn UI: Offers a component library that streamlines UI development, ensuring consistency and reusability.

  • Database Management: The stack encourages the use of Prisma with SQL databases like PostgreSQL for its typesafe schema and relational data modeling. Conversely, while Firebase (a NoSQL option) offers initial ease of setup, it requires a deeper understanding of NoSQL database design to avoid future complexities. Prisma's compatibility with MongoDB is also noted for those seeking NoSQL benefits with ORM features.

This stack not only streamlined the development process but also fostered an environment where frontend and backend could coexist harmoniously within a single codebase, addressing my initial concerns of complexity and understanding.

folder structure

After extensive research and refinement of my frontend project structure, I've developed what I consider the optimal folder organization. This structure not only facilitates a clean separation of concerns but also enhances development efficiency. Here's a breakdown of the core directories within the src folder, which houses both frontend and backend code:

  1. app Directory: Utilizes Next.js's file-based routing, including route groups for organizing pages with similar layouts. This approach simplifies navigation within the codebase and aligns with the Next.js routing documentation. The structure is as follows:

    • /app/(auth)/: Contains authentication-related routes.
    • /app/(core)/: Groups core pages sharing the same main layout.
    • /app/api: Dedicated to API routes, primarily for tRPC and NextAuth.
  2. components Directory: Serves as the home for all React components, divided into custom and ui subdirectories:

    • ui: Houses reusable UI components like buttons, dialogs, and labels, many of which are generated using Shadcn UI.
    • custom: Contains components that leverage ui components for more specialized use cases. This includes subfolders like cards, footers, and navbars, each containing specific components (e.g., main-navbar.tsx, users-table.tsx). Additionally, screens subfolder features components for specific pages such as home-page.tsx and about-page.tsx, where data fetching is typically handled by parent components in the app folder.
  3. lib Directory: Encapsulates custom hooks, utility functions, and other reusable code pieces beneficial for both front and backend development.

  4. server Directory: Contains server-side files, including tRPC route handlers and NextAuth configuration, reinforcing the backend aspect of the project.

  5. styles Directory: Dedicated to CSS styles, ensuring a centralized location for styling concerns.

  6. trpc Directory: Includes essential files for integrating tRPC within the project:

    • react.tsx: Facilitates access to tRPC routes from client-side components.
    • server.ts: Enables server-side components to interact with tRPC routes.

This meticulously designed folder structure promotes code reusability, simplifies maintenance, and streamlines the development process by providing clear distinctions between different types of code and functionality within the project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment