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);
@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