Skip to content
Back to skills

Tpl Fullstack T3 Stack

ASecurity

Template do pack (fullstack/01-t3-stack.md). Orienta o agente em stacks fullstack e arquitetura ponta a ponta alinhado a esse contexto.

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

Works with

  • cursor
  • cli
  • api

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add artubss/SKILLS-CLAUDE-CODE --skill tpl-fullstack-t3-stack --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tpl Fullstack T3 Stack?

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

Security grade badge for Tpl Fullstack T3 Stack
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/artubss-tpl-fullstack-t3-stack/badge)](https://www.skillsdirectory.com/skills/artubss-tpl-fullstack-t3-stack)

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: tpl-fullstack-t3-stack
description: Template do pack (fullstack/01-t3-stack.md). Orienta o agente em stacks fullstack e arquitetura ponta a ponta alinhado a esse contexto.
metadata:
  version: 1.0.0
  source_template: fullstack/01-t3-stack.md
  generated_by: install_pack_templates_as_claude_skills
---

# PROJECT: T3 Stack App (Next.js + tRPC + Prisma + NextAuth)

Skill gerado a partir do pack `templates-claude-code`. Arquivo de origem: `fullstack/01-t3-stack.md`. Use como baseline e adapte ao projeto antes de mudancas grandes.

## Conteudo do template

## STACK
- **Framework:** Next.js 15 (App Router)
- **API Layer:** tRPC v11
- **ORM:** Prisma 5 + PostgreSQL
- **Auth:** NextAuth.js v5 (Auth.js)
- **Styling:** Tailwind CSS v3 + shadcn/ui
- **Language:** TypeScript 5.x (strict)
- **Testing:** Vitest + @testing-library/react
- **Deployment:** Vercel + Neon (DB)

---

## PROJECT STRUCTURE
```
src/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   ├── api/
│   │   ├── auth/
│   │   │   └── [...nextauth]/
│   │   │       └── route.ts         # NextAuth handler
│   │   └── trpc/
│   │       └── [trpc]/
│   │           └── route.ts         # tRPC HTTP handler
│   └── dashboard/
│       └── page.tsx
├── server/
│   ├── auth.ts                      # NextAuth config
│   ├── db.ts                        # Prisma client singleton
│   └── api/
│       ├── root.ts                  # AppRouter assembly
│       ├── trpc.ts                  # Context + procedure builders
│       └── routers/
│           ├── user.ts
│           └── post.ts
├── trpc/
│   ├── server.ts                    # Server-side caller
│   └── client.tsx                   # TRPCReactProvider + hooks
├── components/
│   └── ui/                          # shadcn/ui components
└── lib/
    └── utils.ts
prisma/
└── schema.prisma
```

---

## ARCHITECTURE RULES
1. **Context carries auth session** — `createTRPCContext` reads NextAuth session; attach to ctx for all procedures.
2. **`protectedProcedure` enforces auth** — every mutation that touches user data uses `protectedProcedure`, never `publicProcedure`.
3. **End-to-end type safety** — never use `as` casts on tRPC responses; let inference flow from router → client.
4. **Server Components call tRPC directly** — use server-side caller (`server.ts`), not fetch; do not hit HTTP in RSC.
5. **Prisma client is a singleton** — use `globalThis.__prisma` pattern to avoid hot-reload connection exhaustion.
6. **Input validated by Zod** — every router procedure has `.input(zSchema)`.
7. **No business logic in route handlers** — logic lives in tRPC routers; `app/api/` contains only minimal glue.

---

## tRPC SETUP

```typescript
// src/server/api/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server'
import { ZodError } from 'zod'
import superjson from 'superjson'
import type { Session } from 'next-auth'
import { auth } from '../auth'
import { db }   from '../db'

interface CreateContextOptions { session: Session | null }

export const createTRPCContext = async (): Promise<CreateContextOptions & { db: typeof db }> => {
  const session = await auth()
  return { session, db }
}

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 const createCallerFactory = t.createCallerFactory
export const createTRPCRouter   = t.router
export const publicProcedure    = t.procedure

export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
  if (!ctx.session?.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' })
  }
  return next({ ctx: { ...ctx, session: ctx.session } })
})
```

---

## ROUTER DEFINITION

```typescript
// src/server/api/routers/post.ts
import { z } from 'zod'
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'

export const postRouter = createTRPCRouter({
  list: publicProcedure
    .input(z.object({ limit: z.number().min(1).max(100).default(20), cursor: z.string().optional() }))
    .query(async ({ ctx, input }) => {
      const posts = await ctx.db.post.findMany({
        take:    input.limit + 1,
        cursor:  input.cursor ? { id: input.cursor } : undefined,
        orderBy: { createdAt: 'desc' },
        include: { author: { select: { id: true, name: true, image: true } } },
      })
      const nextCursor = posts.length > input.limit ? posts.pop()!.id : undefined
      return { posts, nextCursor }
    }),

  create: protectedProcedure
    .input(z.object({ title: z.string().min(1).max(200), body: z.string().min(1) }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.post.create({
        data: { ...input, authorId: ctx.session.user.id },
      })
    }),

  delete: protectedProcedure
    .input(z.object({ id: z.string().cuid() }))
    .mutation(async ({ ctx, input }) => {
      const post = await ctx.db.post.findUnique({ where: { id: input.id } })
      if (!post) throw new TRPCError({ code: 'NOT_FOUND' })
      if (post.authorId !== ctx.session.user.id) throw new TRPCError({ code: 'FORBIDDEN' })
      return ctx.db.post.delete({ where: { id: input.id } })
    }),
})
```

---

## CLIENT USAGE (React Hooks)

```typescript
// src/trpc/client.tsx
'use client'
import { createTRPCReact } from '@trpc/react-query'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink } from '@trpc/client'
import superjson from 'superjson'
import type { AppRouter } from '../server/api/root'

export const trpc = createTRPCReact<AppRouter>()

// In a component:
function PostList() {
  const { data, isLoading } = trpc.post.list.useQuery({ limit: 10 })
  const createPost = trpc.post.create.useMutation({
    onSuccess: () => utils.post.list.invalidate(),
  })
  const utils = trpc.useUtils()

  if (isLoading) return <p>Loading...</p>
  return (
    <ul>
      {data?.posts.map(p => <li key={p.id}>{p.title}</li>)}
    </ul>
  )
}

// Server Component (no HTTP — direct DB call)
// src/app/dashboard/page.tsx
import { createCaller } from '~/server/api/root'
import { createTRPCContext } from '~/server/api/trpc'

export default async function DashboardPage() {
  const ctx    = await createTRPCContext()
  const caller = createCaller(ctx)
  const { posts } = await caller.post.list({ limit: 5 })
  // posts is fully typed PostWithAuthor[]
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
```

---

## ROUTING TABLE

| Procedure | Type | Auth | Action |
|-----------|------|------|--------|
| `post.list` | query | No | Paginated post list |
| `post.byId` | query | No | Single post |
| `post.create` | mutation | Yes | Create post |
| `post.update` | mutation | Yes (owner) | Update post |
| `post.delete` | mutation | Yes (owner) | Delete post |
| `user.me` | query | Yes | Current user profile |
| `user.update` | mutation | Yes | Update profile |
| `user.listByAdmin` | query | Yes (admin) | Admin user list |
| `auth/signin` | HTTP GET | No | NextAuth sign-in page |
| `auth/callback` | HTTP GET | No | NextAuth OAuth callback |

---

## QUALITY GATES
- [ ] `tsc --noEmit` — zero errors
- [ ] `trpc.post.create.useMutation` result type matches Prisma return type (no `as`)
- [ ] All mutations use `protectedProcedure`
- [ ] Prisma `schema.prisma` validated with `prisma validate`
- [ ] `pytest run` / `vitest run` — all tests pass
- [ ] No `fetch('/api/trpc/...')` calls — use tRPC hooks only
- [ ] Server Components use direct caller, not HTTP
- [ ] `.env` file has all vars declared in `env.js` (T3 env validation)
- [ ] `prisma migrate deploy` tested against staging DB

---

## FORBIDDEN
- ❌ Raw `fetch` to tRPC endpoints from client components — use `trpc.*` hooks
- ❌ Direct `db.*` calls in Server/Client components — go through tRPC
- ❌ Mutations without Zod `.input()` validation
- ❌ `protectedProcedure` in public API paths (naming confusion — use separate routers)
- ❌ `any` casts on tRPC return types
- ❌ Creating multiple Prisma clients — one singleton only
- ❌ Storing session data in localStorage — use NextAuth session cookies
- ❌ `"use client"` on pages that can be Server Components

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…