Skip to content

Instantly share code, notes, and snippets.

@mkcode
Last active November 7, 2024 23:29
Show Gist options
  • Save mkcode/a590d1c8f7b0a37b8299965de7f7e958 to your computer and use it in GitHub Desktop.
Save mkcode/a590d1c8f7b0a37b8299965de7f7e958 to your computer and use it in GitHub Desktop.
How to setup Next App Router + Clerk + TRPC
// TRPC API endpoint
// src/app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { type NextRequest } from "next/server";
import { env } from "~/env";
import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
import { getAuth } from "@clerk/nextjs/server";
/**
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
* handling a HTTP request (e.g. when you make requests from Client Components).
*/
const createContext = async (req: NextRequest) => {
return createTRPCContext({
headers: req.headers,
auth: getAuth(req),
});
};
const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: () => createContext(req),
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
});
export { handler as GET, handler as POST };
// TRPC Proxy for server side usage
// src/trpc/server.ts
import "server-only";
import {
createTRPCProxyClient,
loggerLink,
TRPCClientError,
} from "@trpc/client";
import { callProcedure } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { type TRPCErrorResponse } from "@trpc/server/rpc";
import { cookies, headers } from "next/headers";
import { NextRequest } from "next/server";
import { cache } from "react";
import { appRouter, type AppRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
import { transformer } from "./shared";
import { getAuth } from "@clerk/nextjs/server";
/**
* This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
* handling a tRPC call from a React Server Component.
*/
const createContext = cache(() => {
return createTRPCContext({
headers: new Headers({
cookie: cookies().toString(),
"x-trpc-source": "rsc",
}),
auth: getAuth(
new NextRequest("https://notused.com", { headers: headers() }),
),
});
});
export const api = createTRPCProxyClient<AppRouter>({
transformer,
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
/**
* Custom RSC link that lets us invoke procedures without using http requests. Since Server
* Components always run on the server, we can just call the procedure as a function.
*/
() =>
({ op }) =>
observable((observer) => {
createContext()
.then((ctx) => {
return callProcedure({
procedures: appRouter._def.procedures,
path: op.path,
rawInput: op.input,
ctx,
type: op.type,
});
})
.then((data) => {
observer.next({ result: { data } });
observer.complete();
})
.catch((cause: TRPCErrorResponse) => {
observer.error(TRPCClientError.from(cause));
});
}),
],
});
// TRPC server response
// src/server/api/trpc.ts
/**
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
* 1. You want to modify request context (see Part 1).
* 2. You want to create a new middleware or type of procedure (see Part 3).
*
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
* need to use are documented accordingly near the end.
*/
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
import { db } from "~/server/db";
import { type getAuth } from "@clerk/nextjs/server";
type AuthObject = ReturnType<typeof getAuth>;
/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API.
*
* These allow you to access things when processing a request, like the database, the session, etc.
*
* This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
* wrap this and provides the required context.
*
* @see https://trpc.io/docs/server/context
*/
export const createTRPCContext = async (opts: {
headers: Headers;
auth: AuthObject;
}) => {
return {
db,
userId: opts.auth.userId,
...opts,
};
};
/**
* 2. INITIALIZATION
*
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
* errors on the backend.
*/
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*
* These are the pieces you use to build your tRPC API. You should import these a lot in the
* "/src/server/api/routers" directory.
*/
/**
* This is how you create new routers and sub-routers in your tRPC API.
*
* @see https://trpc.io/docs/router
*/
export const createTRPCRouter = t.router;
/**
* Public (unauthenticated) procedure
*
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
* guarantee that a user querying is authorized, but you can still access user session data if they
* are logged in.
*/
export const publicProcedure = t.procedure;
/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
// Make ctx.userId non-nullable in protected procedures
return next({ ctx: { userId: ctx.userId } });
});
/**
* Protected (authenticated) procedure
*
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
* the session is valid and guarantees `ctx.session.user` is not null.
*
* @see https://trpc.io/docs/procedures
*/
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
@mkcode
Copy link
Author

mkcode commented Jan 30, 2024

@growupanand - It seems to me that the most likely reason for this would be that you forgot to run the build step to generate to prisma typescript definitions. This used to be in the old t3-turbo repo when it used prisma, but it was removed when they migrated to Drizzle b/c drizzle doesn't need a build step.

