Turborepo

Estoy examinando cómo funciona create-t3-turbo para tratar de aprender cómo funciona pnpm y Turborepo

Una de las cosas que he visto es que en pnpm-workspaces.yaml no solo declaran las carpetas en las que están todos los paquetes, sino que además, crean catalogs para establecer de manera global las versiones a utilizar para diferentes paquetes.

packages:
  - "apps/*"
  - "packages/*"
  - "tooling/*"
 
catalog:
  # Dev tooling
  eslint: ^9.25.1
  prettier: ^3.5.3
  tailwindcss: ^3.4.15
  typescript: ^5.8.3
 
  # Misc
  zod: ^3.24.2
 
  # Tanstack & tRPC
  "@tanstack/react-query": ^5.69.0
  "@trpc/client": ^11.1.0
  "@trpc/tanstack-react-query": ^11.1.0
  "@trpc/server": ^11.1.0

De esta manera podemos declarar en el package.json de cualquiera de nuestros paquetes:

"dependencies": {
    "@trpc/client": "catalog:",
    "@trpc/server": "catalog:",

Para utilizar las versiones de esos dos paquetes declaradas dentro de catalog:

pnpm 10 requerido

Para que catalogs funcione correctamente es necesario declarar en el package.json raíz del repo que vamos a utilizar al menos la versión 10 de pnpm

"packageManager": "[email protected]",

TRPC

Integrando TRPC como un package dentro de un monorepo.

He seguido el quickstart de la documentación oficial

package.json

{
  "name": "@repo/api",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./src/index.ts"
    }
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "lint": "biome lint",
    "lint:fix": "biome lint --write"
  },
  "dependencies": {
    "@trpc/client": "catalog:",
    "@trpc/server": "catalog:",
    "superjson": "^2.2.2"
  },
  "devDependencies": {
    "@biomejs/biome": "catalog:",
    "@repo/typescript-config": "workspace:*",
    "typescript": "catalog:"
  }
}

index.ts - Inicializamos el TRPC Router, pasándole un objeto con los procedimientos, y lo exportamos junto con su type.

import { peopleRouter } from './router/people.js';
import { createTRPCRouter } from './trpc.js';
 
export const appRouter = createTRPCRouter({
  people: peopleRouter
});
 
// Export type router type signature,
// NOT the router itself.
export 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[]
 */
export const createCaller = createCallerFactory(appRouter);

trpc.ts - Inicializamos TRPC y exportamos:

  • t.router: Método para instanciar el router
  • t.procedure: Método para instanciar procesos de TRPC
import { initTRPC } from '@trpc/server';
 
/**
 * Initialization of tRPC backend
 * Should be done only once per backend!
 */
const t = initTRPC.create();
 
/**
 * Middleware for timing procedure execution and adding an articifial delay in development.
 */
const timingMiddleware = t.middleware(async ({path, next}) => {
    const start = Date.now();
    if (t._config.isDev) {
        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;
})
 
/**
 * Create a server-side caller
 * @see https://trpc.io/docs/server/server-side-calls
 */
export const createCallerFactory = t.createCallerFactory;
 
/**
 * Export reusable router and procedure helpers
 * that can be used throughout the router
 */
export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;

/router/people - Creamos el peopleRouter que incluimos más arriba en appRouter

import { TRPCRouterRecord } from "@trpc/server";
import { publicProcedure } from "../trpc.js";
 
export const peopleRouter = {
    list: publicProcedure.query(async () => [
      {
        name: 'John Doe',
      },
      {
        name: 'Jane Doe',
      },
      {
        name: 'John Smith',
      },
      {
        name: 'Jane Smith',
      },
    ]),
  } satisfies TRPCRouterRecord

Y en la aplicación que consume la api:

Paquetes necesarios:

  • @trpc/server
  • @trpc/client
  • @trpc/tanstack-react-query
  • @tanstack/react-query

Integración con Tanstack Query y llamadas Client Side

/trpc/react.ts - Exportamos el provider y un hook de TRPC desde createTRPCContext. Le pasamos como genérico el type de nuestro appRouter para que Typescript conozca nuestra API.

import { createTRPCContext } from '@trpc/tanstack-react-query'
import type { AppRouter } from '@repo/api'
 
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>()

root-provider.tsx - Integramos tRPC con Tanstack Query

import { QueryClient } from '@tanstack/react-query'
import superjson from 'superjson'
import { createTRPCClient, httpBatchStreamLink } from '@trpc/client'
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'
 
import { TRPCProvider } from '@/integrations/trpc/react'
import type { AppRouter } from '@repo/api'
 
 
function getUrl() {
  const base = (() => {
    if (typeof window !== 'undefined') return ''
    return `http://localhost:${process.env.PORT ?? 3000}`
  })()
  return `${base}/api/trpc`
}
 
export const trpcClient = createTRPCClient<AppRouter>({
  links: [
    httpBatchStreamLink({
      url: getUrl(),
    }),
  ],
})
 
const queryClient = new QueryClient({
  defaultOptions: {
    dehydrate: { serializeData: superjson.serialize },
    hydrate: { deserializeData: superjson.deserialize },
  },
})
 
const serverHelpers = createTRPCOptionsProxy({
  client: trpcClient,
  queryClient: queryClient,
})
 
export function getContext() {
  return {
    queryClient,
    trpc: serverHelpers,
  }
}
 
export function Provider({ children }: { children: React.ReactNode }) {
  return (
    <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
      {children}
    </TRPCProvider>
  )
}
 

Llamadas Server Side

Docs

Para llamar a procedimientos TRPC desde el servidor debemos usar createCallerFactory, una función que nos permite crear un caller server-side para el router que le pasemos como argumento.

En este caso es necesario crearlo en como hemos hecho más arriba entrpc.ts

export const createCallerFactory = t.createCallerFactory;

Y luego exportamos el caller ya instanciado desde ìndex.ts

const createCaller = createCallerFactory(appRouter);

Y después desde la aplicación que consume, usamos el Factory Pattern para instanciar el caller y le pasamos el router como argumento.

En NextJS podríamos hacerlo así dentro de un RSC

  const trpc = createCaller({})
  const people = await trpc.people.list()
})