Skip to content

Instantly share code, notes, and snippets.

@mkcode
Last active June 18, 2024 05:15
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);
@oxillix
Copy link

oxillix commented Dec 22, 2023

This seems very insecure

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

@acatzk
Copy link

acatzk commented Dec 26, 2023

Oh my friend. You solve my problem. Thank you so much!

@ninja-1337
Copy link

Oh my friend. You solved my problem. Thank you so much!

Same here

@bgolubovic
Copy link

bgolubovic commented Jan 11, 2024

This seems very insecure

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

What should be used here?

I have this setup:
packages/api/src/trpc.ts

interface AuthContext {
  auth: SignedInAuthObject | SignedOutAuthObject;
}

/**
 * 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 createContextInner = async ({ auth }: AuthContext) => {
  return {
    auth,
    db,
  };
};

export const createContext = async (opts: CreateNextContextOptions) => {
  return await createContextInner({ auth: getAuth(opts.req) });
};

/**
 * 2. INITIALIZATION
 *
 * This is where the trpc api is initialized, connecting the context and
 * transformer
 */
const t = initTRPC.context<typeof createContext>().create({
  transformer: superjson,
  errorFormatter: ({ shape, error }) => ({
    ...shape,
    data: {
      ...shape.data,
      zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
    },
  }),
});

/**
 * Create a server-side caller
 * @see https://trpc.io/docs/server/server-side-calls
 */
export const createCallerFactory = t.createCallerFactory;

/**
 * 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 folder
 */

/**
 * This is how you create new routers and subrouters in your tRPC API
 * @see https://trpc.io/docs/router
 */
export const createTRPCRouter = t.router;

/**
 * Public (unauthed) 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;

/**
 * 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
 */

// check if the user is signed in, otherwise throw a UNAUTHORIZED CODE
const isAuthed = t.middleware(({ next, ctx }) => {
  if (!ctx.auth.userId) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  return next({
    ctx: {
      auth: ctx.auth,
    },
  });
});

// export this procedure to be used anywhere in your application
export const protectedProcedure = t.procedure.use(isAuthed);

And in the apps/nextjs/src/trpc/server..ts

/**
 * This wraps the `createContext` helper and provides the required context for the tRPC API when
 * handling a tRPC call from a React Server Component.
 */
const createContextRSC = cache(async () => {
  // not sure what to use here, I do not need Headers, or session
});
export const api = createCaller(createContextRSC);

packages/api/src/index.ts

const createCaller = createCallerFactory(appRouter);

packages/api/src/root.ts

import { authRouter } from "./router/auth";
import { postRouter } from "./router/post";
import { createTRPCRouter } from "./trpc";

export const appRouter = createTRPCRouter({
  auth: authRouter,
  post: postRouter,
});

// export type definition of API
export type AppRouter = typeof appRouter;

Thank you all!

@growupanand
Copy link

@mkcode , Thank you so much for this useful, it is working perfectly. I have only one issue, I am not able to build app. I am getting the below error while doing next build, I am not able to figure out how to fix it.

The inferred type of 'api' cannot be named without a reference to '../../../../packages/db/node_modules/@prisma/client/runtime/library'. This is likely not portable. A type annotation is necessary.ts(2742)

It comes when I am using db in creating context,

import { db } from "~/server/db";

https://gist.github.com/mkcode/a590d1c8f7b0a37b8299965de7f7e958#file-trpc-ts-L38

If I don't use any db then this error not showing, by the way, I am using Prisma as db client which is also on a separate package folder in my turbo repo

You can check my repo branch here - https://github.com/growupanand/ConvoForm/tree/145-add-trpc-for-backend

@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

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