I am a frontend dev and i learn backend and TRPC, I built a user registration procedure which looks like so
export const registration = publicProcedure
.input(RegistrationFormSchema)
.mutation(async ({ input: { email, password }, ctx }) => {
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
Failures.throwCustomError(
{ code: 'CONFLICT', message: `Email ${email} already exists` },
{ procedure: PROCEDURE_NAME },
);
}
try {
const hashedPassword = await hash(password, SALT_ROUNDS);
const user = await prisma.user.create({ data: { email, password: hashedPassword } });
console.log({ ctx });
return { success: true, data: user };
} catch (error) {
if (error instanceof Error) {
Failures.handleServerError(error, {
procedure: PROCEDURE_NAME,
metadata: { problem: 'Error while registering user.' },
});
}
}
});
in TRPC docs there is an authorization section https://trpc.io/docs/server/authorization but it only say how to spread the news about authorized user as far as i understand but context object does not expose a setHeader method or anything like that. How can i store the session in a cookie in standalone trpc adaper? Or am i thinking about this in a wrong way? :) Thanks
You can make use of tRPC's context.
tRPC by design is very versatile, it can work over different types of ways. It works by separating between procedures and its adapters. Procedures are the functions that you define in your appRouter
variable, and adapters are the ways of calling them.
In Next.js (especially with the new app router), we can make use of this separation to call the same procedure in the backend as a function and the frontend as an HTTP call. Get it?
We can use tRPC's createCallerFactory
to create a simple adapter that just calls the procedures:
// `t` is your initialized tRPC instance
// we'll discuss a bit more about createContext later on
const createContext = ...;
export const trpcBackend = t.createCallerFactory(createContext)(appRouter);
Next, we can use the variable trpcBackend
to call tRPC procedures as if they were regular functions. So, In Next.js we could write something like:
// this runs in a server, not in a client
export default async function HomePage() {
const posts = await trpcBackend.post.listPosts();
return <...>{posts.map(...)}</...>;
}
And that post.listPosts
procedure defined in your appRouter
gets called as if they were just regular async functions!
But we can also create a caller that maps HTTP requests by making use of trpc's fetchRequestHandler
. Suppose we have a route.ts
file in Next.js:
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: ...,
});
export { handler as GET, handler as POST };
Next.js will interpret the GET
and POST
exports to handle incoming requests respectively. So, if a GET
request comes in, it will call the exported GET
function (which will call the handler
anonymous function), and so does POST
.
In the first argument, Next.js also provides a NextRequest
object which exposes things like the body, headers, etc.
However, this won't create nice-looking HTTP endpoints (say, /post/listPost
), its a bit unstandard: /api/trpc/post.listPost?batch=1&input=*url-encoded json*
.
That's because it's meant to work with a special tRPC-flavoured client. In react, we could use the createTRPCReact
function to create a tRPC client that could work in a react client, it looks something like this (simplified):
"use client";
// this runs in the client
export default function PostList() {
const { data, error } = trpcFrontend.post.listPosts.useQuery();
if (error) return `Error: ${error.message}`;
if (!data) return "Loading...";
return <>{data.map(...)}</>;
}
Isn't it cool being able to call the same function in the backend and the frontend? That's the point of tRPC!
Let's delve a bit deeper under the hood. In the previous code, we were using trpcFrontend
to call tRPC functions in the react frontend right? What does it look like?
Well it looks something like this:
export const api = createTRPCReact<AppRouter>();
export function TRPCReactProvider(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
api.createClient({
links: [
loggerLink({
// ...
}),
httpBatchLink({
transformer: SuperJSON,
url: getBaseUrl() + "/api/trpc",
headers: () => {
const headers = new Headers();
headers.set("x-trpc-source", "nextjs-react");
return headers;
},
}),
],
}),
);
return (
<QueryClientProvider client={queryClient}>
<api.Provider client={trpcClient} queryClient={queryClient}>
{props.children}
</api.Provider>
</QueryClientProvider>
);
}
I'd like to focus on a specific part of the code, which is the links
argument of api.createClient
.
api.createClient({
links: [
loggerLink({ /* ... */ }),
httpBatchLink({
transformer: SuperJSON,
url: getBaseUrl() + "/api/trpc",
headers: () => {
const headers = new Headers();
headers.set("x-trpc-source", "nextjs-react");
return headers;
},
}),
],
}),
See that httpBatchLink
? It's a link provided by tRPC. Suspiciously, we need to specify the API endpoint url where a tRPC's fetchRequestAdapter
is running (recall that we have Next.js routing requests coming to /api/trpc
into tRPC's fetchRequestAdapter
). So, this must be the culprit that sends requests to that endpoint! Read more about links here.
httpBatchLink
is a terminating link. It is the last link to be called, and its return value is the return value of the trpcFrontend.post.listPost
call (which is an object containing { data, error }
and quite a lot of other stuff).
httpBatchLink
serves calls from the frontend (post.listPosts), to be transformed into HTTP requests into an endpoint where tRPC's fetchRequestAdapter
lives, where it will then call the real procedure, and respond back in the form of an HTTP response.
The diagram looks something like this:
┌─Frontend──────────────────────────────┐ ┌─Backend───────────────────────────────────────────────────────────────┐
│ ┌───────────────┐ ┌───────────────┐ │ │ ┌─────────────────────┐ ┌───────────┐ ┌────────────────────────┐ │
│ │ post.listPost ├──►│ httpBatchLink ├─┼─HTTP──┼─►│ fetchRequestHandler ├──►│ appRouter ├──►│ the listPost procedure │ │
│ └───────────────┘ └───────────────┘ │ │ └─────────────────────┘ └───────────┘ └────────────────────────┘ │
└───────────────────────────────────────┘ └───────────────────────────────────────────────────────────────────────┘
Okay, now let's get back to the original question: How can you put cookies inside tRPC procedures? The short answer is, we can make use of the createContext
function.
Context is a some kind of global variable or store that can be accessed by tRPC procedures. It can be used to store things like authentication data, and you can even put functions inside them! Read more about contexts.
A context is created by the adapter, which then uses that context to call a tRPC procedure.
Recall the previous code that creates a fetchRequestAdapter
to call tRPC procedures as HTTP endpoints:
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: ...,
});
export { handler as GET, handler as POST };
Here, I omitted createContext
a bit. createContext
is a function that gets called each request to build an object that will be the ctx
in your tRPC procedures. You can say it represents the current state which the tRPC procedure is in. But the cool part is that as you create a tRPC context, you are given the actual request object (this is only the case with fetchRequestHandler
).
Here's how it looks like:
const createContext = async ({
req,
}: {
req: NextRequest;
}) => {
return {
headers: req.headers,
db: (await import("~/drizzle/or/something")).db,
};
};
// ...
const handler = (req: NextRequest) =>
fetchRequestHandler({
// ...
createContext,
});
// ...
Because db
is returned inside the field of this object, we can access ctx.db
inside tRPC procedures to do db-related work like inserting, selecting, etc. Like so:
// ...
publicProcedure
.input(/* ... */)
.mutation(async ({ input, ctx }) => {
await ctx.db.insert(post).values({...});
// ...
})
// ...
We can also pass the request's headers into your tRPC procedures by passing the headers
field the value of req.headers
.
As seen from the link you shared, tRPC exposed a user
field into the context by using the given req
object to get its cookie headers, parse them in some way, and create a meaningful user
field:
export async function createContext({
req, /* <-- these args are provided by fetchRequestAdapter */
res,
}: trpcNext.CreateNextContextOptions) {
async function getUserFromHeader() {
// ! notice that it accesses `req.headers`
if (req.headers.authorization) {
const user = await decodeAndVerifyJwtToken(
req.headers.authorization.split(' ')[1],
);
return user;
}
return null;
}
const user = await getUserFromHeader();
return {
// pass `{ user }` where trpc procedures could use `ctx.user`
user,
};
}
Remember that context returned from createContext
is just a simple object. We can literally put anything within it, and that includes functions!
fetchRequestAdapter
specifically provided us with the resHeaders
variable (as an argument of createContext
) which we can use to build up headers for the response.
We can utilize this, and create a function which makes setting cookies a little easier. Here's the end code:
const createContext = async ({
req,
resHeaders,
}: {
req: NextRequest;
resHeaders: Headers,
}) => {
return {
headers: req.headers,
db: (await import("~/drizzle/or/something")).db,
setCookie(name, value, attributes) {
resHeaders.append(
"set-cookie",
`${name}=${value}${attributes ? `; ${attributes}` : ""}`,
);
},
};
};
We then can use the function we made inside trpc procedures similar to .db
:
publicProcedure
.input(/* ... */)
.mutation(async ({ input, ctx }) => {
ctx.setCookie("session", generateCookie());
// ...
})
You could extend this further by adding functions like getCookie
that makes use of the .headers
provided by the req
object.
And that's it! sorta..
This approach works with fetchRequestAdapter
, any other adapters may not work, and its your job to write an adapter for each of them that provides the same functionality.
Just a quick note, if you're using Next.js's RSC and using tRPC with createCallerFactory
, it's not possible to set cookies or headers in your createContext
. I don't know why, it just doesn't work.
So, if you want to set cookies, use a fetchRequestAdapter
instead, which will require you to use a route.ts
file.
If you do need to call tRPC procedures inside an RSC, you can stub the setCookie
function by throwing an error instead, because it doesn't make sense to set cookies on procedures that queries data.
I hope this helps in some way! :)