# Code Style and Conventions Standards for tRPC
This document outlines the code style and conventions for tRPC (TypeScript Remote Procedure Call) applications. Adhering to these standards will promote code consistency, readability, maintainability, and collaboration within development teams. These standards are aligned with the latest tRPC versions and best practices.
## 1. General Principles
### 1.1. Consistency
* **Standard:** Maintain a consistent code style throughout the entire project. Inconsistencies make code harder to read and understand.
* **Why:** Consistency reduces cognitive load for developers, allowing them to focus on the logic rather than the style.
* **How:** Use automated formatters and linters (e.g., Prettier, ESLint with recommended tRPC configs) to enforce a uniform style.
### 1.2. Readability
* **Standard:** Write code that is easy to read and understand, even by someone unfamiliar with the codebase.
* **Why:** Readability improves collaboration, reduces debugging time, and makes code easier to maintain.
* **How:** Use meaningful names, add comments when necessary, and keep functions and components concise.
### 1.3. Maintainability
* **Standard:** Design code in a modular and organized manner, making it easy to modify and extend.
* **Why:** Maintainable code reduces the risk of introducing bugs when making changes and simplifies future development.
* **How:** Follow SOLID principles, use design patterns appropriately, and write thorough tests.
### 1.4. Conventions
* **Standard:** Follow established naming and coding conventions to enhance code clarity and predictability.
* **Why:** Conventions provide a common language for developers, making it easier to understand the purpose and function of code elements.
* **How:** Consistently apply naming conventions for variables, functions, components, and files.
## 2. Formatting
### 2.1. Line Length
* **Standard:** Limit line length to a reasonable number of characters (e.g., 80-120) to improve readability.
* **Why:** Long lines are hard to read, especially on smaller screens.
* **How:** Configure your code editor or formatter to automatically wrap lines that exceed the limit.
### 2.2. Indentation
* **Standard:** Use consistent indentation (e.g., 2 or 4 spaces) to visually represent code structure. Do not use tabs.
* **Why:** Proper indentation clearly shows the hierarchy of code blocks and statements.
* **How:** Configure your code editor to automatically indent code based on language conventions. Use ".editorconfig" to enforce rules across your team.
### 2.3. Spacing
* **Standard:** Use whitespace to improve code clarity and separate logical blocks of code.
* **Why:** Whitespace makes code less dense and easier to scan.
* **How:**
* Add a blank line between function declarations.
* Use spaces around operators and after commas.
* Add whitespace to separate logical sections within a function.
### 2.4. File Structure
* **Standard:** Organize files into logical directories based on feature, module, or functionality.
* **Why:** A clear file structure makes it easier to find and manage code.
* **How:**
* Use descriptive directory names.
* Group related files together.
* Consider using an architectural pattern (e.g., feature-sliced design).
### 2.5. Prettier Configuration
* **Standard:** Enforce consistent formatting using Prettier.
* **Why:** Automated formatting reduces style-related debates and ensures a unified look.
* **How:**
Create a ".prettierrc.js" file in the root of your project:
"""javascript
// .prettierrc.js
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
useTabs: false,
bracketSpacing: true,
arrowParens: 'always',
};
"""
Include a ".prettierignore" to exclude compiled files, node_modules, etc.
"""
# .prettierignore
node_modules
dist
build
.next
"""
## 3. Naming Conventions
### 3.1. General Naming
* **Standard:** Use descriptive and meaningful names for variables, functions, and components.
* **Why:** Clear names make code self-documenting and reduce the need for excessive comments.
* **How:** Choose names that accurately reflect the purpose and function of the code element.
### 3.2. Variables
* **Standard:** Use "camelCase" for variable names.
* **Example:** "const userProfile = { ... };"
* **Standard:** Use plural names for arrays.
* **Example:** "const users: User[] = [];"
### 3.3. Functions
* **Standard:** Use "camelCase" for function names.
* **Standard:** Aim for descriptive names using verbs to clearly indicate what the function *does*.
* **Example:** "function getUserById(id: string): User { ... }"
### 3.4. Components (React, if applicable)
* **Standard:** Use "PascalCase" for component names.
* **Example:** "const UserProfile: React.FC = () => { ... };"
### 3.5. Types and Interfaces
* **Standard:** Use "PascalCase" for type and interface names.
* **Standard:** Append "Type" or "Interface" for clarity (optional, but encouraged).
* **Example:**
"""typescript
interface UserInterface {
id: string;
name: string;
}
"""
### 3.6. Constants
* **Standard:** Use "UPPER_SNAKE_CASE" for constant names.
* **Example:** "const MAX_USERS = 100;"
### 3.7. tRPC Router and Procedure Names
* **Standard:** Use "camelCase" for tRPC router and procedure names. This is very important for consistent API naming.
* **Standard:** Procedures should be named descriptively, reflecting the data they operate on.
* **Example:**
"""typescript
const appRouter = trpc.router({
getUserById: trpc.procedure
.input(z.string())
.query(async ({ input }) => {
// ...
}),
createUser: trpc.procedure
.input(CreateUserSchema)
.mutation(async ({ input }) => {
// ...
}),
});
export type AppRouter = typeof appRouter;
"""
**Don't Do This:**
"""typescript
// Avoid vague names
const appRouter = trpc.router({
doSomething: trpc.procedure // Vague and meaningless.
.input(z.string())
.query(async ({ input }) => {
// ...
}),
});
"""
## 4. tRPC Specific Conventions
### 4.1. Input Validation with Zod
* **Standard:** Always use Zod for input validation.
* **Why:** Zod provides type-safe validation and automatically generates TypeScript types from your schemas.
* **How:** Define Zod schemas for all tRPC procedure inputs.
"""typescript
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
const CreateUserSchema = z.object({
name: z.string().min(3),
email: z.string().email(),
age: z.number().min(18).optional(),
});
export const appRouter = router({
createUser: publicProcedure
.input(CreateUserSchema)
.mutation(async ({ input }) => {
// Input is guaranteed to match the schema and is type-safe!
const { name, email, age } = input; // No need for manual type casting
// Create user logic here
return { success: true, user: { name, email, age } };
}),
getUserCount: publicProcedure
.query(async () => {
// Fetch count of the users
return 42;
}),
});
"""
**Don't Do This:**
"""typescript
// Avoid manual validation. It is error-prone and not type-safe.
const appRouter = trpc.router({
createUser: publicProcedure
.input((val: any) => { // Implicit "any" type!
if (!val.name || val.name.length < 3) {
throw new Error('Invalid name');
}
return val as { name: string; email: string }; // Manual type casting!
})
.mutation(async ({ input }) => {
const { name, email } = input; // Need to manually assert types
// ...
}),
});
"""
### 4.2. Error Handling
* **Standard:** Use tRPC's "TRPCError" for custom error handling and propagate errors to the client.
* **Why:** Provides a structured way to handle errors and ensures consistent error reporting.
* **How:** Throw "TRPCError" with appropriate error codes and messages.
"""typescript
import { TRPCError } from '@trpc/server';
import { publicProcedure, router } from './trpc';
import { z } from 'zod';
const appRouter = router({
getUser: publicProcedure
.input(z.string())
.query(async ({ input }) => {
const user = await db.findUserById(input); // Assume db is your database client
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: "User with id '${input}' not found",
});
}
return user;
}),
});
"""
**Don't Do This:**
"""typescript
// Avoid generic errors. They don't provide specific information to the client.
const appRouter = trpc.router({
getUser: publicProcedure
.input(z.string())
.query(async ({ input }) => {
try {
const user = await db.findUserById(input);
if (!user) {
throw new Error('User not found'); // Generic error
}
return user;
} catch (err) {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR' }); // Still too generic! Client doesn't know *why*
}
}),
});
"""
* **Standard:** Use appropriate HTTP status codes within "TRPCError", such as "NOT_FOUND", "BAD_REQUEST", "FORBIDDEN", etc.
### 4.3. Middleware
* **Standard:** Use tRPC middleware for cross-cutting concerns like authentication, authorization, and logging.
* **Why:** Middleware provides a reusable way to apply logic to multiple procedures.
* **How:** Create reusable middleware functions and apply them to your routers or procedures.
"""typescript
import { publicProcedure, router, middleware } from './trpc';
import { TRPCError } from '@trpc/server';
// Example Authentication Middleware
const isLoggedIn = middleware(async ({ ctx, next }) => {
if (!ctx.user?.id) { // Assuming you have user data in your context
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
user: ctx.user, // Optionally refine user data
},
});
});
const protectedProcedure = publicProcedure.use(isLoggedIn);
export const appRouter = router({
// This procedure is only accessible to logged-in users
getSecretData: protectedProcedure.query(() => {
return "This is secret data!";
}),
});
"""
### 4.4. Context
* **Standard:** Leverage the tRPC context to share data between middleware and procedures, such as database connections, authentication information, or user sessions.
* **Why:** Centralizes shared resources and avoids passing them as arguments to every procedure.
* **How:** Define a context creation function and pass it to your tRPC server. In Next.js, this is passed when creating the API route.
"""typescript
import { inferAsyncReturnType } from '@trpc/server';
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getAuth } from '@clerk/nextjs/server'
/**
* Creates context for an incoming request
* @link https://trpc.io/docs/context
*/
export const createContext = async (opts: CreateNextContextOptions) => {
const { req } = opts;
// Use Clerk for authentication
const auth = getAuth(req);
// Get the user ID from Clerk
const userId = auth.userId;
console.log('userId:', userId);
return {
userId,
};
};
export type Context = inferAsyncReturnType;
"""
### 4.5. Data Transformation and Serialization
* **Standard:** Consider using data transformation libraries (e.g., SuperJSON) for complex data types to ensure proper serialization and deserialization.
* **Why:** JavaScript's built-in "JSON.stringify" may not handle all data types correctly (e.g., Dates, BigInts).
* **How:** Integrate SuperJSON into your tRPC setup for seamless data transformation.
"""typescript
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
const t = initTRPC.create({
transformer: superjson, // Use SuperJSON for data transformation
errorFormatter({ shape }) {
return shape;
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
"""
### 4.6. API Versioning (If Required)
* **Standard:** If your API requires versioning, incorporate it into your router structure.
* **Why:** Allows you to introduce breaking changes without affecting existing clients.
* **How:** Create separate routers for each version of your API (e.g., "appRouterV1", "appRouterV2"). Consider using URL prefixes.
### 4.7. Batching and Links
* **Standard:** Utilize tRPC's batching and links features to optimize client-server communication.
* **Why:** Reduces the number of network requests and improves performance.
* **How:** Use "httpBatchLink" on the client to automatically batch requests to the server.
"""typescript
import { httpBatchLink } from '@trpc/client';
import { AppRouter } from './path/to/server';
const trpc = createTRPCReact({
config() {
return {
transformer: superjson,
links: [
httpBatchLink({
url: '/api/trpc', // Your tRPC API endpoint
}),
],
};
},
ssr: false,
});
"""
### 4.8. Testing
* **Standard:** Write unit and integration tests for your tRPC procedures and middleware.
* **Why:** Ensures that your API is working as expected and prevents regressions.
* **How:** Use testing frameworks like Jest or Vitest and mock dependencies as needed. Consider using "supertest" to test your API endpoints.
### 4.9. Documentation
* **Standard:** Document your tRPC API using tools like OpenAPI or Swagger. (tRPC integrates with OpenAPI).
* **Why:** Provides a clear description of your API endpoints, inputs, and outputs.
* **How:** Generate OpenAPI schemas from your tRPC router using the "@trpc/openapi" adapter.
## 5. Security Considerations
### 5.1. Input Validation
* **Standard:** Always validate user inputs, even if you're using Zod on the server. Client-side validation provides a better user experience and reduces unnecessary requests.
* **Why:** Prevents injection attacks, data corruption, and unexpected behavior.
* **How:** Use Zod schemas on both the client and server to ensure consistent validation.
### 5.2. Authentication and Authorization
* **Standard:** Implement robust authentication and authorization mechanisms to protect your API endpoints.
* **Why:** Prevents unauthorized access to sensitive data and functionality.
* **How:** Use tRPC middleware to enforce authentication and authorization rules based on user roles or permissions. Integrate with authentication providers like Clerk, Auth0, or Firebase Authentication.
### 5.3. Rate Limiting
* **Standard:** Implement rate limiting to prevent abuse and denial-of-service attacks.
* **Why:** Protects your API from being overwhelmed by excessive requests.
* **How:** Use middleware to track the number of requests from a given IP address or user and block requests that exceed the limit.
### 5.4. CORS Configuration
* **Standard:** Configure CORS (Cross-Origin Resource Sharing) to allow requests only from trusted origins.
* **Why:** Prevents malicious websites from making requests to your API on behalf of unsuspecting users.
* **How:** Set the "Access-Control-Allow-Origin" header in your HTTP responses or use a CORS middleware to handle CORS configuration.
### 5.5. Secrets Management
* **Standard:** Store sensitive information (API keys, database passwords, etc.) securely using environment variables or a secrets management system.
* **Why:** Prevents sensitive information from being exposed in your codebase.
* **How:** Use ".env" files (for development) and environment variables (for production) to store secrets. **DO NOT COMMIT ".env" files to your repository.**
## 6. Anti-Patterns to Avoid
* **Over-fetching:** Avoid returning more data than is needed by the client.
* **Under-fetching:** Avoid requiring the client to make multiple requests to fetch all the necessary data.
* **Nested Procedures:** Avoid creating deeply nested tRPC routers, as they can be difficult to maintain.
* **Ignoring Errors:** Always handle errors properly and provide informative error messages to the client.
* **Global State:** Avoid relying on global state within your tRPC procedures.
## 7. Modern Best Practices
* **Server Actions (Next.js):** When using tRPC with Next.js, consider using Server Actions for mutations to simplify data fetching and form handling.
* **Optimistic Updates:** Implement optimistic updates on the client to provide a more responsive user experience.
* **Data Caching:** Utilize server-side caching (e.g., Redis, Memcached) to improve performance and reduce database load.
* **Edge Functions:** Deploy your tRPC API to edge functions (e.g., Vercel Edge Functions, Cloudflare Workers) to reduce latency and improve performance for users around the world.
* **tRPC + Next.js App Router:** For new Next.js projects, use the App Router directory structure for improved performance. Also, consider using React Server Components with tRPC where possible for improved data fetching.
By adhering to these code style and convention standards, you can create tRPC applications that are consistent, readable, maintainable, and secure. This will improve collaboration, reduce development time, and ensure the long-term success of your projects.
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.
# 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<UserState>((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 ( <div> <h1>{user.name}</h1> <p>ID: {user.id}</p> </div> ); } 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 ( <form onSubmit={handleSubmit}> <input type="text" value={newName} onChange={(e) => setNewName(e.target.value)} placeholder="New Name" /> <button type="submit">Update Name</button> </form> ); } 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 ( <button onClick={() => addLike({ postId: postId })} disabled={isLoading}> Like </button> ); } """ ## 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 ( <QueryClientProvider client={queryClient}> {/* your app */} </QueryClientProvider> ) } """ ### 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 ( <button onClick={() => deletePost({ postId: postId })}>Delete</button> ); } """ ### 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 ( <div> <h1>{data.title}</h1> <p>{data.content}</p> </div> ); } 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 ( <div> <h2>{user.name}'s Posts</h2> {posts?.map(post => ( <div key={post.id}> <h3>{post.title}</h3> <p>{post.content}</p> </div> ))} </div> ); } 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.