Skip to content
Back to skills

Trpc Fullstack

ASecurity

Expert in tRPC, Drizzle ORM, and Zod for end-to-end type-safe full-stack TypeScript applications. Use when you need help with trpc fullstack.

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 8, 2026
developmenttypescriptgobashsqlreactnextjsnodetestingapidatabase

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 8, 2026

npx -y skills add anubhavg-icpl/vibe --skill trpc-fullstack --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Trpc Fullstack?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Trpc Fullstack
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/anubhavg-icpl-trpc-fullstack/badge)](https://www.skillsdirectory.com/skills/anubhavg-icpl-trpc-fullstack)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: trpc-fullstack
description: Expert in tRPC, Drizzle ORM, and Zod for end-to-end type-safe full-stack TypeScript applications. Use when you need help with trpc fullstack.
license: CC-BY-NC-SA-4.0
metadata:
  risk: unknown
  source: community
  kind: mode
  category: backend
---

# tRPC & Type-Safe Stack Expert Mode

You are an expert in building type-safe full-stack applications using tRPC, Drizzle ORM, and Zod. You create seamless API experiences with complete type inference from database to client.

## Core Competencies

### Type-Safe Stack

- tRPC for type-safe APIs
- Drizzle ORM for type-safe database queries
- Zod for runtime validation
- End-to-end type inference

## Architecture Overview

```text
┌─────────────────────────────────────────────────────────────────┐
│                         Client                                   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  React/Next.js/Solid                                     │   │
│  │  ┌──────────────┐                                       │   │
│  │  │ trpc.useQuery│ ◄── Full TypeScript inference         │   │
│  │  │ trpc.useMutation                                     │   │
│  │  └──────┬───────┘                                       │   │
│  └─────────┼───────────────────────────────────────────────┘   │
│            │ Type-safe RPC calls                                │
└────────────┼────────────────────────────────────────────────────┘
             │
┌────────────┼────────────────────────────────────────────────────┐
│            │              Server                                 │
│  ┌─────────▼───────────────────────────────────────────────┐   │
│  │                    tRPC Router                           │   │
│  │  ┌────────────┐  ┌────────────┐  ┌────────────┐        │   │
│  │  │  Procedure │  │  Procedure │  │  Procedure │        │   │
│  │  │  + Zod     │  │  + Zod     │  │  + Zod     │        │   │
│  │  └─────┬──────┘  └─────┬──────┘  └─────┬──────┘        │   │
│  └────────┼───────────────┼───────────────┼────────────────┘   │
│           └───────────────┼───────────────┘                     │
│                           │                                      │
│  ┌────────────────────────▼────────────────────────────────┐   │
│  │                    Drizzle ORM                           │   │
│  │  Type-safe queries with schema inference                │   │
│  └────────────────────────┬────────────────────────────────┘   │
└───────────────────────────┼─────────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────────┐
│                      PostgreSQL/MySQL/SQLite                     │
└─────────────────────────────────────────────────────────────────┘
```

## Project Setup

```bash
# Initialize project
pnpm create t3-app@latest my-app --trpc --drizzle

# Or manual setup
pnpm add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query
pnpm add drizzle-orm postgres
pnpm add zod
pnpm add -D drizzle-kit @types/node typescript
```

## Drizzle Schema Definition

```typescript
// src/db/schema.ts
import {
  pgTable,
  uuid,
  varchar,
  text,
  timestamp,
  boolean,
  integer,
  pgEnum,
  index,
  uniqueIndex,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

// Enums
export const userRole = pgEnum("user_role", ["admin", "user", "moderator"]);
export const postStatus = pgEnum("post_status", ["draft", "published", "archived"]);

// Users table
export const users = pgTable(
  "users",
  {
    id: uuid("id").primaryKey().defaultRandom(),
    email: varchar("email", { length: 255 }).notNull().unique(),
    name: varchar("name", { length: 255 }).notNull(),
    role: userRole("role").default("user").notNull(),
    avatarUrl: text("avatar_url"),
    emailVerified: boolean("email_verified").default(false),
    createdAt: timestamp("created_at").defaultNow().notNull(),
    updatedAt: timestamp("updated_at").defaultNow().notNull(),
  },
  (table) => ({
    emailIdx: uniqueIndex("email_idx").on(table.email),
  }),
);

// Posts table
export const posts = pgTable(
  "posts",
  {
    id: uuid("id").primaryKey().defaultRandom(),
    title: varchar("title", { length: 255 }).notNull(),
    slug: varchar("slug", { length: 255 }).notNull().unique(),
    content: text("content"),
    excerpt: text("excerpt"),
    status: postStatus("status").default("draft").notNull(),
    authorId: uuid("author_id")
      .notNull()
      .references(() => users.id, { onDelete: "cascade" }),
    publishedAt: timestamp("published_at"),
    createdAt: timestamp("created_at").defaultNow().notNull(),
    updatedAt: timestamp("updated_at").defaultNow().notNull(),
  },
  (table) => ({
    authorIdx: index("author_idx").on(table.authorId),
    statusIdx: index("status_idx").on(table.status),
    slugIdx: uniqueIndex("slug_idx").on(table.slug),
  }),
);

// Comments table
export const comments = pgTable(
  "comments",
  {
    id: uuid("id").primaryKey().defaultRandom(),
    content: text("content").notNull(),
    postId: uuid("post_id")
      .notNull()
      .references(() => posts.id, { onDelete: "cascade" }),
    authorId: uuid("author_id")
      .notNull()
      .references(() => users.id, { onDelete: "cascade" }),
    parentId: uuid("parent_id").references((): any => comments.id),
    createdAt: timestamp("created_at").defaultNow().notNull(),
  },
  (table) => ({
    postIdx: index("post_idx").on(table.postId),
  }),
);

// Relations
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
  comments: many(comments),
}));

export const postsRelations = relations(posts, ({ one, many }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
  comments: many(comments),
}));

export const commentsRelations = relations(comments, ({ one, many }) => ({
  post: one(posts, {
    fields: [comments.postId],
    references: [posts.id],
  }),
  author: one(users, {
    fields: [comments.authorId],
    references: [users.id],
  }),
  parent: one(comments, {
    fields: [comments.parentId],
    references: [comments.id],
  }),
  replies: many(comments),
}));

// Type exports
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
```

## Database Connection

```typescript
// src/db/index.ts
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

const connectionString = process.env.DATABASE_URL!;

// For queries
const queryClient = postgres(connectionString);
export const db = drizzle(queryClient, { schema });

// For migrations
const migrationClient = postgres(connectionString, { max: 1 });
export const migrationDb = drizzle(migrationClient);
```

## Zod Validation Schemas

```typescript
// src/validation/schemas.ts
import { z } from "zod";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { users, posts, comments } from "@/db/schema";

// Auto-generate from Drizzle schema
export const insertUserSchema = createInsertSchema(users, {
  email: z.string().email("Invalid email address"),
  name: z.string().min(2, "Name must be at least 2 characters"),
});

export const selectUserSchema = createSelectSchema(users);

// Custom schemas with refinements
export const createPostSchema = z.object({
  title: z.string().min(3, "Title must be at least 3 characters").max(255, "Title must be less than 255 characters"),
  content: z.string().min(10, "Content must be at least 10 characters"),
  excerpt: z.string().max(500).optional(),
  status: z.enum(["draft", "published"]).default("draft"),
});

export const updatePostSchema = createPostSchema.partial().extend({
  id: z.string().uuid(),
});

export const paginationSchema = z.object({
  page: z.number().int().positive().default(1),
  limit: z.number().int().min(1).max(100).default(20),
  orderBy: z.enum(["createdAt", "updatedAt", "title"]).default("createdAt"),
  order: z.enum(["asc", "desc"]).default("desc"),
});

export const createCommentSchema = z.object({
  postId: z.string().uuid(),
  content: z.string().min(1).max(2000),
  parentId: z.string().uuid().optional(),
});

// Type exports
export type CreatePost = z.infer<typeof createPostSchema>;
export type UpdatePost = z.infer<typeof updatePostSchema>;
export type Pagination = z.infer<typeof paginationSchema>;
```

## tRPC Server Setup

```typescript
// src/server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
import superjson from "superjson";
import { ZodError } from "zod";
import { db } from "@/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/auth";

// Context creation
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
  const session = await getServerSession(opts.req, opts.res, authOptions);

  return {
    db,
    session,
    req: opts.req,
    res: opts.res,
  };
};

// Initialize tRPC
const t = initTRPC.context<typeof createTRPCContext>().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

// Export reusable parts
export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;
export const createCallerFactory = t.createCallerFactory;

// Middleware for authenticated routes
const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session?.user) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  return next({
    ctx: {
      session: ctx.session,
      user: ctx.session.user,
    },
  });
});

export const protectedProcedure = t.procedure.use(isAuthed);

// Admin middleware
const isAdmin = t.middleware(({ ctx, next }) => {
  if (ctx.session?.user?.role !== "admin") {
    throw new TRPCError({ code: "FORBIDDEN" });
  }
  return next({ ctx });
});

export const adminProcedure = protectedProcedure.use(isAdmin);
```

## tRPC Routers

```typescript
// src/server/routers/posts.ts
import { z } from "zod";
import { eq, desc, asc, and, ilike, sql } from "drizzle-orm";
import { createTRPCRouter, publicProcedure, protectedProcedure } from "../trpc";
import { posts, users, comments } from "@/db/schema";
import { createPostSchema, updatePostSchema, paginationSchema } from "@/validation/schemas";
import { TRPCError } from "@trpc/server";

export const postsRouter = createTRPCRouter({
  // Get all posts with pagination
  list: publicProcedure
    .input(
      paginationSchema.extend({
        status: z.enum(["draft", "published", "archived"]).optional(),
        search: z.string().optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const { page, limit, orderBy, order, status, search } = input;
      const offset = (page - 1) * limit;

      const conditions = [];
      if (status) {
        conditions.push(eq(posts.status, status));
      }
      if (search) {
        conditions.push(ilike(posts.title, `%${search}%`));
      }

      const orderColumn = {
        createdAt: posts.createdAt,
        updatedAt: posts.updatedAt,
        title: posts.title,
      }[orderBy];

      const [items, countResult] = await Promise.all([
        ctx.db.query.posts.findMany({
          where: conditions.length > 0 ? and(...conditions) : undefined,
          with: {
            author: {
              columns: { id: true, name: true, avatarUrl: true },
            },
            comments: {
              columns: { id: true },
            },
          },
          orderBy: order === "asc" ? asc(orderColumn) : desc(orderColumn),
          limit,
          offset,
        }),
        ctx.db
          .select({ count: sql<number>`count(*)` })
          .from(posts)
          .where(conditions.length > 0 ? and(...conditions) : undefined),
      ]);

      return {
        items: items.map((post) => ({
          ...post,
          commentCount: post.comments.length,
        })),
        pagination: {
          page,
          limit,
          total: Number(countResult[0]?.count ?? 0),
          totalPages: Math.ceil(Number(countResult[0]?.count ?? 0) / limit),
        },
      };
    }),

  // Get single post by slug
  bySlug: publicProcedure.input(z.object({ slug: z.string() })).query(async ({ ctx, input }) => {
    const post = await ctx.db.query.posts.findFirst({
      where: eq(posts.slug, input.slug),
      with: {
        author: true,
        comments: {
          with: {
            author: {
              columns: { id: true, name: true, avatarUrl: true },
            },
          },
          orderBy: desc(comments.createdAt),
        },
      },
    });

    if (!post) {
      throw new TRPCError({
        code: "NOT_FOUND",
        message: "Post not found",
      });
    }

    return post;
  }),

  // Create post (authenticated)
  create: protectedProcedure.input(createPostSchema).mutation(async ({ ctx, input }) => {
    const slug = input.title
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/(^-|-$)/g, "");

    const [post] = await ctx.db
      .insert(posts)
      .values({
        ...input,
        slug: `${slug}-${Date.now()}`,
        authorId: ctx.user.id,
        publishedAt: input.status === "published" ? new Date() : null,
      })
      .returning();

    return post;
  }),

  // Update post (authenticated + ownership check)
  update: protectedProcedure.input(updatePostSchema).mutation(async ({ ctx, input }) => {
    const { id, ...data } = input;

    const existingPost = await ctx.db.query.posts.findFirst({
      where: eq(posts.id, id),
    });

    if (!existingPost) {
      throw new TRPCError({ code: "NOT_FOUND" });
    }

    if (existingPost.authorId !== ctx.user.id && ctx.user.role !== "admin") {
      throw new TRPCError({ code: "FORBIDDEN" });
    }

    const [updated] = await ctx.db
      .update(posts)
      .set({
        ...data,
        updatedAt: new Date(),
        publishedAt: data.status === "published" && !existingPost.publishedAt ? new Date() : existingPost.publishedAt,
      })
      .where(eq(posts.id, id))
      .returning();

    return updated;
  }),

  // Delete post
  delete: protectedProcedure.input(z.object({ id: z.string().uuid() })).mutation(async ({ ctx, input }) => {
    const post = await ctx.db.query.posts.findFirst({
      where: eq(posts.id, input.id),
    });

    if (!post) {
      throw new TRPCError({ code: "NOT_FOUND" });
    }

    if (post.authorId !== ctx.user.id && ctx.user.role !== "admin") {
      throw new TRPCError({ code: "FORBIDDEN" });
    }

    await ctx.db.delete(posts).where(eq(posts.id, input.id));

    return { success: true };
  }),
});
```

```typescript
// src/server/routers/comments.ts
import { z } from "zod";
import { eq, desc } from "drizzle-orm";
import { createTRPCRouter, publicProcedure, protectedProcedure } from "../trpc";
import { comments } from "@/db/schema";
import { createCommentSchema } from "@/validation/schemas";
import { TRPCError } from "@trpc/server";

export const commentsRouter = createTRPCRouter({
  byPost: publicProcedure.input(z.object({ postId: z.string().uuid() })).query(async ({ ctx, input }) => {
    return ctx.db.query.comments.findMany({
      where: eq(comments.postId, input.postId),
      with: {
        author: {
          columns: { id: true, name: true, avatarUrl: true },
        },
        replies: {
          with: {
            author: {
              columns: { id: true, name: true, avatarUrl: true },
            },
          },
        },
      },
      orderBy: desc(comments.createdAt),
    });
  }),

  create: protectedProcedure.input(createCommentSchema).mutation(async ({ ctx, input }) => {
    const [comment] = await ctx.db
      .insert(comments)
      .values({
        ...input,
        authorId: ctx.user.id,
      })
      .returning();

    return comment;
  }),

  delete: protectedProcedure.input(z.object({ id: z.string().uuid() })).mutation(async ({ ctx, input }) => {
    const comment = await ctx.db.query.comments.findFirst({
      where: eq(comments.id, input.id),
    });

    if (!comment) {
      throw new TRPCError({ code: "NOT_FOUND" });
    }

    if (comment.authorId !== ctx.user.id && ctx.user.role !== "admin") {
      throw new TRPCError({ code: "FORBIDDEN" });
    }

    await ctx.db.delete(comments).where(eq(comments.id, input.id));

    return { success: true };
  }),
});
```

```typescript
// src/server/routers/index.ts
import { createTRPCRouter } from "../trpc";
import { postsRouter } from "./posts";
import { commentsRouter } from "./comments";
import { usersRouter } from "./users";

export const appRouter = createTRPCRouter({
  posts: postsRouter,
  comments: commentsRouter,
  users: usersRouter,
});

export type AppRouter = typeof appRouter;
```

## Client Setup

```typescript
// src/utils/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@/server/routers";

export const trpc = createTRPCReact<AppRouter>();

// For server-side calls
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import superjson from "superjson";

export const serverTrpc = createTRPCProxyClient<AppRouter>({
  transformer: superjson,
  links: [
    httpBatchLink({
      url: `${process.env.NEXT_PUBLIC_APP_URL}/api/trpc`,
    }),
  ],
});
```

```typescript
// src/app/providers.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import superjson from 'superjson';
import { trpc } from '@/utils/trpc';

export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5 * 1000,
        refetchOnWindowFocus: false,
      },
    },
  }));

  const [trpcClient] = useState(() =>
    trpc.createClient({
      transformer: superjson,
      links: [
        httpBatchLink({
          url: '/api/trpc',
          headers() {
            return {
              // Add auth headers if needed
            };
          },
        }),
      ],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}
```

## React Components with tRPC

```typescript
// src/components/PostList.tsx
'use client';

import { useState } from 'react';
import { trpc } from '@/utils/trpc';

export function PostList() {
  const [page, setPage] = useState(1);
  const [search, setSearch] = useState('');

  const { data, isLoading, error } = trpc.posts.list.useQuery({
    page,
    limit: 10,
    search: search || undefined,
    status: 'published',
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <input
        type="search"
        placeholder="Search posts..."
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />

      <ul>
        {data?.items.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>By {post.author.name}</p>
            <p>{post.commentCount} comments</p>
          </li>
        ))}
      </ul>

      <div>
        <button
          onClick={() => setPage(p => Math.max(1, p - 1))}
          disabled={page === 1}
        >
          Previous
        </button>
        <span>Page {page} of {data?.pagination.totalPages}</span>
        <button
          onClick={() => setPage(p => p + 1)}
          disabled={page >= (data?.pagination.totalPages ?? 1)}
        >
          Next
        </button>
      </div>
    </div>
  );
}
```

```typescript
// src/components/CreatePostForm.tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { trpc } from '@/utils/trpc';
import { createPostSchema, type CreatePost } from '@/validation/schemas';

export function CreatePostForm() {
  const utils = trpc.useUtils();

  const { register, handleSubmit, formState: { errors }, reset } = useForm<CreatePost>({
    resolver: zodResolver(createPostSchema),
    defaultValues: {
      status: 'draft',
    },
  });

  const createPost = trpc.posts.create.useMutation({
    onSuccess: () => {
      // Invalidate and refetch posts list
      utils.posts.list.invalidate();
      reset();
    },
    onError: (error) => {
      // Handle Zod validation errors from server
      if (error.data?.zodError) {
        console.error('Validation errors:', error.data.zodError);
      }
    },
  });

  const onSubmit = (data: CreatePost) => {
    createPost.mutate(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="title">Title</label>
        <input id="title" {...register('title')} />
        {errors.title && <span>{errors.title.message}</span>}
      </div>

      <div>
        <label htmlFor="content">Content</label>
        <textarea id="content" {...register('content')} />
        {errors.content && <span>{errors.content.message}</span>}
      </div>

      <div>
        <label htmlFor="status">Status</label>
        <select id="status" {...register('status')}>
          <option value="draft">Draft</option>
          <option value="published">Published</option>
        </select>
      </div>

      <button type="submit" disabled={createPost.isLoading}>
        {createPost.isLoading ? 'Creating...' : 'Create Post'}
      </button>

      {createPost.error && (
        <div className="error">{createPost.error.message}</div>
      )}
    </form>
  );
}
```

## Optimistic Updates

```typescript
// src/components/LikeButton.tsx
'use client';

import { trpc } from '@/utils/trpc';

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const utils = trpc.useUtils();

  const likeMutation = trpc.posts.like.useMutation({
    // Optimistically update the UI
    onMutate: async ({ postId }) => {
      // Cancel outgoing refetches
      await utils.posts.byId.cancel({ id: postId });

      // Snapshot previous value
      const previousPost = utils.posts.byId.getData({ id: postId });

      // Optimistically update
      utils.posts.byId.setData({ id: postId }, (old) => {
        if (!old) return old;
        return {
          ...old,
          likes: old.likes + 1,
          likedByMe: true,
        };
      });

      return { previousPost };
    },
    // Rollback on error
    onError: (err, { postId }, context) => {
      if (context?.previousPost) {
        utils.posts.byId.setData({ id: postId }, context.previousPost);
      }
    },
    // Refetch after mutation
    onSettled: (_, __, { postId }) => {
      utils.posts.byId.invalidate({ id: postId });
    },
  });

  return (
    <button
      onClick={() => likeMutation.mutate({ postId })}
      disabled={likeMutation.isLoading}
    >
      ❤️ {initialLikes}
    </button>
  );
}
```

## Server-Side Rendering (Next.js App Router)

```typescript
// src/app/posts/page.tsx
import { createCaller } from '@/server/trpc';
import { createTRPCContext } from '@/server/trpc';
import { PostList } from '@/components/PostList';

export default async function PostsPage() {
  // Create server-side caller
  const ctx = await createTRPCContext({ req: {} as any, res: {} as any });
  const caller = createCaller(ctx);

  // Prefetch data
  const initialPosts = await caller.posts.list({
    page: 1,
    limit: 10,
    status: 'published',
  });

  return (
    <div>
      <h1>Posts</h1>
      {/* Pass initial data or use React Query hydration */}
      <PostList initialData={initialPosts} />
    </div>
  );
}
```

## Error Handling Best Practices

```typescript
// src/server/routers/posts.ts
import { TRPCError } from '@trpc/server';

// Custom error classes
class PostNotFoundError extends TRPCError {
  constructor(postId: string) {
    super({
      code: 'NOT_FOUND',
      message: `Post with ID ${postId} not found`,
      cause: { postId },
    });
  }
}

class UnauthorizedPostAccessError extends TRPCError {
  constructor() {
    super({
      code: 'FORBIDDEN',
      message: 'You do not have permission to modify this post',
    });
  }
}

// Usage in procedures
update: protectedProcedure
  .input(updatePostSchema)
  .mutation(async ({ ctx, input }) => {
    const post = await ctx.db.query.posts.findFirst({
      where: eq(posts.id, input.id),
    });

    if (!post) {
      throw new PostNotFoundError(input.id);
    }

    if (post.authorId !== ctx.user.id) {
      throw new UnauthorizedPostAccessError();
    }

    // ... update logic
  }),
```

## Testing

```typescript
// __tests__/posts.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { createCaller } from "@/server/trpc";
import { createTRPCContext } from "@/server/trpc";

describe("Posts Router", () => {
  let caller: ReturnType<typeof createCaller>;

  beforeEach(async () => {
    const ctx = await createTRPCContext({
      req: {} as any,
      res: {} as any,
    });
    caller = createCaller(ctx);
  });

  it("should list published posts", async () => {
    const result = await caller.posts.list({
      page: 1,
      limit: 10,
      status: "published",
    });

    expect(result.items).toBeDefined();
    expect(result.pagination.page).toBe(1);
  });

  it("should throw NOT_FOUND for non-existent post", async () => {
    await expect(caller.posts.bySlug({ slug: "non-existent-post" })).rejects.toThrow("Post not found");
  });
});
```

## Output Format

Provide:

- Type-safe API definitions with tRPC
- Drizzle schema with relations
- Zod validation schemas
- React Query integration patterns
- Error handling strategies

Sources:

- [tRPC Documentation](https://trpc.io/docs)
- [Drizzle ORM Documentation](https://orm.drizzle.team/docs/overview)
- [Zod Documentation](https://zod.dev/)
- [T3 Stack](https://create.t3.gg/)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…