Skip to content

Commit

Permalink
feat: started adding trpc
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbrusegard committed Aug 31, 2024
1 parent db95375 commit 53d74aa
Show file tree
Hide file tree
Showing 12 changed files with 337 additions and 10 deletions.
Binary file modified bun.lockb
Binary file not shown.
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.1",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.45.1",
"@trpc/client": "^10.45.2",
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"@tanstack/react-query": "^5.53.1",
"@trpc/client": "^11.0.0-rc.490",
"@trpc/react-query": "^11.0.0-rc.490",
"@trpc/server": "^11.0.0-rc.490",
"autoprefixer": "^10.4.19",
"client-only": "^0.0.1",
"cmdk": "1.0.0",
"country-flag-icons": "^1.5.12",
"cva": "^1.0.0-beta.1",
Expand All @@ -52,6 +53,7 @@
"reading-time": "^1.5.0",
"server-only": "^0.0.1",
"sharp": "^0.33.4",
"superjson": "^2.2.1",
"tailwind-merge": "^2.5.2",
"zod": "^3.23.8"
},
Expand Down
33 changes: 33 additions & 0 deletions src/app/api/data/[trpc]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { env } from '@/env';
import { appRouter } from '@/server/api/root';
import { createTRPCContext } from '@/server/api/trpc';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import type { NextRequest } from 'next/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 (request: NextRequest) => {
return createTRPCContext({
headers: request.headers,
});
};

const handler = (request: NextRequest) =>
fetchRequestHandler({
endpoint: '/api/data',
req: request,
router: appRouter,
createContext: () => createContext(request),
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 };
5 changes: 4 additions & 1 deletion src/components/providers/RootProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IntlErrorProvider } from '@/components/providers/IntlErrorProvider';
import { TRPCProvider } from '@/components/providers/TRPCProvider';
import { ThemeProvider } from '@/components/providers/ThemeProvider';

type RootProvidersProps = {
Expand All @@ -9,7 +10,9 @@ type RootProvidersProps = {
function RootProviders({ children, locale }: RootProvidersProps) {
return (
<ThemeProvider>
<IntlErrorProvider locale={locale}>{children}</IntlErrorProvider>
<TRPCProvider>
<IntlErrorProvider locale={locale}>{children}</IntlErrorProvider>
</TRPCProvider>
</ThemeProvider>
);
}
Expand Down
57 changes: 57 additions & 0 deletions src/components/providers/TRPCProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';

import { env } from '@/env';
import { api } from '@/lib/api/client';
import { createQueryClient } from '@/lib/api/queryClient';
import { type QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { loggerLink, unstable_httpBatchStreamLink } from '@trpc/client';
import { useState } from 'react';
import SuperJSON from 'superjson';

let clientQueryClientSingleton: QueryClient | undefined = undefined;
const getQueryClient = () => {
if (typeof window === 'undefined') {
// Server: always make a new query client
return createQueryClient();
}
// Browser: use singleton pattern to keep the same query client
if (!clientQueryClientSingleton) {
clientQueryClientSingleton = createQueryClient();
}
return clientQueryClientSingleton;
};

function TRPCProvider(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();

const [trpcClient] = useState(() =>
api.createClient({
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === 'development' ||
(op.direction === 'down' && op.result instanceof Error),
}),
unstable_httpBatchStreamLink({
transformer: SuperJSON,
url: `${env.SITE_URL}/api/data`,
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>
);
}

export { TRPCProvider };
10 changes: 5 additions & 5 deletions src/components/providers/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
'use client';

import { ThemeProvider } from 'next-themes';
import { ThemeProvider as NextThemeProvider } from 'next-themes';

function NextThemeProvider({ children }: { children: React.ReactNode }) {
function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
<NextThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</NextThemeProvider>
);
}

export { NextThemeProvider as ThemeProvider };
export { ThemeProvider };
23 changes: 23 additions & 0 deletions src/lib/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import type { AppRouter } from '@/server/api/root';
import { createTRPCReact } from '@trpc/react-query';
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';

const api = createTRPCReact<AppRouter>();

/**
* Inference helper for inputs.
*
* @example type HelloInput = RouterInputs['example']['hello']
*/
type RouterInputs = inferRouterInputs<AppRouter>;

/**
* Inference helper for outputs.
*
* @example type HelloOutput = RouterOutputs['example']['hello']
*/
type RouterOutputs = inferRouterOutputs<AppRouter>;

export { api, type RouterInputs, type RouterOutputs };
28 changes: 28 additions & 0 deletions src/lib/api/queryClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
QueryClient,
defaultShouldDehydrateQuery,
} from '@tanstack/react-query';
import SuperJSON from 'superjson';

function createQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 30 * 1000,
},
dehydrate: {
serializeData: SuperJSON.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
},
hydrate: {
deserializeData: SuperJSON.deserialize,
},
},
});
}

