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.

  • 9 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…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.