# State Management Standards for tRPC
This document outlines the best practices for managing state in applications using tRPC. It covers strategies for data flow, reactivity, and efficient state management, with a focus on how these principles apply specifically within the tRPC ecosystem.
## 1. Architecture and Principles
### 1.1. Centralized vs. Decentralized State
**Standard:** Prefer a centralized state management solution for global application state, but consider decentralized, component-level state for isolated UI elements.
* **Do This:** Use libraries like Zustand, Jotai, or Redux for managing application-wide data accessed by multiple components.
* **Don't Do This:** Avoid prop drilling large amounts of data through the component tree. Don't tightly couple individual components with direct data dependencies.
* **Why:** Centralized state enhances predictability, simplifies debugging, and facilitates data consistency. Decentralized state improves component isolation and reusability.
**Example (Centralized State with Zustand):**
"""typescript
import { create } from 'zustand';
interface UserState {
user: { id: string; name: string } | null;
setUser: (user: { id: string; name: string } | null) => void;
}
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
export default useUserStore;
// In a tRPC procedure:
import { publicProcedure, router } from './trpc';
import { z } from 'zod';
export const appRouter = router({
getUser: publicProcedure
.input(z.object({ userId: z.string() }))
.query(async ({ input }) => {
// Simulate fetching user from DB
const user = { id: input.userId, name: "Example User" };
return user; // Return data to the client
// This data can later be stored in Zustand
}),
});
// Usage in a React Component:
import useUserStore from './userStore';
import { trpc } from './utils/trpc'; // Ensure this references your tRPC client
function UserProfile({ userId }: { userId: string }) {
const { user, setUser } = useUserStore();
const { data, isLoading } = trpc.getUser.useQuery({ userId: userId }, {
onSuccess: (data) => {
setUser(data); // Update Zustand state with tRPC response
},
});
if (isLoading) return <p>Loading...</p>;
if (!user) return <p>No user found</p>;
return (
{user.name}
<p>ID: {user.id}</p>
);
}
export default UserProfile;
"""
### 1.2. Data Flow with tRPC
**Standard:** Design data flows originating from tRPC procedures, updating the client-side state Reactively.
* **Do This:** Use "useQuery" hooks from "@trpc/react-query" to fetch data, and upon successful retrieval, update your state management solution. Trigger mutations via "useMutation" to modify data, and invalidate queries as needed to refresh the cache.
* **Don't Do This:** Directly manipulate server-side data from client-side components. Avoid mixing tRPC calls directly in complex component logic, keep them separate.
* **Why:** tRPC ensures type safety and prevents inconsistencies between server and client. Leveraging "useQuery" and "useMutation" promotes client-side reactivity upon data changes, and it makes invalidating queries when mutations occur much easier.
**Example (Data Flow with tRPC and "useMutation"):**
"""typescript
// tRPC Router (server)
import { publicProcedure, router } from './trpc';
import { z } from 'zod';
export const appRouter = router({
updateUserName: publicProcedure
.input(z.object({ userId: z.string(), newName: z.string() }))
.mutation(async ({ input }) => {
// In a real app, you'd update a database here.
console.log("Updating user ${input.userId} name to ${input.newName}");
return {userId: input.userId, newName: input.newName}; // Simulate a DB update result
}),
});
// React Component (client)
import { trpc } from './utils/trpc'; // Ensure this references your tRPC client
function UpdateUserName({ userId }: { userId: string }) {
const { mutate: updateName, isLoading, isError } = trpc.updateUserName.useMutation({
onSuccess: (data) => {
// Invalidate the 'getUser' query to refetch fresh data.
trpc.getUser.invalidate({ userId: data.userId });
},
});
const [newName, setNewName] = React.useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
updateName({ userId: userId, newName: newName });
};
if (isLoading) return <p>Updating...</p>;
if (isError) return <p>Error updating name.</p>;
return (
setNewName(e.target.value)}
placeholder="New Name"
/>
Update Name
);
}
export default UpdateUserName;
"""
### 1.3. Optimistic Updates
**Standard:** Implement optimistic updates to provide a responsive user experience, but ensure proper error handling to revert changes when necessary.
* **Do This:** Temporarily update the UI state with the anticipated outcome of a mutation. In the "onMutate" callback of "useMutation", store the previous query data, and revert to it if the mutation fails.
* **Don't Do This:** Implement optimistic updates without handling potential errors or inconsistencies.
* **Why:** Optimistic updates make applications feel faster from the user's perspective. Reverting on error maintains data integrity.
**Example (Optimistic Updates with "useMutation"):**
"""typescript
// React Component (client)
import { trpc } from './utils/trpc'; // Ensure this references your tRPC client
function UpdateLikeCount({ postId }: { postId: string }) {
const utils = trpc.useContext(); // Use trpc context
const { mutate: addLike, isLoading } = trpc.addLike.useMutation({
onMutate: async (variables) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await utils.cancelQueries(['post.getPost', { postId: variables.postId }]);
// Snapshot the previous value
const previousPost = utils.getQueryData(['post.getPost', { postId: variables.postId }]);
// Optimistically update to the new value
utils.setQueryData(['post.getPost', { postId: variables.postId }], (old: any) => {
if (!old) return old;
return { ...old, likes: old.likes + 1 }; // Assuming "likes" is a number
});
// Return a context object with the snapshotted value
return { previousPost };
},
onError: (_err, variables, context) => {
// If the mutation fails, roll back to the previous value
utils.setQueryData(['post.getPost', { postId: variables.postId }], (context as any)?.previousPost);
},
onSettled: (data, error, variables) => {
// Always refetch query after error or success:
utils.invalidateQueries(['post.getPost', { postId: variables.postId }]);
},
});
return (
addLike({ postId: postId })} disabled={isLoading}>
Like
);
}
"""
## 2. Reactivity and Caching
### 2.1. React Query Integration
**Standard:** Leverage React Query's caching and invalidation mechanisms integrated with tRPC for efficient data fetching and state synchronization.
* **Do This:** Use "useQuery" to fetch data and automatically cache responses. Use "invalidateQueries" after mutations to automatically refetch invalidated data, keeping the state current. Configure "staleTime" and "cacheTime" appropriately for different data types.
* **Don't Do This:** Neglect React Query's caching features which can lead to unnecessary API requests. Manually manage cache invalidation.
* **Why:** React Query simplifies data management, eliminates boilerplate code, and optimizes performance by caching API responses.
**Example (Caching Configuration):**
"""typescript
// Wrap your whole app in a QueryClientProvider
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
cacheTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false,
retry: false,
},
},
})
function App() {
return (
{/* your app */}
)
}
"""
### 2.2. Selective Query Invalidation
**Standard:** Invalidate only the specific queries affected by a mutation to avoid unnecessary data refetching.
* **Do This:** Use "trpc.invalidateQueries" with specific query keys after a mutation completes.
* **Don't Do This:** Invalidate all queries globally after every mutation, as this will cause performance issues for larger applications.
* **Why:** Selective invalidation minimizes server load and improves application responsiveness.
**Example (Specific Query Invalidation):**
"""typescript
// React Component (client)
import { trpc } from './utils/trpc'; // Ensure this references your tRPC client
function DeletePost({ postId }: { postId: string }) {
const utils = trpc.useContext();
const { mutate: deletePost, isLoading, isError } = trpc.deletePost.useMutation({
onSuccess: () => {
// Invalidate the posts list query to refresh the list
utils.post.getAllPosts.invalidate();
// Optionally invalidate a specific post query as well
utils.post.getPost.invalidate({ postId: postId });
},
});
if (isLoading) return <p>Deleting...</p>;
if (isError) return <p>Error deleting the post.</p>;
return (
deletePost({ postId: postId })}>Delete
);
}
"""
### 2.3. Prefetching
**Standard:** Use prefetching strategically to load data before it's needed, improving the user experience.
* **Do This:** Use React Query's "queryClient.prefetchQuery" to load data for routes the user is likely to visit soon.
* **Don't Do This:** Prefetch too much data upfront since this can degrade initial load times.
* **Why:** Prefetching can make transitions between pages or components feel instantaneous.
**Example (Prefetching in next.js "getServerSideProps"):**
"""typescript
// pages/posts/[id].tsx
import { GetServerSideProps } from 'next';
import { trpc } from '../../utils/trpc';
import { appRouter } from '../../server/routers/_app';
import { createTRPCContext } from '../../server/trpc';
export const getServerSideProps: GetServerSideProps = async (context) => {
const postId = context.params?.id as string;
const trpcContext = await createTRPCContext({ req: context.req, res: context.res });
const caller = appRouter.createCaller(trpcContext);
// Prefetch the post data
await trpc.post.getPost.prefetch({ postId: postId });
return {
props: {
trpcState: await trpc.getAllPosts.ssr(),
},
};
};
function PostPage({ postId }: { postId: string }) {
const { data, isLoading } = trpc.post.getPost.useQuery({ postId: postId });
if (isLoading) return <p>Loading...</p>;
if (!data) return <p>Post not found</p>;
return (
{data.title}
<p>{data.content}</p>
);
}
export default PostPage;
"""
## 3. Implementation Details
### 3.1. Avoiding Common Pitfalls
**Standard:** Steer clear of common mistakes when integrating state management with tRPC.
* **Don't Do This:**
* Over-fetching or under-fetching data with poorly defined queries.
* Ignoring error states from "useQuery" and "useMutation".
* Creating race conditions when updating state after a mutation.
* Directly mutating the query cache without using provided methods (e.g., "setQueryData"). This can lead to unexpected behaviours.
* Deeply nesting tRPC calls inside component logic. This can make debugging very challenging. Prefer lifting the tRPC call outside component logic. Especially do not use "await trpc.myProc.mutate({})" inside a component's render cycle.
### 3.2. Handling Complex State with tRPC
**Standard:** Address complex state management scenarios within tRPC applications thoughtfully.
* **Do This:**
* Utilize custom React Query hooks to encapsulate complex data fetching and transformation logic.
* Break down large datasets into smaller, manageable units.
* Use dependent queries to fetch data only when necessary.
* If needing to call a tRPC procedure *within* another tRPC procedure (which is quite rare), consider using "inferAsyncReturnType" to correctly type the response for the caller.
**Example (Dependent Queries):**
"""typescript
// React Component (client)
import { trpc } from '../../utils/trpc'; // Ensure this references your tRPC client
function UserPosts({ userId }: { userId: string }) {
// Fetch user details
const { data: user, isLoading: isUserLoading } = trpc.user.getUser.useQuery({ userId: userId });
// Fetch user's posts, but only if user details have loaded
const { data: posts, isLoading: isPostsLoading } = trpc.post.getPostsByUser.useQuery({ userId: userId }, {
enabled: !!user, // Only run this query if 'user' is not null/undefined
});
if (isUserLoading) return <p>Loading user...</p>;
if (!user) return <p>User not found</p>;
if (isPostsLoading) return <p>Loading posts...</p>;
return (
{user.name}'s Posts
{posts?.map(post => (
{post.title}
<p>{post.content}</p>
))}
);
}
export default UserPosts;
"""
### 3.3. Secure State Management
**Standard:** Implement security best practices when working with sensitive data in your application state.
* **Do This:**
* Never store sensitive information directly in the client-side state if it doesn't need to be there. Only store the data required for the currently rendered UI.
* Use environment variables and secure configuration to handle API keys and sensitive credentials.
* Implement proper authentication and authorization on the server-side to prevent unauthorized access to data.
* Sanitize data received from the server before storing it in the state.
* Use HTTPS to encrypt data transmitted between the client and the server.
**Example (Safe data retrieval):**
"""typescript
// tRPC Router (server)
import { publicProcedure, router, protectedProcedure } from './trpc';
import { z } from 'zod';
export const appRouter = router({
getSensitiveData: protectedProcedure // Only accessible if authenticated
.query(async ({ ctx }) => {
if (!ctx.auth.userId) { // Double check, just to be safe
throw new Error("Not authenticated");
}
// Retrieve data, ensuring user has access rights
const sensitiveData = await getSensitiveDataFromDB(ctx.auth.userId);
return sensitiveData;
}),
});
"""
This coding standards document provides a strong foundation for building robust, maintainable, and performant tRPC applications with well-managed state. These standards will help developers, and AI coding assistants alike, write better tRPC code with a focus on state management best practices. Remember to review and update this document regularly as the tRPC ecosystem evolves.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Security Best Practices Standards for tRPC This document outlines security best practices for tRPC applications. Adhering to these standards helps protect against common vulnerabilities, ensures data integrity, and promotes the development of secure and reliable applications. ## 1. Input Validation and Sanitization ### Standards * **Do This:** Always validate and sanitize all inputs received from the client. Define input schemas with Zod or similar validation libraries that support the specific data types and constraints you expect. * **Do This:** Use "z.preprocess" or similar Zod functionalities to sanitize and transform data before validation to normalize inputs, prevent injection attacks, and avoid errors due to unexpected characters. * **Don't Do This:** Rely on client-side validation alone. Client-side validation can be bypassed, making server-side validation crucial. * **Don't Do This:** Pass raw user input directly to database queries or system commands without validation or sanitization. ### Explanation Input validation is essential to prevent various attacks, including SQL injection, cross-site scripting (XSS), and command injection. Validating and sanitizing data ensures it conforms to expected formats and does not contain malicious code. tRPC simplifies this process by integrating seamlessly with Zod for schema definition and validation. ### Code Example """typescript import { z } from 'zod'; import { publicProcedure, router } from './trpc'; const userInputSchema = z.object({ email: z.string().email(), username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9]+$/), // Only alphanumeric characters allowed profileDescription: z.string().max(200).optional().transform(val => val?.trim()), // Trim whitespace }); export const appRouter = router({ createUser: publicProcedure .input(userInputSchema) .mutation(async ({ input }) => { // Input is guaranteed to be valid based on the schema const { email, username, profileDescription } = input; // Database interaction (example) // Using an ORM like Prisma is recommended to further prevent SQL injection const user = await prisma.user.create({ data: { email, username, profileDescription, }, }); return user; }), }); export type AppRouter = typeof appRouter; """ ### Anti-Patterns * Failing to validate input length, format, or type constraints. * Using basic string replacements as the sole sanitization method. Regular expressions and dedicated sanitization libraries offer more robust protection. * Omitting validation for optional fields, which can still introduce vulnerabilities if present with malicious data. ## 2. Authentication and Authorization ### Standards * **Do This:** Implement authentication to verify the identity of users accessing the application. Use secure authentication methods such as OAuth 2.0 or JWT. * **Do This:** Utilize authorization to manage user permissions and restrict access to sensitive data and procedures. Employ role-based access control (RBAC) or attribute-based access control (ABAC) as needed. * **Do This:** Protect API keys and secrets. Never expose them in client-side code. Store API keys in environment variables and access them securely on the server. * **Don't Do This:** Rely solely on cookies for authentication. Cookies can be vulnerable to cross-site scripting (XSS) and cross-site request forgery (CSRF) attacks. Use secure, httpOnly cookies with proper SameSite attributes. * **Don't Do This:** Store sensitive user data, such as passwords, in plain text. Always hash passwords using bcrypt or Argon2 and store the hashed values. ### Explanation Authentication confirms *who* the user is, while authorization dictates *what* a user can do. Robust authentication and authorization mechanisms are critical to preventing unauthorized access and maintaining data integrity. tRPC does not handle authentication/authorization directly, so integration with existing solutions is necessary. ### Code Example """typescript import { TRPCError } from '@trpc/server'; import { z } from 'zod'; import { publicProcedure, router, middleware } from './trpc'; import { verifyJwtToken } from './auth-utils'; // Custom JWT verification function // Authentication Middleware const authMiddleware = middleware(async ({ ctx, next }) => { const token = ctx.req.headers.authorization?.split(' ')[1]; // Bearer <token> if (!token) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing authentication token' }); } try { const user = await verifyJwtToken(token); return next({ ctx: { ...ctx, user, // Add user information to the context }, }); } catch (error) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Invalid authentication token' }); } }); // Protected Procedure const protectedProcedure = publicProcedure.use(authMiddleware); export const appRouter = router({ getUserProfile: protectedProcedure .query(async ({ ctx }) => { // Access user information from the context const userId = ctx.user.id; const userProfile = await prisma.user.findUnique({ where: { id: userId } }); if (!userProfile) { throw new TRPCError({ code: 'NOT_FOUND', message: 'User profile not found' }); } return userProfile; }), updateProfile: protectedProcedure .input(z.object({ bio: z.string().max(200) })) .mutation(async ({ input, ctx }) => { const userId = ctx.user.id; const updatedProfile = await prisma.user.update({ where: { id: userId }, data: { bio: input.bio }, }); return updatedProfile; }), }); export type AppRouter = typeof appRouter; """ ### Anti-Patterns * Using weak or predictable credentials. Enforce strong password policies. * Implementing "forgot password" functionality without proper security measures. * Failing to invalidate tokens after a password reset or account compromise. * Granting excessive permissions to users, violating the principle of least privilege. * Not implementing proper CORS (Cross-Origin Resource Sharing) configurations, allowing unauthorized domains to access your tRPC API. ## 3. Cross-Site Scripting (XSS) Prevention ### Standards * **Do This:** Sanitize all output rendered in the browser to prevent XSS attacks. Use templating engines or libraries that automatically escape HTML entities or implement a Content Security Policy (CSP). * **Do This:** Use "DOMPurify" or similar libraries for more complex HTML sanitization, especially when allowing users to input HTML content. * **Don't Do This:** Directly render user-supplied data in HTML without proper escaping or sanitization. ### Explanation XSS attacks occur when malicious scripts are injected into web pages viewed by other users. Sanitizing output and using CSP can prevent these attacks by ensuring that only trusted code is executed in the browser. While tRPC handles communication, XSS is more commonly a frontend concern when rendering the data received from tRPC. ### Code Example While XSS prevention is primarily a frontend concern, it's still relevant when considering data flowing from tRPC. The focus is on how data is handled *after* it's received from the tRPC server. """typescript // Example using React and DOMPurify on the Frontend AFTER data is retrieved from tRPC import React from 'react'; import DOMPurify from 'dompurify'; interface Props { unsafeHTML: string; // Data fetched from a tRPC procedure } function SafeDisplay({ unsafeHTML }: Props) { const cleanHTML = DOMPurify.sanitize(unsafeHTML); return ( <div dangerouslySetInnerHTML={{ __html: cleanHTML }} /> ); } export default SafeDisplay; """ ### Anti-Patterns * Using "dangerouslySetInnerHTML" in React without proper sanitization. * Allowing users to upload arbitrary files that can be interpreted as HTML or JavaScript. * Not configuring a Content Security Policy (CSP) to restrict the sources of content that the browser is allowed to load. ## 4. Cross-Site Request Forgery (CSRF) Prevention ### Standards * **Do This:** Implement CSRF protection to prevent unauthorized actions on behalf of authenticated users. Use synchronizer tokens or the SameSite cookie attribute. * **Do This:** Ensure your tRPC API is only accessible from your intended domain. * **Don't Do This:** Rely solely on cookies for authentication, as they are automatically sent with every request, making your application vulnerable to CSRF attacks if not properly protected. ### Explanation CSRF attacks occur when an attacker tricks a user into performing actions on a web application without their knowledge. CSRF protection mechanisms, such as synchronizer tokens and the SameSite cookie attribute, can prevent these attacks by ensuring that requests originate from the legitimate application. tRPC itself doesn't have built-in CSRF protection, so you rely on your framework/libraries. ### Code Example (Next.js with CSRF Protection) This example showcases CSRF protection using the "csrf-sync" library in a Next.js application, demonstrating a common approach to protecting tRPC endpoints. Note that the "csrf-sync" library generates and validates CSRF tokens, helping to mitigate CSRF attacks. """typescript // pages/api/trpc/[trpc].ts (API Route) import { appRouter } from '../../../server/router'; import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { csrf } from 'csrf-sync'; const { generateToken, verifyToken, getTokenFromRequest, loadMiddleware, } = csrf({ size: 32, secret: 'a-very-long-and-complex-string-to-use-as-a-secret', salt: 'xxxx', getTokenFromRequest: (req) => { if (req.headers['x-csrf-token']) { return req.headers['x-csrf-token'] as string; } return ''; }, }); const handler = async (req: NextApiRequest, res: NextApiResponse) => { await loadMiddleware(req, res); const token = generateToken(res); res.setHeader('X-CSRF-Token', token); return fetchRequestHandler({ endpoint: '/api/trpc', req, res, router: appRouter, createContext: () => ({ req, res }), onError({ error, path, input, ctx, req }) { console.error('Error:', error); if (error.code === 'BAD_REQUEST') { } }, }); }; export default handler; // server/router.ts (tRPC Router) import { publicProcedure, router } from '../trpc'; import { z } from 'zod'; export const appRouter = router({ protectedMutation: publicProcedure .input(z.object({ data: z.string() })) .mutation(async ({ input }) => { // Perform the mutation logic here return { success: true, message: "Data processed: ${input.data}" }; }), }); export type AppRouter = typeof appRouter; """ ### Anti-Patterns * Not using a CSRF protection mechanism at all. * Using a weak or predictable CSRF token. * Failing to validate the CSRF token on every state-changing request. * Allowing cross-origin requests without proper CORS configuration. ## 5. Rate Limiting and DoS Protection ### Standards * **Do This:** Implement rate limiting to prevent abuse and protect against denial-of-service (DoS) attacks. Limit the number of requests from a single IP address or user within a specified time frame. * **Do This:** Use middleware to apply rate limiting rules to specific procedures or the entire tRPC API. * **Don't Do This:** Rely solely on client-side rate limiting, as it can be easily bypassed. * **Don't Do This:** Expose sensitive procedures without rate limiting. ### Explanation Rate limiting restricts the number of requests a client can make within a given time period, preventing abuse, protecting against DoS attacks, and ensuring fair usage of resources. ### Code Example """typescript import { TRPCError } from '@trpc/server'; import { publicProcedure, router, middleware } from './trpc'; import { rateLimit } from './rate-limit'; // Custom rate limiting utility // Rate Limiting Middleware const rateLimitMiddleware = middleware(async ({ ctx, next }) => { const identifier = ctx.req.headers['x-forwarded-for'] || ctx.req.socket.remoteAddress; // Get client IP address if (!identifier) { throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Could not determine client IP' }); } const rateLimitResult = await rateLimit(identifier, 10, '60 s'); // Allow 10 requests per IP address every 60 seconds if (!rateLimitResult.success) { throw new TRPCError({ code: 'TOO_MANY_REQUESTS', message: 'Rate limit exceeded' }); } return next({ ctx, }); }); // Protected Procedure with Rate Limiting const rateLimitedProcedure = publicProcedure.use(rateLimitMiddleware); export const appRouter = router({ expensiveOperation: rateLimitedProcedure .query(async () => { // Perform an expensive operation here return { success: true }; }), }); export type AppRouter = typeof appRouter; """ """typescript // rate-limit.ts (Example Rate Limiting Implementation - using Redis) import { Redis } from '@upstash/redis' const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL, token: process.env.UPSTASH_REDIS_REST_TOKEN, }) export async function rateLimit(identifier: string, limit: number, timeFrame: string) { const key = "rateLimit:${identifier}"; const { remaining, reset } = await redis.incr(key) .set('ex', timeFrame).then(res => { return { remaining: limit - res, reset: parseInt(timeFrame) } }) const success = remaining >= 0; return { success, reset, remaining }; } """ ### Anti-Patterns * Not implementing rate limiting at all. * Using overly generous rate limits that allow for abuse. * Failing to handle rate limit errors gracefully on the client side. * Relying on a single rate limiting scheme that doesn't adapt to different types of requests. ## 6. Error Handling and Logging ### Standards * **Do This:** Implement robust error handling to catch and handle exceptions gracefully. Return meaningful error messages to the client without exposing sensitive information. * **Do This:** Use structured logging to record application events and errors. Include relevant context in log messages, such as user IDs, request parameters, and timestamps. * **Don't Do This:** Expose stack traces or internal error details to the client. This can reveal sensitive information about your application. * **Don't Do This:** Ignore errors or fail to log them properly. Unhandled errors can lead to unexpected behavior and security vulnerabilities. ### Explanation Proper error handling and logging are crucial for diagnosing issues, monitoring application health, and identifying potential security threats. ### Code Example """typescript import { TRPCError } from '@trpc/server'; import { publicProcedure, router } from './trpc'; import { z } from 'zod'; import logger from './logger'; // Custom logger export const appRouter = router({ getData: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { try { const data = await someAsyncOperation(input.id); if (!data) { throw new TRPCError({ code: 'NOT_FOUND', message: 'Data not found.', }); } return data; } catch (error) { logger.error("Error fetching data for id ${input.id}: ${error}"); if (error instanceof TRPCError) { throw error; // Re-throw known TRPC errors } throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'An unexpected error occurred.', }); } }), }); export type AppRouter = typeof appRouter; """ ### Anti-Patterns * Catching all exceptions and returning a generic error message, obscuring the root cause of the error. * Logging sensitive information, such as passwords or API keys. * Using "console.log" for logging in production. Use a dedicated logging library * Not implementing monitoring and alerting for critical errors. ## 7. Dependency Management and Security Audits ### Standards * **Do This:** Keep all dependencies up to date to patch security vulnerabilities. Use tools like "npm audit" or "yarn audit" to identify and fix vulnerabilities in your dependencies. * **Do This:** Regularly review and audit your application code for security vulnerabilities. Consider using static analysis tools or hiring a security consultant to perform penetration testing. * **Don't Do This:** Use outdated or unmaintained dependencies. * **Don't Do This:** Neglect security audits or rely solely on automated tools. Human review is essential to identify complex vulnerabilities. ### Explanation Dependencies can introduce security vulnerabilities if they are not properly maintained. Regularly updating dependencies and performing security audits can help identify and mitigate these risks. ### Code Example (N/A - Dependency Management is configuration-based) * **Using "npm audit":** """bash npm audit npm audit fix """ * **Using "yarn audit":** """bash yarn audit yarn audit fix """ ### Anti-Patterns * Ignoring security vulnerabilities reported by dependency audit tools. * Disabling security updates or pinning dependencies to specific versions indefinitely. * Not having a process for responding to security incidents. ## 8. Context Security ### Standards * **Do This:** Use the tRPC context to pass authenticated user information or other security-related data securely. * **Do This:** Ensure the context is constructed securely and avoid injecting untrusted data into it. Validate any data retrieved from the context within your procedures. * **Don't Do This:** Store sensitive data (like secrets) directly in the context if it's not needed for every procedure. Pass it explicitly where needed. * **Don't Do This:** Trust the context implicitly. Always validate the data within the context, especially when using it for authorization decisions. ### Explanation The tRPC context serves as a secure and efficient way to pass data to your procedures. Proper use of the context improves security and performance, while misuse can lead to vulnerabilities. ### Code Example """typescript import { publicProcedure, router, middleware } from './trpc'; import { TRPCError } from '@trpc/server'; // Example Authentication Middleware (simplified) const authMiddleware = middleware(async ({ ctx, next }) => { const userId = getUserIdFromHeaders(ctx.req.headers); // Replace with your actual authentication logic if (!userId) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' }); } return next({ ctx: { ...ctx, userId, // Add authenticated user ID to the context }, }); }); const protectedProcedure = publicProcedure.use(authMiddleware); export const appRouter = router({ // Protected procedure that accesses user ID from the context getUserData: protectedProcedure .query(async ({ ctx }) => { const userId = ctx.userId; // Access user ID from context const userData = await fetchUserData(userId); if (!userData) { throw new TRPCError({ code: 'NOT_FOUND', message: 'User data not found' }); } return userData; }), }); export type AppRouter = typeof appRouter; // Mock functions for demonstration purposes function getUserIdFromHeaders(headers: Headers): string | null { // Replace this with your actual authentication logic const authHeader = headers.get('authorization'); if (authHeader && authHeader.startsWith('Bearer ')) { // In a real implementation, you would verify the token and extract the user ID return 'mock-user-id'; } return null; } async function fetchUserData(userId: string): Promise<any> { // Replace this with your actual data fetching logic return { id: userId, name: 'Mock User', data: 'Sensitive Data' }; } """ ### Anti-Patterns * Injecting untrusted data directly into the context without validation. * Assuming the context is always present or contains valid data. * Overusing the context to pass data that is not essential to the security or operation of your procedures. * Failing to sanitize or validate data retrieved from the context before using it in your procedures, especially for authorization decisions. By adhering to these security best practices, you can build more secure and resilient tRPC applications that protect against common vulnerabilities and ensure the confidentiality, integrity, and availability of your data. Remember to stay informed about the latest security threats and adapt your practices accordingly.
# Deployment and DevOps Standards for tRPC This document outlines the recommended standards and best practices for deploying and managing tRPC applications in production environments. Following these guidelines will ensure maintainability, performance, security, and scalability of your tRPC applications. ## 1. Build Processes and CI/CD for tRPC ### 1.1. Standard: Use a Robust Build Process **Do This:** Use a build tool like "esbuild", "swc", or "webpack" to transpile and bundle your tRPC application. **Don't Do This:** Deploy source code directly to production without a build step. **Why:** A build process allows you to: * Transpile TypeScript code to JavaScript. * Bundle modules for efficient loading. * Minify code and assets to reduce payload size. * Perform code analysis and linting. * Run tests to ensure code quality. """typescript // Example: Setting up esbuild for a tRPC backend const esbuild = require('esbuild'); esbuild.build({ entryPoints: ['src/index.ts'], bundle: true, outfile: 'dist/index.js', platform: 'node', format: 'cjs', minify: true, sourcemap: true, }).catch(() => process.exit(1)); """ ### 1.2. Standard: Implement CI/CD Pipelines **Do This:** Use a CI/CD (Continuous Integration/Continuous Deployment) system such as GitHub Actions, GitLab CI, CircleCI, or Jenkins. Configure it to automatically build, test, and deploy your application on every commit, merge, or tag. **Don't Do This:** Manually build and deploy your application. **Why:** CI/CD automates the deployment process, reducing the risk of human error and ensuring consistent deployments. """yaml # Example: GitHub Actions workflow for a tRPC application name: CI/CD on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Use Node.js 18 uses: actions/setup-node@v3 with: node-version: 18 - name: Install dependencies run: npm install - name: Build run: npm run build - name: Run tests run: npm run test - name: Deploy to Production if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: | # Add your deployment script here (e.g., SSH, Docker, Serverless) echo "Deploying to production..." """ ### 1.3. Standard: Versioning and Rollback Strategies **Do This:** Implement versioning for your deployments (e.g., using semantic versioning or Git tags). Have a clear rollback strategy in case of deployment failures. **Don't Do This:** Deploy without a versioning system or a rollback plan. **Why:** Versioning allows you to track changes and easily revert to previous stable versions. Rollback strategies help minimize downtime and prevent critical failures from affecting users. ### 1.4. Standard: Environment Variables **Do This:** Use environment variables to manage configuration settings such as API keys, database connections, and feature flags. Store sensitive information securely using tools like HashiCorp Vault or AWS Secrets Manager. **Don't Do This:** Hardcode configuration settings directly in your code or commit sensitive information to your repository. **Why:** Environment variables allow you to configure your application for different environments without modifying the code. Secrets management systems enhance security by protecting sensitive information. """typescript // Example: Accessing environment variables in a tRPC procedure import { z } from 'zod'; import { publicProcedure, router } from './trpc'; const apiKey = process.env.MY_API_KEY; export const appRouter = router({ getSecretData: publicProcedure.query(() => { if (!apiKey) { throw new Error('API key not found'); } return { secret: apiKey }; }), }); export type AppRouter = typeof appRouter; """ ## 2. Production Considerations for tRPC ### 2.1. Standard: Logging and Monitoring **Do This:** Implement comprehensive logging and monitoring for your tRPC application using tools like Winston, Morgan, or Sentry. Monitor server performance, API latency, and error rates. **Don't Do This:** Rely on default console logs or ignore error reporting. **Why:** Logging and monitoring enable you to identify and diagnose issues in production, optimize performance, and ensure application stability. """typescript // Example: Using Winston for logging in a tRPC backend import winston from 'winston'; const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/combined.log' }), ], }); // Example: Logging a tRPC procedure call import { z } from 'zod'; import { publicProcedure, router } from './trpc'; export const appRouter = router({ greet: publicProcedure .input(z.object({ name: z.string() })) .query(({ input }) => { logger.info("Received greeting request for ${input.name}"); return "Hello, ${input.name}!"; }), }); export type AppRouter = typeof appRouter; """ ### 2.2. Standard: Error Handling **Do This:** Implement robust error handling in your tRPC procedures using custom errors or built-in utilities like ".use(middleware)". Handle errors gracefully and provide informative error messages to the client. **Don't Do This:** Allow unhandled exceptions to crash your server or expose sensitive information in error messages. **Why:** Proper error handling ensures a smooth user experience, prevents data corruption, and reduces the risk of security vulnerabilities. """typescript // Example: Custom error in a tRPC procedure import { TRPCError } from '@trpc/server'; import { z } from 'zod'; import { publicProcedure, router } from './trpc'; export const appRouter = router({ getUser: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { const user = await fetchUserFromDatabase(input.id); if (!user) { throw new TRPCError({ code: 'NOT_FOUND', message: "User with id ${input.id} not found", }); } return user; }), }); export type AppRouter = typeof appRouter; async function fetchUserFromDatabase(id: string): Promise<any> { // Simulated database fetch return new Promise((resolve, reject) => { setTimeout(() => { const users = [{id: '1', name: 'Alice'}, {id: '2', name: 'Bob'}]; const user = users.find(u => u.id === id); resolve(user); }, 500); }); } """ ### 2.3. Standard: Resource Optimization (e.g. Database Connections) **Do This:** Manage resources efficiently, such as database connections, file handles, and memory. Use connection pooling, caching, and other optimization techniques to minimize resource consumption. **Don't Do This:** Create excessive resources or leak resources, which can lead to performance degradation and service outages. **Why:** Efficient resource management improves performance, reduces costs, and ensures scalability. ### 2.4. Standard: Caching **Do This:** Implement caching mechanisms at various levels (e.g., server-side caching, client-side caching, CDN caching) to reduce latency and improve response times. Use tools like Redis, Memcached, or HTTP caching headers. Consider using tRPC's built-in query invalidation for client-side cache management, using "useMutation" and "invalidateQueries". **Don't Do This:** Cache sensitive data without proper security measures or rely solely on client-side caching for critical data. **Why:** Caching reduces the load on your servers, improves performance, and enhances user experience. """typescript // Example: tRPC mutation invalidating cache on updates import { z } from 'zod'; import { publicProcedure, router } from './trpc'; export const appRouter = router({ updateUser: publicProcedure .input(z.object({ id: z.string(), name: z.string() })) .mutation(async ({ input }) => { // Update user in database const updatedUser = await updateUserInDatabase(input.id, input.name); return updatedUser; }), getUser: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { const user = await fetchUserFromDatabase(input.id); return user; }), }); export type AppRouter = typeof appRouter; """ """typescript jsx // Example (React): Using useMutation and invalidateQueries to update cache. import { trpc } from '../utils/trpc'; function UserProfile({ userId }: { userId: string }) { const { data: user } = trpc.getUser.useQuery({ id: userId }); const updateUserMutation = trpc.updateUser.useMutation({ onSuccess: () => { // Invalidate queries and refetch data trpc.invalidateQueries(['getUser', { id: userId }]); }, }); const handleUpdateName = async (newName: string) => { await updateUserMutation.mutateAsync({ id: userId, name: newName }); } if (!user) return <div>Loading...</div>; return ( <div> <h1>{user.name}</h1> <button onClick={() => handleUpdateName('New Name')}>Update Name</button> </div> ); } """ ### 2.5. Standard: Data Validation **Do This:** Implement strong data validation on both the client and server sides. Use tools like Zod or Yup to define schemas for your tRPC inputs and outputs. **Don't Do This:** Trust data coming from the client or perform insufficient validation, which can lead to security vulnerabilities or application errors. **Why:** Data validation prevents invalid data from entering your system, ensures data integrity, and reduces the risk of security attacks. ### 2.6 Standard: Security Best Practices **Do This:** Implement industry-standard security measures such as: * **Authentication and Authorization:** Secure your tRPC procedures with authentication and authorization mechanisms using JWT, OAuth, or similar protocols. * **Rate Limiting:** Protect your API from abuse by implementing rate limiting. Libraries like "express-rate-limit" can be used on your server. * **Input Sanitization:** Sanitize user input to prevent XSS and other injection attacks. * **CORS Configuration:** Properly configure Cross-Origin Resource Sharing (CORS) to restrict cross-origin requests. * **HTTPS:** Enforce HTTPS to encrypt data transmitted between the client and server. * **Regular Security Audits:** Conduct regular security audits to identify and address potential vulnerabilities. **Don't Do This:** Expose sensitive data in your API, use weak authentication methods, or neglect security updates. **Why:** Security is critical for protecting your application, data, and users. """typescript // Example: Implementing rate limiting using express-rate-limit import rateLimit from 'express-rate-limit'; import express from 'express'; import { createExpressMiddleware } from '@trpc/server/adapters/express'; import { appRouter } from './router'; const app = express(); // Rate limiting middleware const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: 'Too many requests, please try again after 15 minutes', }); app.use(limiter); // tRPC middleware app.use( '/trpc', createExpressMiddleware({ router: appRouter, }) ); app.listen(3000, () => { console.log('Server listening on port 3000'); }); """ ## 3. tRPC-Specific Considerations ### 3.1. Standard: tRPC Middleware Usage **Do This:** Leverage tRPC middleware for tasks such as logging, authentication, authorization and input validation. Middleware allows you to apply consistent logic across multiple procedures. **Don't Do This:** Duplicate logic in individual procedures when it can be handled more efficiently with middleware. **Why:** Middleware promotes code reuse, simplifies maintenance, and enforces consistency. """typescript // Example: Authorization middleware import { TRPCError, initTRPC } from '@trpc/server'; import { createContext } from './context'; const t = initTRPC.context<typeof createContext>().create(); const isAuthed = t.middleware(({ next, ctx }) => { if (!ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next({ ctx: { user: ctx.user, // Add user to context }, }); }); export const protectedProcedure = t.procedure.use(isAuthed); export const router = t.router; export const publicProcedure = t.procedure; """ ### 3.2. Standard: Data Serialization **Do This:** Be mindful of data serialization when using tRPC. Ensure that data is correctly serialized and deserialized between the client and server. Use appropriate data types and formats to avoid unexpected issues. Consider using transformers, like superjson, if you require complex type support. **Don't Do This:** Ignore data serialization issues or use incompatible data types, which can lead to data loss or corruption. **Why:** Accurate data serialization is essential for ensuring data integrity and proper communication between the client and server, especially so when working with very complex types. """typescript // Example with superjson transformer. import { initTRPC } from '@trpc/server'; import superjson from 'superjson'; const t = initTRPC.create({ transformer: superjson, }); export const router = t.router; export const publicProcedure = t.procedure; """ ### 3.3. Standard: API Versioning with tRPC **Do This:** Implement API versioning for your tRPC procedures to maintain backward compatibility and allow for future changes. Use different routers for different API versions. **Don't Do This:** Make breaking changes to your API without a versioning strategy. **Why:** API versioning enables you to evolve your API without disrupting existing clients. ### 3.4 Standard: Serverless Functions **Do This:** Consider deploying your tRPC backend as serverless functions (e.g., AWS Lambda, Google Cloud Functions, Netlify Functions) for scalability and cost-effectiveness. **Don't Do This:** Deploy long-running tRPC processes in serverless environments, which can lead to timeouts and performance issues. Optimize your tRPC procedures for short execution times. **Why:** Serverless functions offer automatic scaling, pay-per-use pricing, and simplified deployment. ### 3.5 Standard: Use latest tRPC Features. **Do This:** Stay up-to-date with the latest tRPC releases and features. Use the features that bring benefits to your code. **Don't Do This:** Stay stuck on an old version longer than neccesary. **Why:** Recent releases usually contain performance and/or security updates. ## Conclusion By adhering to these deployment and DevOps standards, you can significantly improve the reliability, performance, security, and maintainability of your tRPC applications. This document provides a comprehensive guide for developers and AI coding assistants to promote best practices and ensure consistent code quality.
# Component Design Standards for tRPC This document outlines the coding standards specifically for designing reusable and maintainable components within tRPC applications. These guidelines focus on leveraging the latest tRPC features and best practices to promote code clarity, prevent common pitfalls, and optimize performance. ## 1. Principles of Reusable tRPC Components ### 1.1 Abstraction and Encapsulation * **Do This:** Abstract complex logic into reusable functions and modules. Apply this at all scales, from individual helpers used in multiple procedures to entirely separate routers that are composed together into your server. * **Why:** Abstraction reduces code duplication and improves readability. Encapsulation hides implementation details, minimizing the impact of changes on other parts of the application. """typescript // Good: Abstracted helper function const validateEmail = (email: string): boolean => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // More robust regex should be used in prod. return emailRegex.test(email); }; const userRouter = t.router({ createUser: t.procedure .input(z.object({ email: z.string() })) .mutation(async ({ input }) => { if (!validateEmail(input.email)) { throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid email format' }); } // ... rest of create user logic }), }); """ * **Don't Do This:** Embed the same complex logic repeatedly throughout your tRPC procedures. Avoid exposing internal implementation details of your procedures directly to the client. """typescript // Bad: Duplicated validation logic const userRouter = t.router({ createUser: t.procedure .input(z.object({ email: z.string() })) .mutation(async ({ input }) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(input.email)) { throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid email format' }); } // ... rest of create user logic }), updateUserEmail: t.procedure .input(z.object({ id: z.string(), email: z.string() })) .mutation(async ({ input }) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(input.email)) { throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid email format' }); } // ... rest of update user logic }), }); """ ### 1.2 Single Responsibility Principle (SRP) * **Do This:** Design components (procedures, routers, helper functions) to have a single, well-defined purpose. * **Why:** SRP makes components easier to understand, test, and modify. Changes to one aspect of the component will be less likely to affect other parts of the system. """typescript // Good: Separate service for interacting with the database import { prisma } from './db'; // Assuming you are using Prisma/ORM const userService = { getUserById: async (id: string) => { return await prisma.user.findUnique({ where: { id } }); }, createUser: async (email: string) => { return await prisma.user.create({ data: { email } }); }, // ... other user-related database operations }; const userRouter = trpc.router({ getUser: t.procedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { return userService.getUserById(input.id); }), createUser: t.procedure .input(z.object({ email: z.string() })) .mutation(async ({ input }) => { return userService.createUser(input.email); }), }); """ * **Don't Do This:** Create overly complex procedures that handle multiple unrelated tasks. """typescript // Bad: Combining data fetching and email sending in single procedure const userRouter = t.router({ createUserAndSendWelcomeEmail: t.procedure .input(z.object({ email: z.string() })) .mutation(async ({ input }) => { const user = await prisma.user.create({data: {email: input.email}}); // Complex email sending logic here... return user; }), }); """ ### 1.3 Composition over Inheritance * **Do This:** Favor composing smaller, reusable functions or modules into larger components rather than using inheritance. In tRPC, this often manifests as composing multiple routers together via "mergeRouters". * **Why:** Composition provides greater flexibility and reduces the risk of creating tightly coupled systems. """typescript // Good: Composing routers const authRouter = trpc.router({ getSession: trpc.procedure.query(() => { // ... authentication logic }), }); const userRouter = trpc.router({ getUser: trpc.procedure .input(z.object({id: z.string()})) .query(async ({input, ctx}) => { //Make sure user is authenticated if(!ctx.session) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return await prisma.user.findUnique({where: {id: input.id}}); }), }); const appRouter = trpc.mergeRouters(authRouter, userRouter); // Export type definition of API export type AppRouter = typeof appRouter; """ * **Don't Do This:** Attempt to create deeply nested hierarchies of routers or procedures to achieve code reuse if composition would be more appropriate. Avoid creating "base" routers that are only meant to be extended. ## 2. tRPC Router Design ### 2.1 Router Naming and Organization * **Do This:** Use descriptive names for routers that clearly indicate their domain or purpose (e.g., "userRouter", "productRouter", "authRouter"). Organize routers into logical modules based on functionality. * **Why:** Clear naming and organization improve code discoverability and maintainability. """typescript // Good: Clear router organization // src/server/routers/user.ts export const userRouter = t.router({ /* ... */ }); // src/server/routers/product.ts export const productRouter = t.router({ /* ... */ }); // src/server/routers/auth.ts export const authRouter = t.router({ /* ... */ }); """ * **Don't Do This:** Use generic or ambiguous names for routers (e.g., "router1", "api"). Place routers in arbitrary locations without a clear organizational structure. ### 2.2 Context Management * **Do This:** Utilize the tRPC context effectively to share data and dependencies across procedures within a router. Keep the context lean and focused on essential information. * **Why:** Sharing context reduces code duplication and improves performance by avoiding redundant data fetching. """typescript // Good: Using context to share database connection import { inferAsyncReturnType } from '@trpc/server'; import { prisma } from './db'; // src/server/context.ts export const createContext = async () => { return { prisma, }; }; type Context = inferAsyncReturnType<typeof createContext>; export const t = trpc.context<Context>().create(); const userRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.string() })) .query(async ({ input, ctx }) => { return await ctx.prisma.user.findUnique({ where: { id: input.id } }); }), }); """ * **Don't Do This:** Pass data or dependencies as arguments to every procedure, especially if they are already available in the context. Overload the context with unnecessary information. ### 2.3 Input Validation and Transformation * **Do This:** Use Zod schemas to rigorously validate and transform input data for all tRPC procedures. Define schemas that accurately reflect the expected data structure and constraints. * **Why:** Input validation prevents errors, protects against malicious input, and ensures type safety. Transformation allows you to sanitize and normalize data before processing it. """typescript // Good: Validating and transforming input with Zod import { z } from 'zod'; const userRouter = t.router({ createUser: t.procedure .input( z.object({ email: z.string().email(), name: z.string().min(2).max(50), }) ) .mutation(async ({ input }) => { // input is guaranteed to be valid and transformed const { email, name } = input; // Destructure to use type-safe values return await prisma.user.create({ data: { email, name } }); }), }); """ * **Don't Do This:** Skip input validation or rely on implicit type coercion. Define overly permissive schemas that allow invalid data to reach your application logic. Avoid sanitizing or transforming data that comes from the client. ### 2.4 Error Handling * **Do This:** Use "TRPCError" to throw appropriate error codes for different scenarios in your tRPC procedures. Implement centralized error handling to log errors, notify developers, and provide informative error messages to the client. * **Why:** Proper error handling improves the user experience, helps debug issues, and enhances the security of your application. """typescript // Good: Using TRPCError with appropriate error codes import { TRPCError } from '@trpc/server'; const userRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.string() })) .query(async ({ input, ctx }) => { const user = await ctx.prisma.user.findUnique({ where: { id: input.id } }); if (!user) { throw new TRPCError({ code: 'NOT_FOUND', message: "User with id ${input.id} not found", }); } return user; }), }); """ * **Don't Do This:** Throw generic errors or uncaught exceptions. Return plain strings or error objects without a standard format. ### 2.5 Middleware * **Do This:** Use tRPC middleware to implement cross-cutting concerns such as authentication, authorization, logging, and data caching. Create reusable middleware functions that can be applied to multiple procedures or routers. Leverage the "unstable_allowGet" option for queries protected by middleware. * **Why:** Middleware promotes code reuse and reduces duplication. """typescript // Good: Authentication middleware const isAuthed = t.middleware(({ ctx, next }) => { if (!ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next({ ctx: { user: ctx.user, }, }); }); const authedProcedure = t.procedure.use(isAuthed); const userRouter = t.router({ getSecret: authedProcedure.query(({ ctx }) => { return "This is the secret"; }), }); """ * **Don't Do This:** Embed authentication or authorization logic directly in every procedure. Create overly complex middleware functions that perform multiple unrelated tasks. ## 3. tRPC Procedure Design ### 3.1 Query vs. Mutation * **Do This:** Use queries for read-only operations that do not modify data. Use mutations for operations that create, update, or delete data. * **Why:** Distinguishing between queries and mutations improves code clarity, optimizes performance, and enables better caching strategies. """typescript // Good: Using queries for read operations and mutations for write operations const userRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.string() })) .query(async ({ input, ctx }) => { // Query return await ctx.prisma.user.findUnique({ where: { id: input.id } }); }), updateUser: t.procedure .input(z.object({ id: z.string(), email: z.string().email() })) .mutation(async ({ input, ctx }) => { // Mutation return await ctx.prisma.user.update({ where: { id: input.id }, data: { email: input.email }, }); }), }); """ * **Don't Do This:** Use queries to perform data modifications or mutations to fetch data. ### 3.2 Optimistic Updates * **Do This:** For mutations that update UI state, consider using optimistic updates to provide a smoother user experience. * **Why:** Optimistic updates make the UI feel more responsive by immediately reflecting the changes made by the user, even before the server confirms the update. """typescript // Example (React component): import { useMutation } from '@trpc/react-query'; import { useState } from 'react'; const Component = () => { const [text, setText] = useState('initial Value'); const {mutate} = useMutation('example.updateText', { onMutate: async (newText) => { // Cancel any outgoing refetches (so they don't overwrite our optimistic update) // await queryClient.cancelQueries(['example.getText']) setText(newText); }, }); return ( <div> <input type="text" value={text} onChange={(e) => { setText(e.target.value); mutate(e.target.value); }} /> </div> ); }; """ * **Don't Do This:** Neglect to handle potential errors or conflicts when using optimistic updates. ### 3.3 Batching and Caching * **Do This:** Consider batching multiple requests into a single request to reduce network overhead. Implement server-side and client-side caching to improve performance. Use tRPC utilities or libraries like "@trpc/react-query" for efficient caching. * **Why:** Batching and caching can significantly improve the performance and scalability of your application, especially for frequently accessed data. * **Don't Do This:** Over-cache data or cache sensitive information. Neglect to invalidate the cache when data changes. ## 4. Code Style and Formatting * **Do This:** Follow a consistent code style guide (e.g., Prettier, ESLint) to ensure code uniformity. Use meaningful names for variables, functions, and components. Provide clear and concise comments to explain complex logic. * **Why:** Consistent code style improves readability and maintainability. Clear names and comments make it easier to understand the code. * **Don't Do This:** Use inconsistent code formatting or ambiguous names. Write overly verbose or unhelpful comments. ## 5. Testing * **Do This:** Write unit tests for individual tRPC procedures and integration tests for complete workflows. Mock dependencies to isolate components during testing. Use tools like Jest and Supertest for testing tRPC APIs. * **Why:** Testing ensures that your tRPC APIs function correctly and reliably. * **Don't Do This:** Neglect to write tests or write incomplete or ineffective tests. ## 6. Security Considerations * **Do This:** Protect your tRPC APIs against common security vulnerabilities such as injection attacks, cross-site scripting (XSS), and cross-site request forgery (CSRF). Sanitize user input, validate data on the server-side, and implement proper authentication and authorization mechanisms. Rate limit your tRPC endpoints to prevent abuse. * **Why:** Security vulnerabilities can compromise the integrity and confidentiality of your application and data. * **Don't Do This:** Expose sensitive data through your tRPC APIs. Store sensitive data in client-side cookies or local storage. These standards provide a foundation for building robust, maintainable, and secure tRPC applications. Adhering to these guidelines will promote collaboration, reduce errors, and improve the overall quality of your code.
# Tooling and Ecosystem Standards for tRPC This document outlines the recommended tooling and ecosystem practices for developing tRPC applications. Adhering to these standards ensures consistency, maintainability, performance, and security across your projects. ## 1. Core Tooling & Libraries ### 1.1. tRPC CLI **Standard:** Utilize the tRPC CLI for project scaffolding and code generation whenever possible. * **Do This:** Use "npx create-t3-app@latest" or a similar template that integrates tRPC from the start. * **Don't Do This:** Manually configure tRPC from scratch unless absolutely necessary. **Why:** The CLI provides a pre-configured setup with best practices baked in, saving time and reducing the risk of misconfigurations. It also promotes a consistent project structure. **Example:** """bash npx create-t3-app@latest my-trpc-app """ ### 1.2. TypeScript **Standard:** Leverage TypeScript's full capabilities for type safety throughout the entire tRPC stack (client and server). * **Do This:** Define clear, specific types for your input and output schemas using Zod or similar validation libraries. * **Don't Do This:** Rely on "any" or implicit types, as this defeats the purpose of tRPC's end-to-end type safety. **Why:** Type safety prevents runtime errors and improves code maintainability. tRPC's strength lies in its seamless integration with TypeScript, facilitating type inference and auto-completion. **Example:** """typescript import { z } from 'zod'; import { publicProcedure, router } from './trpc'; const userRouter = router({ getUser: publicProcedure .input(z.object({ id: z.string() })) .output(z.object({ id: z.string(), name: z.string() })) .query(({ input }) => { // Logic to fetch user from database return { id: input.id, name: 'Example User' }; }), }); export type UserRouter = typeof userRouter; """ ### 1.3. Zod (or similar Schema Validation) **Standard:** Employ Zod (or a similar schema validation library like Yup or Joi) for defining and validating input data. * **Do This:** Use Zod's "z" object to define schemas for your route inputs and outputs. * **Don't Do This:** Manually validate input data using custom logic, as this is error-prone and less maintainable. **Why:** Zod provides a concise and expressive way to define data schemas, enabling type safety and validation with minimal boilerplate. It automatically infers TypeScript types from schemas which aligns perfectly with tRPC. **Example:** """typescript import { z } from 'zod'; import { publicProcedure, router } from './trpc'; const postSchema = z.object({ title: z.string().min(5).max(100), content: z.string().min(10), published: z.boolean().default(false), }); const postRouter = router({ createPost: publicProcedure .input(postSchema) .mutation(async ({ input }) => { // Logic to create post in database using input return { success: true, postId: '123' }; }), getPost: publicProcedure .input(z.object({ postId: z.string() })) .output(postSchema.extend({ createdAt: z.date() })) //Output extends base type .query(({ input }) => { return { title: 'Example Post', content: 'This is an example post content', published: true, createdAt: new Date(), }; }), }); export type PostRouter = typeof postRouter; """ ### 1.4. React Query / TanStack Query (Client-Side Data Fetching) **Standard:** Utilize React Query (or TanStack Query, formerly known as React Query) for managing client-side data fetching, caching, and state. * **Do This:** Use "useQuery", "useMutation", and "useInfiniteQuery" hooks provided by React Query to interact with tRPC procedures. * **Don't Do This:** Implement custom data fetching logic or manage client-side state manually. **Why:** React Query simplifies data fetching, caching, and state management. Its integration with tRPC is seamless, enabling type-safe data fetching and automatic revalidation. **Example:** """typescript jsx import { useQuery } from '@tanstack/react-query'; import { trpc } from '../utils/trpc'; // Assuming your tRPC client is in utils/trpc.ts function UserProfile({ userId }: { userId: string }) { const { data: user, isLoading, error } = trpc.user.getUser.useQuery({ id: userId }); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> <h1>{user.name}</h1> <p>ID: {user.id}</p> </div> ); } """ ### 1.5. ESLint and Prettier **Standard:** Enforce consistent code formatting and styling using ESLint and Prettier. * **Do This:** Configure ESLint and Prettier with recommended rules for TypeScript and React, and integrate them into your development workflow. * **Don't Do This:** Rely on manual code formatting or inconsistent styling. **Why:** ESLint and Prettier automatically enforce code style guidelines, improving code readability and maintainability. Integrate with your IDE & CI/CD Pipelines. **Example Configuration (package.json):** """json { "scripts": { "lint": "eslint . --ext .ts,.tsx", "format": "prettier --write ." }, "devDependencies": { "eslint": "^8.0.0", "prettier": "^2.0.0" } } """ Ensure that you configure ESLint and Prettier appropriately for your project, including typescript and react plugins. Example ".eslintrc.js": """javascript module.exports = { parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint', 'react-hooks'], extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'prettier', ], rules: { // Add any custom rules here }, }; """ ## 2. Advanced Tooling and Patterns ### 2.1. tRPC Transformers **Standard:** Utilize tRPC Transformers for complex data serialization and transformation. * **Do This:** Use transformers to handle date serialization, BigInts, or other custom data types efficiently. * **Don't Do This:** Manually serialize/deserialize complex data types in your procedures, as this can lead to inconsistencies and performance issues. **Why:** Transformers allow you to customize how data is serialized and deserialized between the client and server, improving performance and handling complex data types seamlessly. **Example (Date Serialization):** """typescript import { z } from 'zod'; import { publicProcedure, router } from './trpc'; import { transformer } from './utils/transformer'; //Custom Transformer Implementation const eventSchema = z.object({ title: z.string(), startTime: z.date(), }); export const eventRouter = router({ createEvent: publicProcedure .input(eventSchema) .mutation(async ({ input }) => { console.log("Received Data From The CLient: ",Input.startTime); return { success: true, createdAt: new Date() }; }), }); """ A custom Transformer is required to properly serialize a Date to be sent across the application. """typescript // utils/transformer.ts import { createTransformer } from 'ts-transformer-keys'; import { isDate } from 'util/types'; export const transformer = { input: (val: any) => { // serialize if (isDate(val)) { return val.toISOString(); } return val; }, output: (val: any) => { // deserialize if (typeof val === "string" && val.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/)) { return new Date(val); } return val }, }; """ Implement this transformer in with your tRPC initialization. """typescript // trpc.ts import { initTRPC } from '@trpc/server'; import { transformer } from './utils/transformer'; const t = initTRPC.create({ transformer, errorFormatter({ shape }) { return shape; }, }); export const router = t.router; export const middleware = t.middleware; export const publicProcedure = t.procedure; """ ### 2.2. Monitoring and Logging **Standard:** Implement robust monitoring and logging to track application health and debug issues effectively. * **Do This:** Integrate logging middleware into your tRPC router to log requests, responses, and errors. Use a dedicated logging service (e.g., Sentry, Datadog, or similar) for centralized monitoring. * **Don't Do This:** Rely on "console.log" statements for production environments. **Why:** Monitoring and Logging tools are vital for understanding the behavior of an application and are critical during debugging. **Example (Logging Middleware):** """typescript import { publicProcedure, router, middleware } from './trpc'; import { observable } from '@trpc/server/observable'; import { EventEmitter } from 'events'; const emitter = new EventEmitter(); const loggerMiddleware = middleware(async ({ path, type, next, input, ctx }) => { const start = Date.now(); const result = await next({ input, ctx }); const durationMs = Date.now() - start; console.log("[${type}] ${path} - ${durationMs}ms"); if (result.ok) { console.log("Result data:", result.data); } else { console.error('Error:', result.error); } return result; }); const appRouter = router({ hello: publicProcedure .query(() => { return "hello world"; }), eventStream: publicProcedure.subscription(() => { return observable((emit) => { const onMessage = (data: string) => { emit.next(data); }; emitter.on('push', onMessage); return () => { emitter.off('push', onMessage); }; }); }), }).middleware(loggerMiddleware); export type AppRouter = typeof appRouter; """ ### 2.3. Dependency Injection (DI) **Standard:** Consider using a DI container for managing dependencies, especially in larger applications. * **Do This:** Use libraries like "tsyringe" or "inversify" to manage dependencies and inject them into your tRPC resolvers. * **Don't Do This:** Hardcode dependencies within resolvers, as this makes testing and refactoring difficult. **Why:** Dependency Injection promotes loose coupling, making your code more modular, testable, and maintainable. **Example (tsyringe):** """typescript import 'reflect-metadata'; import { injectable, container } from 'tsyringe'; import { publicProcedure, router } from './trpc'; @injectable() class UserService { getUser(id: string) { return { id, name: 'Example User' }; } } const userRouter = router({ getUser: publicProcedure .query(() => { const userService = container.resolve(UserService); return userService.getUser("ExampleID"); }), }); export type UserRouter = typeof userRouter; """ ### 2.4 Caching Strategies **Standard:** Implement comprehensive caching strategies to optimize data retrieval and reduce database load. * **Do This:** Implement server-side caching using tools like Redis or Memcached to cache frequently accessed data, use React Query's client-side caching, and configure appropriate cache invalidation strategies. * **Don't Do This:** Disable caching altogether or use overly simplistic caching without considering invalidation strategies. **Why:** Caching significantly improves application performance, reduces database load, and enhances responsiveness. Server-side caching avoids redundant database queries, while client-side caching reduces network requests. ### 2.5 Rate Limiting **Standard:** Implement rate limiting to protect your tRPC API from abuse and ensure fair usage. * **Do This:** Use middleware to limit the number of requests from a single IP address or user within a specific time window. Libraries like "rate-limit" and stores like Redis can be used to implement rate limiting effectively. * **Don't Do This:** Expose your tRPC API without any form of rate limiting. **Why:** Rate limiting prevents malicious actors from overwhelming your server and ensures that legitimate users have a good experience. It's a crucial security measure for any public-facing API. ## 3. Common Anti-Patterns and Mistakes * **Over-fetching:** Avoid fetching more data than you need in your resolvers. Implement field selection or specific data transfer objects (DTOs). * **Tight Coupling:** Keep your resolvers independent of specific database implementations or external services. Abstract dependencies using interfaces and dependency injection. * **Ignoring Errors:** Handle errors properly in your resolvers and return meaningful error messages to the client. Use try-catch blocks and error logging. * **Complex Logic in Resolvers:** Keep your resolvers lean and focused on data retrieval and transformation. Move complex business logic to separate services or modules. * **Lack of Testing:** Write unit tests for your resolvers to ensure they function correctly and handle edge cases. * **Not using Middlewares:** Middlewares promote code-reusability by handling cross-cutting concerns like authentication, authorization, and logging. ## 4. Performance Optimization * **Batching:** Use data loaders for batching database queries. This prevents the N+1 problem and significantly improves performance. * **Indexing:** Ensure your database tables have appropriate indexes to speed up query performance. * **Caching (as mentioned previously):** Client-side and Server-side. * **Code Splitting:** Split your tRPC router into smaller, manageable modules to reduce initial loading time. By adhering to these tooling and ecosystem standards, you can build robust, scalable, and maintainable tRPC applications. Remember to stay up-to-date with the latest tRPC releases and best practices to take full advantage of its capabilities.
# Core Architecture Standards for tRPC This document outlines the core architectural standards for tRPC applications. It provides guidelines on project structure, organization principles, and preferred patterns to ensure maintainability, performance, and security. These standards are based on the latest versions of tRPC and modern best practices. ## 1. Project Structure and Organization A well-structured project is crucial for maintainability and scalability. tRPC projects especially benefit from a clear separation of concerns, as they often involve both client and server-side code tightly integrated through type-safe APIs. ### 1.1. Standard Directory Structure Adopt a consistent directory structure to promote discoverability and maintainability. **Do This:** """ project-root/ ├── src/ │ ├── server/ # Server-side code │ │ ├── routers/ # tRPC routers │ │ │ ├── auth.ts # Authentication router │ │ │ ├── user.ts # User management router │ │ │ └── index.ts # Root router (combining all routers) │ │ ├── context.ts # tRPC context creation │ │ ├── trpc.ts # tRPC initialization and helpers │ │ └── index.ts # Server entry point │ ├── client/ # Client-side code (e.g., React components, hooks) │ │ ├── components/ # Reusable UI components │ │ ├── hooks/ # Custom React hooks │ │ ├── utils/ # Client-side utilities │ │ ├── trpc.ts # tRPC client initialization │ │ └── index.tsx # Client entry point │ ├── shared/ # Code shared between client and server │ │ ├── schemas/ # Zod schemas for data validation │ │ └── utils.ts # Shared utility functions │ ├── utils/ # General utility functions, types, or constants │ ├── app.d.ts # Type definitions. For Next.js, this is <root>/app/ │ └── index.ts # Main entry point ├── .env # Environment variables ├── package.json ├── tsconfig.json └── README.md """ **Don't Do This:** * Mixing client and server code within the same directory. * Placing all tRPC routers in a single file. * Lacking a clear separation of concerns in the codebase. * Ignoring a dedicated folder for reusable components and hooks. **Why This Matters:** A consistent structure makes it easier for developers to locate specific files and understand the project's overall organization. Separation of "/client" and "/server" code prevents accidental inclusion of server-side dependencies in client-side bundles. Shared code goes into "/shared" to minimise duplicated code. ### 1.2. Modular Router Design Break down your tRPC API into smaller, manageable routers based on domain or functionality. **Do This:** Create separate router files for different parts of your API (e.g., "auth.ts", "user.ts", "posts.ts"). """typescript // server/routers/auth.ts import { t } from '../trpc'; import { z } from 'zod'; export const authRouter = t.router({ getSession: t.procedure.query(({ ctx }) => { return ctx.session; }), createUser: t.procedure .input(z.object({ name: z.string(), email: z.string().email() })) .mutation(async ({ input }) => { // Create user in database return { success: true }; }), }); // server/routers/user.ts import { t } from '../trpc'; export const userRouter = t.router({ getUserById: t.procedure .input(z.string()) .query(async ({ input }) => { // Fetch user from database return { id: input, name: 'Example User' }; }), }); // server/routers/index.ts import { authRouter } from './auth'; import { userRouter } from './user'; import { t } from '../trpc'; export const appRouter = t.router({ auth: authRouter, user: userRouter, }); export type AppRouter = typeof appRouter; """ **Don't Do This:** * Defining all tRPC procedures in a single, monolithic router file. * Creating routers that are excessively large and difficult to navigate. * Failing to group related procedures into logical routers. **Why This Matters:** Modular routers improve code organization and make it easier to understand and maintain your API. It reduces the complexity of a single file and improves code navigation, making it simpler to understand the API's functions. ### 1.3. Shared Code Directory Centralize code shared between the client and server in a dedicated "shared/" directory. **Do This:** Use Zod schemas for data validation and define them in the "shared/schemas/" directory. """typescript // shared/schemas/user.ts import { z } from 'zod'; export const userSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), }); export type User = z.infer<typeof userSchema>; """ """typescript // server/routers/user.ts import { t } from '../trpc'; import { userSchema } from '../../shared/schemas/user'; export const userRouter = t.router({ getUserById: t.procedure .input(z.string()) .query(async ({ input }) => { // Fetch user from database const user:User = {id: input, name: 'Example User', email:'test@email.com'}; //example for demo return userSchema.parse(user); //Validate the data }), }); """ """typescript // client/hooks/use-user.ts import { trpc } from '../trpc'; import {User} from '../../shared/schemas/user' export function useUser(userId: string) { const { data, isLoading, error } = trpc.user.getUserById.useQuery(userId); if(data){ const parsedData: User = data //Typesafe assertion. Typescript knows returned 'data' is the proper type! } return { user: data, isLoading, error }; } """ **Don't Do This:** * Duplicating validation schemas or types between the client and server. * Importing server-side code directly into client-side components (without going through tRPC). * Ignoring the concept of code sharing, which leads to repetition. **Why This Matters:** Sharing code reduces redundancy, ensures consistency between client and server, and improves maintainability by centralizing common logic. Using Zod schemas prevents type mismatches between client and server, and shared validation assures the data is correctly validated at BOTH points. ## 2. tRPC Context and Middleware tRPC context and middleware are critical for managing dependencies, authentication, and authorization. ### 2.1. Centralized Context Creation Create a centralized context function that provides access to dependencies like database connections, authentication status, and other shared resources. **Do This:** """typescript // server/context.ts import { inferAsyncReturnType } from '@trpc/server'; import { CreateNextContextOptions } from '@trpc/server/adapters/next'; /** * Creates context for an incoming request * @link https://trpc.io/docs/context */ export const createContext = async (opts: CreateNextContextOptions) => { const { req, res } = opts; // Get the session from the server using the unstable_getServerSession wrapper function const session = {user:{name:'Test User'}}; //Replace once authentication is setup properly return { session, req, res, prisma: {}, // Add your Prisma or database instance here }; }; export type Context = inferAsyncReturnType<typeof createContext>; """ **Don't Do This:** * Creating context directly within each procedure. * Missing including request and/or response objects, making it hard to get metadata. * Hardcoding dependencies within procedures, making them difficult to test. **Why This Matters:** Centralized context creation promotes code reuse and simplifies dependency injection. It also allows to easily add logging, instrumentation or other cross cutting concerns. This keeps individual procedures focused on their specific business logic. ### 2.2. Authentication and Authorization Middleware Implement authentication and authorization using tRPC middleware to secure your API endpoints. **Do This:** """typescript // server/trpc.ts import { initTRPC, TRPCError } from '@trpc/server'; import { Context } from './context'; const t = initTRPC.context<Context>().create(); const middleware = t.middleware; const isAuthed = middleware(async (opts) => { const { ctx } = opts; if (!ctx.session?.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return opts.next({ ctx: { user: ctx.session.user, }, }); }); export const router = t.router; export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use(isAuthed); export const tRPC = t; //Export the t variable export const createCallerFactory = t.createCallerFactory; //helper to allow testing without http request export { t }; //Added to support older coding styles. """ """typescript // server/routers/user.ts import { protectedProcedure, router } from '../trpc'; export const userRouter = router({ getSecretMessage: protectedProcedure.query(() => { return "You are authenticated"; }), }); """ **Don't Do This:** * Performing authentication or authorization checks directly within procedures. * Exposing sensitive data or operations without proper authentication and authorization. * Using insecure methods to store or transmit authentication credentials. **Why This Matters:** tRPC middleware enables you to centralize authentication and authorization logic. Securing your API endpoints prevents unauthorized access to sensitive data and operations. ### 2.3. Custom Middleware for Logging and Error Handling Use custom middleware to add logging and error handling to your tRPC API. **Do This:** """typescript import { initTRPC, TRPCError } from '@trpc/server'; import { Context } from './context'; const t = initTRPC.context<Context>().create(); const middleware = t.middleware; const logger = middleware(async (opts) => { //Middleware for logging console.log('┌── tRPC Request'); console.log("│ Path: ${opts.path}"); console.log("│ Input: ${JSON.stringify(opts.input)}"); const result = await opts.next(); console.log('└── tRPC Response'); if (result.ok) { console.log(' │ OK:', result.data); } else { console.log(' │ Error:', result.error); } return result; }); const tRPC = initTRPC.context<Context>().middleware(logger).create(); export const publicProcedure = tRPC.procedure; const isAuthed = middleware(async (opts) => { //...authentication logic... }) export const router = t.router; export const protectedProcedure = t.procedure.use(isAuthed); export {tRPC} """ **Don't Do This:** * Relying solely on client-side logging or error handling. * Missing capturing errors in middleware, resulting in unhandled exceptions. * Missing including request and/or response, making it difficult to triage issues. * Logging sensitive information like passwords or API keys. **Why This Matters:** Middleware enables you to centralize logging and error handling logic. Centralized logging facilitates monitoring and debugging and error handling improves the reliability of your API. ## 3. Data Validation with Zod Zod schemas are essential for validating data passed to and from tRPC procedures. ### 3.1. Strict Input Validation Use Zod schemas to strictly validate all input data to your tRPC procedures. **Do This:** """typescript // server/routers/user.ts import { z } from 'zod'; import {protectedProcedure, router } from '../trpc'; const userCreateSchema = z.object({ name: z.string().min(2).max(50), email: z.string().email(), }); export const userRouter = router({ createUser: protectedProcedure .input(userCreateSchema) .mutation(async ({ input }) => { // Create user in database using validated input return { success: true }; }), }); """ **Don't Do This:** * Skipping input validation, which can lead to data corruption and security vulnerabilities. * Relying solely on client-side validation, as it can be bypassed. * Using weak or incomplete validation schemas. **Why This Matters:** Strict input validation prevents invalid data from entering your application. This improves data integrity and prevents common security vulnerabilities, such as injection attacks. ### 3.2. Output Validation and Transformation Use Zod schemas to validate and transform the output data from your tRPC procedures. **Do This:** """typescript // server/routers/user.ts import { z } from 'zod'; import { protectedProcedure, router } from '../trpc'; const userSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), }); export const userRouter = router({ getUserById: protectedProcedure .input(z.string()) .output(userSchema) //This is relatively new, so ensure your tRPC is up to date. .query(async ({ input }) => { // Fetch user from database const user = { id: input, name: 'Example User', email: 'test@example.com' }; return userSchema.parse(user); // Validate the data }), }); """ **Don't Do This:** * Returning raw data without validation or transformation. * Assuming that data returned from the database or other sources is always valid. * Skipping output validation, especially for public APIs. **Why This Matters:** Validating output ensures that the data returned by your API is consistent with your schema. This improves data integrity, prevents unexpected errors on the client-side and provides type safety throughout your application. Output validation and Transformation is now built into tRPC as of v10.44.0. ### 3.3. Reusable Schemas Create reusable Zod schemas for common data structures, such as user profiles, posts, and comments, and store them in the "/shared" directory. **Do This:** """typescript // shared/schemas/user.ts import { z } from 'zod'; export const userSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), }); export type User = z.infer<typeof userSchema>; """ **Don't Do This:** * Duplicating schemas across multiple routers or components. * Defining complex schemas directly within procedures, which can make them difficult to maintain. * Failing to create reusable schemas for common data structures. **Why This Matters:** Reusable schemas reduce redundancy, ensure consistency across your application and improve maintainability by centralizing schema definitions. DRY: Don't Repeat Yourself. ## 4. Testing Robust testing is essential for maintaining the quality of your tRPC API. ### 4.1. Unit Testing for Procedures Write unit tests for individual tRPC procedures to ensure they function correctly. **Do This:** """typescript // server/routers/user.test.ts import { appRouter } from './index'; // Import your root router import { createCallerFactory } from '../trpc'; import { describe, expect, it } from 'vitest'; describe('User Router', () => { it('should get a user by ID', async () => { const caller = createCallerFactory({} as any)( { user: {name: 'test user'} }); //Mock Context const result = await caller.user.getUserById("123") expect(result.name).toBe('Example User'); expect(result.id).toBe('123'); }); }); """ **Don't Do This:** * Skipping unit tests for tRPC procedures. * Writing tests that are too broad or too focused on implementation details. * Ignoring edge cases or error conditions in your tests. **Why This Matters:** Unit tests ensure that individual procedures function correctly. Testing mitigates regressions, reduces bugs, and improves the overall reliability of your API. ### 4.2. Integration Testing for Routers Write integration tests to verify the interaction between different tRPC routers and procedures. **Do This:** """typescript // server/routers/index.test.ts import { appRouter } from './index'; // Import your root router import { createCallerFactory } from '../trpc'; import { describe, expect, it } from 'vitest'; describe('Root Router', () => { it('should call protected procedure', async () => { const caller = createCallerFactory({} as any)( { user: {name: 'test user'} }); //Mock the authentication. const result = await caller.user.getSecretMessage(); expect(result).toBe('You are authenticated'); }); }); """ **Don't Do This:** * Relying solely on unit tests, which may not catch integration issues. * Writing integration tests that are too complex or difficult to maintain. * Failing to test the interaction between different routers and procedures. **Why This Matters:** Integration tests verify the interaction between different parts of your API. This prevents integration issues, ensures that routers and procedures work together correctly and improves the overall stability of your application. ### 4.3. End-to-End Testing End-to-end (E2E) tests ensure that your entire application, including your tRPC API, works as expected from the user's perspective. Because tRPC is inherently full stack, E2E tests are extremely valuable. **Do This:** * Use tools like Playwright or Cypress to write E2E tests that simulate user interactions with your application. * Test common user flows, such as login, data entry, and form submission. * Verify that data is correctly displayed and persisted in the database. **Don't Do This:** * Skipping E2E tests, which may miss critical integration issues. * Writing E2E tests that are too brittle or difficult to maintain. * Failing to test the entire application flow from the user's perspective. **Why This Matters:** E2E tests verify that your entire application works as expected. Testing improves user experience, reduces the risk of critical bugs in production and ensures the overall quality of your application. Even for teams that don't fully practice TDD, E2E testing can be tremendously helpful. ## 5. Performance Optimization Optimize your tRPC API for performance to ensure a smooth and responsive user experience. ### 5.1. Data Fetching Strategies Use efficient data fetching strategies to minimize the number of database queries and network requests. **Do This:** * Use techniques like query batching and caching to reduce the number of database queries. * Implement data pagination and filtering to retrieve only the necessary data. * Use appropriate indexes in your database to optimize query performance. **Don't Do This:** * Fetching unnecessary data, which can slow down your API and increase network traffic. * Performing multiple database queries when a single query would suffice. * Ignoring the performance impact of database queries and network requests. **Why This Matters:** Optimized data fetching improves API performance, reduces network traffic, and provides a better user experience. Carefully consider if batching, caching, pagination, or other optimizations might be beneficial in each specific case. ### 5.2. Caching Strategies Implement caching strategies to reduce the load on your server and improve response times. **Do This:** * Use client-side caching to store frequently accessed data in the user's browser. * Implement server-side caching using tools like Redis or Memcached to store API responses. * Use appropriate cache invalidation strategies to ensure that cached data is up-to-date. **Don't Do This:** * Caching sensitive data without proper security measures. * Using overly aggressive caching strategies that can lead to stale data. * Ignoring the performance benefits of caching. **Why This Matters:** Caching reduces the load on servers by avoiding unneccessary processing when data doesn't change frequently. This improves API performance and response times. You must implement appropriate cache invalidation to ensure clients are looking at updated data. ### 5.3. Code Splitting Use code splitting to reduce the initial load time of your client-side application. **Do This:** * Split your code into smaller bundles that can be loaded on demand. * Use dynamic imports to load components and modules only when they are needed. * Optimize your build process to minimize the size of your JavaScript bundles. **Don't Do This:** * Creating large, monolithic JavaScript bundles that can slow down your application. * Ignoring the performance benefits of code splitting. * Over complicating code splitting, which can result in increased complexity and maintenance overhead. **Why This Matters:** Code splitting reduces the initial load time of the application. This significantly improves the user experience, especially on mobile devices, and reduces the time to first meaningful paint. ## 6. Security Best Practices Implement security best practices to protect your tRPC API from common vulnerabilities. ### 6.1. Input Validation Strictly validate all input data to prevent injection attacks and other security vulnerabilities (as covered in section 3.1). ### 6.2. Authentication and Authorization Implement robust authentication and authorization mechanisms to protect your API endpoints (as covered in section 2.2). ### 6.3. Rate Limiting Implement rate limiting to prevent abuse and denial-of-service attacks. **Do This:** * Limit the number of requests that a user or IP address can make within a certain time period. * Use tools like Redis or Memcached to store rate limiting data. * Monitor your API for suspicious activity and adjust rate limits accordingly. **Don't Do This:** * Failing to implement rate limiting, which can leave your API vulnerable to abuse. * Using overly restrictive rate limits that can negatively impact legitimate users. * Ignoring the importance of rate limiting as a security measure. **Why This Matters:** Rate limiting prevents abuse, protects your API from denial-of-service attacks, and improves the overall security and stability of your application. ### 6.4. CORS Configuration Configure Cross-Origin Resource Sharing (CORS) to prevent unauthorized access to your API from different domains. **Do This:** * Specify the allowed origins in your CORS configuration. * Use wildcard origins sparingly, as they can create security vulnerabilities. * Ensure that your CORS configuration is properly configured in your production environment. **Don't Do This:** * Disabling CORS entirely, which can leave your API vulnerable to cross-site scripting (XSS) attacks. * Using overly permissive CORS configurations that allow unauthorized access to your API. * Ignoring the importance of CORS as a security measure. **Why This Matters:** CORS prevents unauthorized access, protects your API from cross-site scripting (XSS) attacks, and improves the overall security of your application. This document provides a foundation for establishing coding standards for tRPC applications. By adhering to these guidelines, development teams can build robust, maintainable, and secure APIs. As tRPC and related technologies evolve, these standards should be periodically reviewed and updated to reflect the latest best practices.