Skip to content

Instantly share code, notes, and snippets.

@mkcode
Last active June 23, 2024 14:46
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);
@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

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