Skip to content

Commit

Permalink
feat: trpc api
Browse files Browse the repository at this point in the history
  • Loading branch information
ixahmedxi committed May 26, 2024
1 parent 35d6532 commit 27dd6a0
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 5 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
- [x] Playwright E2E tests.
- [x] github actions.
- [x] SEO (dub.co is a great reference) for example robots.ts file.
- [x] tRPC api with tanstack query.
- [ ] Shadcn UI.
- [ ] Slate/Plate.js editor.
- [ ] tRPC api with tanstack query.
- [ ] New & Refined README.md.
- [ ] Add a `CONTRIBUTING.md` file.

Expand All @@ -18,7 +18,6 @@
- [x] Neon Database / Drizzle orm.
- [x] Upstash redis
- [x] Clerk authentication.
- [ ] Arcjet.
- [ ] Resend & React email.
- [ ] Stripe payment.
- [ ] Sentry.
Expand Down
Binary file modified bun.lockb
Binary file not shown.
3 changes: 2 additions & 1 deletion cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ words:
- Shadcn
- Signin
- Signup
- sslmode
- superjson
- tailwindcss
- tanstack
- trpc
Expand All @@ -44,4 +46,3 @@ words:
- typecheck
- Uploadthing
- Upstash
- sslmode
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-slot": "^1.0.2",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.39.0",
"@trpc/client": "^11.0.0-rc.374",
"@trpc/react-query": "^11.0.0-rc.374",
"@trpc/server": "^11.0.0-rc.374",
"@upstash/redis": "^1.31.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand All @@ -72,6 +76,8 @@
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"server-only": "^0.0.1",
"superjson": "^2.2.1",
"tailwind-merge": "^2.3.0",
"zod": "^3.23.8"
},
Expand Down
6 changes: 5 additions & 1 deletion src/app/(dashboard)/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { api } from '@/lib/trpc/server';
import { SignOutButton } from '@clerk/nextjs';

/**
* This is the main page for the dashboard.
* @returns A Next.js RSC page component.
*/
export default function DashboardHome() {
export default async function DashboardHome() {
const { message } = await api.greeting.protected();

return (
<main>
<h1>Hello World</h1>
<SignOutButton />
<h1>{message}</h1>
</main>
);
}
29 changes: 29 additions & 0 deletions src/app/api/trpc/[trpc]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { NextRequest } from 'next/server';

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';

import { env } from '@/env';
import { appRouter } from '@/server';
import { createTRPCContext } from '@/server/trpc';

const createContext = async (req: NextRequest) => {
return createTRPCContext({
headers: req.headers,
});
};

const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createContext(req),
onError: ({ path, error }) => {
env.NODE_ENV === 'development' &&
console.error(
`❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`,
);
},
});

export { handler as GET, handler as POST };
3 changes: 2 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ThemeProvider } from 'next-themes';
import type { PropsWithChildren } from 'react';

import { constructMetadata } from '@/lib/utils';
import { TRPCReactProvider } from '@/lib/trpc/react';

export const metadata: Metadata = constructMetadata();

Expand All @@ -34,7 +35,7 @@ export default function RootLayout({ children }: PropsWithChildren) {
>
<body>
<ThemeProvider attribute="class" disableTransitionOnChange>
{children}
<TRPCReactProvider>{children}</TRPCReactProvider>
</ThemeProvider>
</body>
</html>
Expand Down
68 changes: 68 additions & 0 deletions src/lib/trpc/react.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';

import { useState } from 'react';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { loggerLink, unstable_httpBatchStreamLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server';
import superjson from 'superjson';

import { getBaseUrl } from '@/lib/utils';
import { type AppRouter } from '@/server';

const createQueryClient = () => new QueryClient();

let clientQueryClientSingleton: QueryClient | undefined = undefined;
const getQueryClient = () => {
if (typeof window === 'undefined') {
return createQueryClient();
}
return (clientQueryClientSingleton ??= createQueryClient());
};

export const api = createTRPCReact<AppRouter>();

export type RouterInputs = inferRouterInputs<AppRouter>;
export type RouterOutputs = inferRouterOutputs<AppRouter>;

/**
* The TRPC React Provider is a component that initializes trpc and react query.
* @param props Props for the TRPC React Provider.
* @param props.children The children of the TRPC React Provider, which would be
* the entire page/layout in most cases.
* @returns React Provider component that initializes trpc and react query
* integration for the client.
*/
export function TRPCReactProvider(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: 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>
);
}
18 changes: 18 additions & 0 deletions src/lib/trpc/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'server-only';

import { cache } from 'react';
import { headers } from 'next/headers';

import { createCaller } from '@/server';
import { createTRPCContext } from '@/server/trpc';

const createContext = cache(async () => {
const heads = new Headers(headers());
heads.set('x-trpc-source', 'rsc');

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

export const api = createCaller(createContext);
10 changes: 10 additions & 0 deletions src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { greetingRouter } from './routers/greeting';
import { createCallerFactory, createRouter } from './trpc';

export const appRouter = createRouter({
greeting: greetingRouter,
});

export type AppRouter = typeof appRouter;

export const createCaller = createCallerFactory(appRouter);
13 changes: 13 additions & 0 deletions src/server/routers/greeting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from 'zod';

import { createRouter, protectedProcedure } from '../trpc';

export const greetingRouter = createRouter({
protected: protectedProcedure
.input(z.object({ name: z.string().optional() }).optional())
.query(({ input, ctx }) => {
return {
message: `Hello, ${input?.name ?? ctx.session.firstName ?? ''}, I'm from the tRPC procedure!`,
};
}),
});
46 changes: 46 additions & 0 deletions src/server/trpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { currentUser } from '@clerk/nextjs/server';
import { initTRPC, TRPCError } from '@trpc/server';
import superjson from 'superjson';
import { ZodError } from 'zod';

import { db } from '@/db';

export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await currentUser();

return {
db,
session,
...opts,
};
};

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,
},
};
},
});

export const createCallerFactory = t.createCallerFactory;
export const createRouter = t.router;
export const publicProcedure = t.procedure;

export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}

return next({
ctx: {
session: { ...ctx.session },
},
});
});

0 comments on commit 27dd6a0

Please sign in to comment.