export { createQueryClient };
31 changes: 31 additions & 0 deletions src/lib/api/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'server-only';

import { createQueryClient } from '@/lib/api/queryClient';
import { type AppRouter, createCaller } from '@/server/api/root';
import { createTRPCContext } from '@/server/api/trpc';
import { createHydrationHelpers } from '@trpc/react-query/rsc';
import { headers } from 'next/headers';
import { cache } from 'react';

/**
* 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(() => {
const heads = new Headers(headers());
heads.set('x-trpc-source', 'rsc');

return createTRPCContext({
headers: heads,
});
});

const getQueryClient = cache(createQueryClient);
const caller = createCaller(createContext);

const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(
caller,
getQueryClient,
);

export { api, HydrateClient };
25 changes: 25 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { testRouter } from '@/server/api/routers/test';
import { createCallerFactory, createTRPCRouter } from '@/server/api/trpc';

/**
* This is the primary router for your server.
*
* All routers added in /api/routers should be manually added here.
*/
const appRouter = createTRPCRouter({
test: testRouter,
});

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

/**
* Create a server-side caller for the tRPC API.
* @example
* const trpc = createCaller(createContext);
* const res = await trpc.post.all();
* ^? Post[]
*/
const createCaller = createCallerFactory(appRouter);

export { appRouter, createCaller, type AppRouter };
14 changes: 14 additions & 0 deletions src/server/api/routers/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createTRPCRouter, publicProcedure } from '@/server/api/trpc';
import { posts } from '@/server/db/schema';

const testRouter = createTRPCRouter({
getLatest: publicProcedure.query(async ({ ctx }) => {
const post = await ctx.db.query.posts.findFirst({
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
});

return post ?? null;
}),
});

export { testRouter };
111 changes: 111 additions & 0 deletions src/server/api/trpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { auth } from '@/server/auth';
import { db } from '@/server/db';
import { s3 } from '@/server/s3';
/**
* 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 } from '@trpc/server';
import superjson from 'superjson';
import { ZodError } from 'zod';

/**
* 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 }) => {
return {
auth,
db,
s3,
...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,
},
};
},
});

/**
* Create a server-side caller.
*
* @see https://trpc.io/docs/server/server-side-calls
*/
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" directory.
*/

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

/**
* Middleware for timing procedure execution and adding an artificial delay in development.
*
* You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
* network latency that would occur in production but not in local development.
*/
const timingMiddleware = t.middleware(async ({ next, path }) => {
const start = Date.now();

if (t._config.isDev) {
// artificial delay in dev
const waitMs = Math.floor(Math.random() * 400) + 100;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}

const result = await next();

const end = Date.now();
console.log(`[TRPC] ${path} took ${end - start}ms to execute`);

return result;
});

/**
* 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.
*/
const publicProcedure = t.procedure.use(timingMiddleware);

export { createTRPCRouter, createCallerFactory, publicProcedure };

0 comments on commit 53d74aa

Please sign in to comment.