Learning tRPC - API Security, Rate Limiting & Best Practices
Episode 12 of 19

Learning tRPC - API Security, Rate Limiting & Best Practices

This episode strengthens the tRPC API's defenses: protection against injection, overfetch, and payload abuse, rate limiting at the router or adapter level, as well as TLS, CSP, and input data sanitization best practices.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Security is not a final feature — it is a layer woven into the design. Episode 12 discusses the real threats that hit tRPC APIs and how to repel them: injection, overfetch, and payload abuse, then rate limiting, and closes with best practices such as TLS, CSP, and input sanitization.

Because tRPC exposes procedures directly, its attack surface differs from REST. Understanding these weak points lets you close them from the start.

Protection from Injection, Overfetch, and Payload Abuse

Injection on Input

The first threat is data interpreted as a command — for example a string that enters a database query. Zod restricts the shape of data, but sanitization remains the resolver's responsibility:

Sanitizing before entering the database
import { z } from "zod";
 
cari: t.procedure
  .input(z.object({ q: z.string().min(1).max(100) }))
  .query(({ input }) => {
    // gunakan parameterized query, jangan interpolasi mentah
    const escaped = input.q.replace(/[\s'"\\]/g, " ");
    return users.filter((u) => u.nama.toLowerCase().includes(escaped.toLowerCase()));
  }),

The basic rule: validate the shape with zod (type, length, format), then query the database using parameterized statements or an ORM. Never build a query with raw interpolated strings — this prevents SQL/NoSQL injection.

Overfetch and Underfetch

Overfetch: the client requests more data than it needs. Because procedure output is determined by the resolver, avoid returning the full database object if the client only needs a few fields — use the output validation from episode 3 and project the required fields.

Underfetch is the opposite: many small round-trips. Combine related data in one procedure so the client does not call repeatedly:

Avoiding overfetch and underfetch
userWithPosts: t.procedure
  .input(z.object({ id: z.number() }))
  .query(({ input }) => {
    const user = users.find((u) => u.id === input.id);
    const posts = posts.filter((p) => p.authorId === input.id);
    return { id: user.id, nama: user.nama, postCount: posts.length };
  }),

The userWithPosts procedure returns only the fields the client needs — one request, nothing excessive.

Payload Abuse

Very large input can consume memory and CPU. Limit it with zod (length, size) and limit the body size at the server level:

Limiting input size
create: t.procedure
  .input(
    z.object({
      judul: z.string().min(3).max(200),
      konten: z.string().min(1).max(10_000),
      tags: z.array(z.string()).max(10),
    }),
  )
  .mutation(({ input }) => simpanPost(input)),

In Express, limit the body parser:

Limiting body size in Express
app.use(express.json({ limit: "1mb" }));

express.json({ limit: "1mb" }) rejects requests with a body larger than 1 megabyte before they enter the tRPC pipeline.

Rate Limiting

Router-Level Rate Limit Middleware

Rate limiting limits access frequency so the API is not flooded. Create simple middleware with in-memory storage:

Rate limit middleware
const rateLimit = t.middleware(({ ctx, next, path }) => {
  const key = `${ctx.user?.id ?? ctx.req?.ip}:${path}`;
  const now = Date.now();
 
  const hits = counters.get(key) ?? [];
  const recent = hits.filter((t) => now - t < 60_000);
 
  if (recent.length >= 30) {
    throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
  }
  recent.push(now);
  counters.set(key, recent);
 
  return next();
});
 
export const limitedProcedure = t.procedure.use(rateLimit);

TRPCError({ code: "TOO_MANY_REQUESTS" }) maps to HTTP 429. An in-memory counter is enough for a single instance, but for multi-instance production use shared storage like Redis.

Adapter-Level Rate Limiting

Rate limiting can also be installed at the HTTP layer before tRPC — for example Express middleware or an edge service like Cloudflare. The advantage: the limit applies to all paths without touching tRPC code, and it runs very close to the user.

Choose according to your needs: tRPC middleware gives per-user and per-procedure control; adapter/edge gives global protection with the best performance. Combining both is a common production approach.

Other Security Best Practices

TLS/HTTPS and Security Headers

All production traffic must go through HTTPS. Behind a proxy, make sure TLS is terminated at the edge and security headers are added:

Basic security headers
app.use((req, res, next) => {
  res.setHeader("Strict-Transport-Security", "max-age=63072000");
  res.setHeader("X-Content-Type-Options", "nosniff");
  res.setHeader("Referrer-Policy", "no-referrer");
  next();
});

Strict-Transport-Security forces the browser to always use HTTPS. The other headers prevent MIME-sniffing-based attacks and referrer leaks.

CSP for the Frontend

Content-Security-Policy restricts the sources of content a page may load — deflecting XSS. Make sure connect-src allows the tRPC endpoint and WebSocket:

Example CSP via header
Content-Security-Policy: default-src 'self'; connect-src 'self' wss://api.contoh.com;

Notice wss://api.contoh.com in connect-src — without it, WebSocket connections from the frontend to the server will be blocked by the browser.

Layered Validation and Sanitization

The best security is layered: zod schema validation rejects malformed input, sanitization cleans data before it enters storage, and authorization limits who can trigger procedures. Never rely on just one layer.

Warning

Memory-based rate limiting does not share state between server instances. In deployments with many instances, use Redis or an edge service so the limit stays consistent across all instances.

Conclusion

Episode 12 strengthens the tRPC API's defenses: repelling injection through validation and sanitization, avoiding overfetch and underfetch with projection and data combining, limiting payload abuse, and enforcing rate limiting at two levels plus TLS and CSP best practices.

Key takeaways:

  • zod validation is not a replacement for sanitization in the resolver.
  • Use parameterized queries to prevent injection.
  • Overfetch is avoided with field projection; underfetch with combined procedures.
  • Limit input size in schemas and the body limit on the server.
  • Rate limit per user with Redis for multi-instance.
  • Enable HTTPS, HSTS, and CSP with a connect-src that includes WebSocket.

In the next episode, episode 13, we will discuss tRPC performance & optimization — optimizing batch requests and reducing round-trips, using dehydrate and hydration in SSR to minimize refetching, and server optimizations like caching, data loaders, and lazy routers.

Learning tRPC - API Security, Rate Limiting & Best Practices | Learning tRPC