The issue seems mostly unrelated to the code I posted above, please ensure that you can import and call a method on db itself.

@growupanand
Copy link

@mkcode It is fixed now,

I follow this guide, In short, the solution is

Instead of using import { PrismaClient } from "@prisma/client";

We will use import { PrismaClient } from "../lib/generated/client";

This lib folder can be generated by adding the below line in your schema.prisma 's client section

output = "../lib/generated/client"

@acatzk
Copy link

acatzk commented Feb 6, 2024

@mkcode Do you have utils to invalidate a specific query?

Like this in the docs https://tanstack.com/query/v4/docs/framework/react/guides/query-keys

@acatzk
Copy link

acatzk commented Feb 7, 2024

@growupanand Thank you so much this code works.

But the docs in tRPC when handling the invalidate doesn't work

https://trpc.io/docs/client/react/useUtils

const utils = trpc.useUtils();

utils.post.all.invalidate();
utils.post.byId.invalidate({ id: input.id });

Your provided solution works

import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();

queryClient.invalidateQueries({
   queryKey: [["workspace", "getAll"]],
});

@growupanand
Copy link

How do you debug in vscode using this tRPC? It is not working for me, below is my launch.json,

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Next.js: debug server-side",
      "type": "node-terminal",
      "request": "launch",
      "command": "pnpm run dev"
    },
    {
      "name": "Next.js: debug client-side",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:3000"
    },
    {
      "name": "Next.js: debug full stack",
      "type": "node-terminal",
      "request": "launch",
      "command": "pnpm run dev",
      "serverReadyAction": {
        "pattern": "- Local:.+(https?://.+)",
        "uriFormat": "%s",
        "action": "debugWithChrome"
      }
    }
  ]
}

@alexander-densley
Copy link

Im using the new (still in beta but working) version of create t3 app that uses trpc 11 and tanstack 5, (link to pr here: t3-oss/create-t3-app#1741). I implemented everything you have but it's not working, i consistently get "unauthorized - not signed in",

any tips on adapting what you did to the updated version?

@mkcode
Copy link
Author

mkcode commented Feb 20, 2024

@alexander-densley - I won't have time to look into this for at least another week. It seems likely that the issue would be somewhere in this file: https://github.com/t3-oss/create-t3-app/pull/1741/files#diff-9e58d835a446da0f5eac0b0cddedfb036c45bef6b184fdfb09f910ae465928e7

Where we would need the get the getAuth function into the new createCaller method somehow.

Please share with us any progress you can make on this!

@growupanand
Copy link

Im using the new (still in beta but working) version of create t3 app that uses trpc 11 and tanstack 5, (link to pr here: t3-oss/create-t3-app#1741). I implemented everything you have but it's not working, i consistently get "unauthorized - not signed in",

any tips on adapting what you did to the updated version?

@alexander-densley I recently upgraded tRPC 11 and tanstack 5 in my Project convoform, you can see this commit - growupanand/ConvoForm@131c0a7

@wardch
Copy link

wardch commented Feb 27, 2024

Thank you so much mkcode, you've helped me out big time!

@bmarianome
Copy link

Thank you!

@reecejohnson
Copy link

Thank you @mkcode you are a living legend ❤️

@raoufBen02
Copy link

thank you @mkcode saved me a lot of headaches

@vegetablesalad
Copy link

Thank you. This solved hours of searching and trying.

@waqas-on-github
Copy link

waqas-on-github commented Sep 30, 2024

Screenshot from 2024-09-30 21-05-47
got this "Object literal may only specify known properties, and 'auth' does not exist in type '{ headers: Headers; }'.ts(2353)"

@FelipeBelloDultra
Copy link

Is this solution "production-ready" @mkcode?
I'm asking because I'm not sure about this part.

    auth: getAuth(
      new NextRequest("https://notused.com", { headers: headers() }),
    ),

@thekojopatrick
Copy link

Is this solution "production-ready" @mkcode? I'm asking because I'm not sure about this part.

    auth: getAuth(
      new NextRequest("https://notused.com", { headers: headers() }),
    ),

Have you been able to find alternate solution to this ?

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