@dennisrongo/skills 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -13
- package/package.json +1 -1
- package/skills/nextjs-app-router/SKILL.md +228 -175
- package/skills/nextjs-app-router/references/anti-patterns.md +163 -99
- package/skills/nextjs-app-router/references/folder-layout.md +79 -46
- package/skills/nextjs-app-router/references/good-patterns.md +233 -99
- package/skills/nextjs-app-router/references/templates/api-base.md +24 -21
- package/skills/nextjs-app-router/references/templates/auth-slice.md +82 -78
- package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +58 -6
- package/skills/nextjs-app-router/references/templates/db-client.md +48 -0
- package/skills/nextjs-app-router/references/templates/env-and-utils.md +90 -23
- package/skills/nextjs-app-router/references/templates/feature-slice.md +110 -47
- package/skills/nextjs-app-router/references/templates/form-with-zod.md +23 -15
- package/skills/nextjs-app-router/references/templates/middleware.md +69 -59
- package/skills/nextjs-app-router/references/templates/next-config.md +4 -14
- package/skills/nextjs-app-router/references/templates/nextauth-config.md +178 -0
- package/skills/nextjs-app-router/references/templates/package.md +35 -5
- package/skills/nextjs-app-router/references/templates/prisma-schema.md +162 -0
- package/skills/nextjs-app-router/references/templates/providers-and-store.md +35 -21
- package/skills/nextjs-app-router/references/templates/root-layout.md +99 -20
- package/skills/nextjs-app-router/references/templates/route-group-layouts.md +46 -6
- package/skills/nextjs-app-router/references/templates/route-handler.md +168 -0
- package/skills/nextjs-app-router/references/templates/testing.md +105 -31
|
@@ -2,42 +2,79 @@
|
|
|
2
2
|
|
|
3
3
|
Each entry below explains **what** the pattern looks like in the wild, **why** it's bad, and **what to do instead**. The skill must not emit any of these. If a user explicitly requests one, push back with the rationale before complying.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **About the SPA-style choice.** This project is deliberately API-driven, not SSR. Pages are `'use client'`. Data goes through RTK Query → Route Handlers. That choice eliminates a whole class of "is this server or client?" bugs, makes mocking trivial in tests, and matches how the team thinks about the app (frontend + backend with a clean HTTP seam). Several anti-patterns below exist *because* of that choice — they're not absolute rules in every Next.js project, but they are absolute rules in this one.
|
|
6
|
+
|
|
7
|
+
## 1. `fetch()` or `await db.*` inside a server component
|
|
6
8
|
|
|
7
9
|
```tsx
|
|
8
|
-
// app/page.tsx — BAD
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export default function Home() {
|
|
13
|
-
const router = useRouter();
|
|
14
|
-
useEffect(() => { router.push('/dashboard'); });
|
|
10
|
+
// app/(app)/dashboard/page.tsx — BAD
|
|
11
|
+
export default async function DashboardPage() {
|
|
12
|
+
const stats = await db.stats.findMany();
|
|
13
|
+
return <Dashboard stats={stats} />;
|
|
15
14
|
}
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
**Why bad:**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
**Why bad:** the app is API-driven by deliberate choice. SSR data fetching breaks the one-way data flow (UI → RTK Query → /api → DB), produces a different code path in production than in dev (server fetch vs. browser fetch), defeats RTK Query's caching/devtools/invalidation, and makes integration tests harder because you now have two ways to load data.
|
|
18
|
+
|
|
19
|
+
**Do instead:** make the page `'use client'` and call `useGetStatsQuery()` from `src/redux/api/statsApi.ts`. The matching `src/app/api/stats/route.ts` does the `db.*` call.
|
|
20
|
+
|
|
21
|
+
## 2. `'use server'` directive (Server Actions)
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// BAD
|
|
25
|
+
'use server';
|
|
26
|
+
export async function createCustomer(formData: FormData) { ... }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Why bad:** Server Actions are a second mutation path competing with RTK Query mutations. The project picks one. Mixed projects develop "do we do this with an Action or a mutation?" cargo-culting, two error-handling stories, two loading-state stories, and two places to forget the auth check.
|
|
23
30
|
|
|
24
|
-
**Do instead:**
|
|
31
|
+
**Do instead:** mutations are RTK Query mutations calling a Route Handler. The handler does `await auth()` + Zod validation + Prisma write.
|
|
32
|
+
|
|
33
|
+
## 3. `async function Page` in any `page.tsx`
|
|
34
|
+
|
|
35
|
+
Pages are `'use client'` and synchronous. The body uses RTK Query hooks. If you see `export default async function ...Page(...)` you've reintroduced SSR data fetching by accident.
|
|
36
|
+
|
|
37
|
+
## 4. `redirect()` from a server `page.tsx` as the auth gate
|
|
25
38
|
|
|
26
39
|
```tsx
|
|
27
|
-
//
|
|
40
|
+
// BAD
|
|
41
|
+
import { auth } from '@/auth';
|
|
28
42
|
import { redirect } from 'next/navigation';
|
|
29
|
-
export default function
|
|
30
|
-
|
|
43
|
+
export default async function AppLayout({ children }) {
|
|
44
|
+
const session = await auth();
|
|
45
|
+
if (!session) redirect('/auth/login');
|
|
46
|
+
return <>{children}</>;
|
|
31
47
|
}
|
|
32
48
|
```
|
|
33
49
|
|
|
34
|
-
|
|
50
|
+
**Why bad:** belt-and-suspenders auth fragmented across layout files. The gate is `middleware.ts`. A layout-level `redirect` runs *after* the page is requested (more work for nothing) and gives no consistent behavior across routes.
|
|
51
|
+
|
|
52
|
+
**Do instead:** `middleware.ts` re-exports (or wraps) `auth` from `src/auth.config.ts`. Unauthenticated requests to protected paths are 302'd before any layout renders. Layouts assume they're authenticated when they render.
|
|
35
53
|
|
|
36
|
-
##
|
|
54
|
+
## 5. Custom JWT-in-`httpOnly`-cookie schemes alongside NextAuth
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// BAD — competing with NextAuth
|
|
58
|
+
cookies().set('session', signToken(user), { httpOnly: true, ... });
|
|
59
|
+
```
|
|
37
60
|
|
|
38
|
-
|
|
61
|
+
**Why bad:** two auth systems in one app. NextAuth already manages the session cookie; rolling your own next to it gives you race conditions on logout (which cookie wins?), two refresh stories, two CSRF stories, and a guaranteed bug when the user signs out from one but not the other.
|
|
39
62
|
|
|
40
|
-
|
|
63
|
+
**Do instead:** NextAuth is the only auth system. If you need extra state on the session (role, org), add a `jwt` callback in `src/auth.ts` that augments the token.
|
|
64
|
+
|
|
65
|
+
## 6. Decoding the NextAuth JWT manually on the client
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
// BAD
|
|
69
|
+
const token = document.cookie.match(/next-auth.session-token=([^;]+)/)?.[1];
|
|
70
|
+
const claims = jwtDecode(token!);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Why bad:** the cookie is `httpOnly` by default (it shouldn't even be readable from JS). Even if it weren't, decoding ≠ verifying. Use the official hook — it handles refresh, expiry, and SSR-safe rendering.
|
|
74
|
+
|
|
75
|
+
**Do instead:** `const { data: session } = useSession()` from `next-auth/react`. Server-side (Route Handlers): `const session = await auth()`.
|
|
76
|
+
|
|
77
|
+
## 7. `serializableCheck: false` (or `immutableCheck: false`)
|
|
41
78
|
|
|
42
79
|
```ts
|
|
43
80
|
// store.ts — BAD
|
|
@@ -51,74 +88,60 @@ middleware: (getDefaultMiddleware) =>
|
|
|
51
88
|
|
|
52
89
|
```ts
|
|
53
90
|
serializableCheck: {
|
|
54
|
-
ignoredPaths: ['
|
|
91
|
+
ignoredPaths: ['someFeature.someDate'],
|
|
55
92
|
ignoredActions: ['someApi/executeMutation/fulfilled'],
|
|
56
93
|
},
|
|
57
94
|
```
|
|
58
95
|
|
|
59
96
|
…and leave a comment naming what's stored there and why it can't be a plain value.
|
|
60
97
|
|
|
61
|
-
##
|
|
98
|
+
## 8. `@ts-ignore` / `as any` in the store, providers, API layer, or Route Handlers
|
|
62
99
|
|
|
63
100
|
These are the load-bearing files of the entire app. If TypeScript is unhappy with them, the types are wrong — fix them. Use `satisfies`, narrow the inferred type, or extract a typed helper. `@ts-ignore` here hides real type breakage from refactors.
|
|
64
101
|
|
|
65
|
-
##
|
|
102
|
+
## 9. Commented-out reducers, endpoints, imports
|
|
66
103
|
|
|
67
|
-
`store.ts` that imports `authApi` and `dashboardApi` but commented out the lines that register their reducers
|
|
104
|
+
`store.ts` that imports `authApi` and `dashboardApi` but commented out the lines that register their reducers is a half-finished refactor pretending to be code. Either wire it up or delete it. Git history is the right place for "we used to do this."
|
|
68
105
|
|
|
69
|
-
##
|
|
106
|
+
## 10. `moment` alongside `date-fns`
|
|
70
107
|
|
|
71
|
-
`moment` is in maintenance mode, mutable, and ships ~300 KB minified. `date-fns` is tree-shakeable, immutable, and ESM.
|
|
108
|
+
`moment` is in maintenance mode, mutable, and ships ~300 KB minified. `date-fns` is tree-shakeable, immutable, and ESM. Pick `date-fns`.
|
|
72
109
|
|
|
73
|
-
##
|
|
110
|
+
## 11. `styled-components` (or Emotion) in a Tailwind project
|
|
74
111
|
|
|
75
|
-
The stack is Tailwind utilities + Radix primitives + `class-variance-authority` for variants + `cn()` for merging. Adding a CSS-in-JS runtime gives you two styling systems, two specificity stories, and a hydration cost.
|
|
112
|
+
The stack is Tailwind utilities + Radix primitives + `class-variance-authority` for variants + `cn()` for merging. Adding a CSS-in-JS runtime gives you two styling systems, two specificity stories, and a hydration cost.
|
|
76
113
|
|
|
77
|
-
##
|
|
114
|
+
## 12. Duplicate-by-case folders or files
|
|
78
115
|
|
|
79
|
-
`src/models/
|
|
116
|
+
`src/models/StateReports/` and `src/models/statereports/` work on Windows (case-insensitive) and break on Linux (case-sensitive). Enforce one canonical casing.
|
|
80
117
|
|
|
81
|
-
##
|
|
118
|
+
## 13. `dangerouslySetInnerHTML` without server-side sanitization
|
|
82
119
|
|
|
83
|
-
|
|
84
|
-
<div dangerouslySetInnerHTML={{ __html: apiResponse.body }} />
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
If `apiResponse.body` is anything other than provably safe (e.g., a static template you control), this is an XSS vector. The fix is layered:
|
|
120
|
+
If `apiResponse.body` is anything other than provably safe, this is an XSS vector. Fix layered:
|
|
88
121
|
|
|
89
122
|
1. Prefer rendering as JSX / Markdown via a strict renderer (no raw HTML).
|
|
90
|
-
2. If raw HTML is genuinely required, sanitize at the source — server-side allowlist, or DOMPurify on the client with a configured profile.
|
|
91
|
-
3.
|
|
123
|
+
2. If raw HTML is genuinely required, sanitize at the source — server-side allowlist in the Route Handler, or DOMPurify on the client with a configured profile.
|
|
124
|
+
3. Inline comment naming the sanitization point.
|
|
92
125
|
|
|
93
|
-
A scaffolded project should not contain any `dangerouslySetInnerHTML` calls
|
|
126
|
+
A scaffolded project should not contain any `dangerouslySetInnerHTML` calls.
|
|
94
127
|
|
|
95
|
-
##
|
|
128
|
+
## 14. `sessionStorage` / `localStorage` for non-transient state
|
|
96
129
|
|
|
97
|
-
`sessionStorage.setItem('wizardStep', JSON.stringify(state))` is a shortcut that almost always grows into a bug:
|
|
98
130
|
- Not SSR-safe (`ReferenceError: sessionStorage is not defined`).
|
|
99
|
-
- Survives tab refreshes but not new tabs
|
|
100
|
-
- Easy to forget to clear
|
|
131
|
+
- Survives tab refreshes but not new tabs.
|
|
132
|
+
- Easy to forget to clear.
|
|
101
133
|
|
|
102
|
-
**Do instead:**
|
|
103
|
-
- Multi-step wizard state → URL search params (`useSearchParams`) so it's shareable and back-button-safe, or Redux if it must be shared across routes.
|
|
104
|
-
- Token persistence → `httpOnly` cookies (server-set), never `localStorage`.
|
|
105
|
-
- Truly transient UI state → component state.
|
|
134
|
+
**Do instead:** URL search params for shareable state, Redux for cross-route state, component state for transient UI. Tokens never go in `localStorage` — NextAuth's cookie is `httpOnly`.
|
|
106
135
|
|
|
107
|
-
##
|
|
136
|
+
## 15. JWT in a JS-accessible cookie or `localStorage`
|
|
108
137
|
|
|
109
|
-
A token in `
|
|
138
|
+
A token in `localStorage` is reachable from any XSS payload. NextAuth's session cookie is `httpOnly` by default — leave it that way. Do not add `js-cookie` and write parallel auth state to a readable cookie.
|
|
110
139
|
|
|
111
|
-
|
|
112
|
-
- `middleware.ts` reads the cookie on the server to gate routes.
|
|
113
|
-
- RTK Query sends the cookie automatically via `credentials: 'include'` on `fetch`.
|
|
140
|
+
## 16. A `src/proxy.ts` (or equivalent) standing in for `middleware.ts`
|
|
114
141
|
|
|
115
|
-
|
|
142
|
+
Next.js has exactly one place that intercepts requests: `middleware.ts` at the project root. A `src/proxy.ts` that imports `next/server` and exports a function but isn't wired into the framework is dead code that *looks* like a guard. Either move its logic into `middleware.ts` or delete it.
|
|
116
143
|
|
|
117
|
-
##
|
|
118
|
-
|
|
119
|
-
Next.js has exactly one place that intercepts requests at the network boundary: `middleware.ts` at the project root. A file named `src/proxy.ts` that imports `next/server` and exports a function but isn't wired into the framework is dead code that *looks* like a guard. Either move its logic into `middleware.ts` or delete it.
|
|
120
|
-
|
|
121
|
-
## 13. Inline `fetch` in components
|
|
144
|
+
## 17. Inline `fetch` in components
|
|
122
145
|
|
|
123
146
|
```tsx
|
|
124
147
|
useEffect(() => {
|
|
@@ -126,78 +149,119 @@ useEffect(() => {
|
|
|
126
149
|
}, []);
|
|
127
150
|
```
|
|
128
151
|
|
|
129
|
-
This bypasses the cache, retries, error handling, devtools
|
|
152
|
+
This bypasses the cache, retries, error handling, devtools, and tag-based invalidation that RTK Query gives you for free. **All** backend calls go through RTK Query.
|
|
130
153
|
|
|
131
|
-
##
|
|
154
|
+
## 18. Redux for local / URL / form state
|
|
132
155
|
|
|
133
156
|
If a piece of state is:
|
|
134
157
|
- Only read inside one component → `useState` / `useReducer`.
|
|
135
158
|
- Shareable via URL (filters, page numbers, selected tab) → `useSearchParams`.
|
|
136
159
|
- Form values → React Hook Form's internal state.
|
|
160
|
+
- Session/user info → `useSession()` from `next-auth/react`.
|
|
137
161
|
|
|
138
|
-
Reach for Redux only when state is genuinely shared across routes
|
|
162
|
+
Reach for Redux only when state is genuinely shared across routes and isn't already covered by an RTK Query cache or NextAuth.
|
|
139
163
|
|
|
140
|
-
##
|
|
164
|
+
## 19. Many independent `createApi(...)` instances
|
|
141
165
|
|
|
142
166
|
```ts
|
|
143
167
|
export const authApi = createApi({ reducerPath: 'auth', ... });
|
|
144
168
|
export const customersApi = createApi({ reducerPath: 'customers', ... });
|
|
145
|
-
export const invoicesApi = createApi({ reducerPath: 'invoices', ... });
|
|
146
169
|
```
|
|
147
170
|
|
|
148
|
-
Each instance has its own
|
|
171
|
+
Each instance has its own cache and tag-type set; they can't invalidate each other. One base `api` with `injectEndpoints` per domain.
|
|
172
|
+
|
|
173
|
+
## 20. `window.location.href = '/auth/login'` from inside `baseQuery` for 401
|
|
174
|
+
|
|
175
|
+
Loses unsaved form state, full page reload, race conditions when multiple in-flight requests 401 at once.
|
|
176
|
+
|
|
177
|
+
**Do instead:** call `signOut({ callbackUrl: '/auth/login', redirect: true })` from `next-auth/react` inside the base query's 401 branch. NextAuth clears the cookie and routes cleanly.
|
|
178
|
+
|
|
179
|
+
## 21. Multiple `PrismaClient` instances
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
// some-route.ts — BAD
|
|
183
|
+
const prisma = new PrismaClient();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Every dev hot-reload spawns a new client and burns through Postgres connections. Use the singleton:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// src/lib/db.ts
|
|
190
|
+
import { PrismaClient } from '@prisma/client';
|
|
191
|
+
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
|
|
192
|
+
export const db = globalForPrisma.prisma ?? new PrismaClient();
|
|
193
|
+
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
…and every handler imports `db` from there.
|
|
197
|
+
|
|
198
|
+
## 22. Route Handlers without `await auth()`
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
// app/api/customers/route.ts — BAD
|
|
202
|
+
export async function GET() {
|
|
203
|
+
const customers = await db.customer.findMany();
|
|
204
|
+
return Response.json(customers);
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Anyone with the URL can read every customer. **Every** handler (except `/api/auth/[...nextauth]` and explicit public endpoints like `/api/health`) calls `requireSession()` (or `await auth()`) first and 401s on absence. Use the helper in `src/lib/api-auth.ts`.
|
|
209
|
+
|
|
210
|
+
## 23. Route Handlers that trust client-sent user IDs for ownership
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// BAD
|
|
214
|
+
const { userId, ...data } = await req.json();
|
|
215
|
+
await db.customer.create({ data: { ...data, userId } });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
A logged-in user can write under any other user's ID by lying in the request body. Read the user ID from the session:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const session = await requireSession();
|
|
222
|
+
await db.customer.create({ data: { ...validated, userId: session.user.id } });
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Ownership reads use `where: { id, userId: session.user.id }` so a stranger's record returns 404 / null, not someone else's data.
|
|
149
226
|
|
|
150
|
-
##
|
|
227
|
+
## 24. `prisma db push` in CI or against shared databases
|
|
151
228
|
|
|
152
|
-
|
|
153
|
-
- Loses unsaved form state.
|
|
154
|
-
- Forces a full page reload (re-downloads JS, re-hydrates Redux).
|
|
155
|
-
- Race conditions when multiple in-flight requests all hit 401 at once.
|
|
229
|
+
`db push` skips the migration history. Two engineers running `db push` against different branches produce divergent schemas that aren't represented in `prisma/migrations/`. Use `prisma migrate dev` in development and `prisma migrate deploy` in CI. `db push` is only for local prototyping you're about to throw away.
|
|
156
230
|
|
|
157
|
-
|
|
231
|
+
## 25. `prisma migrate reset` without explicit user confirmation
|
|
158
232
|
|
|
159
|
-
|
|
233
|
+
Drops the database. The skill never runs this unprompted. If the user asks for it, confirm and pause for a verbal "yes."
|
|
160
234
|
|
|
161
|
-
|
|
162
|
-
- `loading.tsx` — shown by Suspense while server components fetch.
|
|
163
|
-
- `error.tsx` — caught for runtime errors and RTK Query failures bubbling through `unwrap()`.
|
|
164
|
-
- `not-found.tsx` — for 404s from `notFound()` calls and unmatched routes.
|
|
235
|
+
## 26. Missing `error.tsx` / `loading.tsx` on the `(app)` group
|
|
165
236
|
|
|
166
|
-
Without these
|
|
237
|
+
Without these the user sees a blank screen during RTK Query loads and the global error boundary on any unwrap failure.
|
|
167
238
|
|
|
168
|
-
##
|
|
239
|
+
## 27. No tests at all
|
|
169
240
|
|
|
170
|
-
A `package.json` with no `test` script
|
|
241
|
+
A `package.json` with no `test` script means nothing gets enforced before merge. The bar is "smoke tests exist": one schema test, one component test, one E2E auth flow.
|
|
171
242
|
|
|
172
|
-
##
|
|
243
|
+
## 28. ESLint extending only `next/core-web-vitals`
|
|
173
244
|
|
|
174
|
-
|
|
175
|
-
- `@typescript-eslint/recommended` (or `recommended-type-checked` if your CI has the budget).
|
|
176
|
-
- `plugin:react-hooks/recommended` — catches the missing-deps and Rules-of-Hooks bugs that `next/core-web-vitals` does not.
|
|
177
|
-
- `plugin:jsx-a11y/recommended` — accessibility lint.
|
|
178
|
-
- Project rules: `no-console: ['warn', { allow: ['warn', 'error'] }]`, `@typescript-eslint/no-explicit-any: 'error'`, `react-hooks/exhaustive-deps: 'error'`, `prefer-const: 'error'`.
|
|
245
|
+
Add `@typescript-eslint/recommended`, `react-hooks/recommended`, `jsx-a11y/recommended`, plus project rules (`no-console: warn`, `no-explicit-any: error`, `exhaustive-deps: error`, `prefer-const: error`).
|
|
179
246
|
|
|
180
|
-
##
|
|
247
|
+
## 29. Over-extended Tailwind config
|
|
181
248
|
|
|
182
|
-
|
|
183
|
-
- Brand colors (named with a project prefix).
|
|
184
|
-
- Custom fonts.
|
|
185
|
-
- Genuine design tokens you reuse.
|
|
249
|
+
50+ custom spacing tokens, three custom animation timings — design-system debt. Extend Tailwind only for brand colors, custom fonts, and genuine design tokens.
|
|
186
250
|
|
|
187
|
-
|
|
251
|
+
## 30. Per-feature client guards as the only auth check
|
|
188
252
|
|
|
189
|
-
|
|
253
|
+
A `<AuthExpirationHandler>` running on an interval is **not** the primary guard. The primary guard is `middleware.ts`. Client guards exist to handle session expiry mid-app — belt-and-suspenders, not the gate.
|
|
190
254
|
|
|
191
|
-
|
|
255
|
+
## 31. PM2 / `ecosystem.config.js` without considering simpler hosting
|
|
192
256
|
|
|
193
|
-
|
|
257
|
+
If the target is Vercel / Cloudflare / a container orchestrator, PM2 is unused weight. Add only when the user explicitly asks for VM-style hosting.
|
|
194
258
|
|
|
195
|
-
|
|
259
|
+
## 32. Importing the Prisma adapter (or `@/lib/db`) inside `src/auth.config.ts`
|
|
196
260
|
|
|
197
|
-
|
|
261
|
+
`auth.config.ts` is consumed by `middleware.ts`, which runs on the Edge runtime. Edge cannot import the Prisma client (Node-only). If you accidentally import `@/lib/db` into `auth.config.ts`, the production build fails with a confusing "module not found in edge runtime" error.
|
|
198
262
|
|
|
199
|
-
|
|
263
|
+
**Rule:** `auth.config.ts` has providers + callbacks only. `auth.ts` imports `auth.config.ts`, adds the adapter, and exports the runtime objects. Middleware imports from `auth.config.ts` (or from a thin Edge-safe shim).
|
|
200
264
|
|
|
201
|
-
##
|
|
265
|
+
## 33. `'use server'` anywhere
|
|
202
266
|
|
|
203
|
-
|
|
267
|
+
Listed again because it matters. Grep `src/` for `'use server'` before reporting "done" — any hit is a bug.
|
|
@@ -4,7 +4,7 @@ This is the source-of-truth tree for a new project. Every file/folder listed her
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
{{project-name}}/
|
|
7
|
-
├── .env.example # committed; documents required env vars
|
|
7
|
+
├── .env.example # committed; documents required env vars (no secrets)
|
|
8
8
|
├── .eslintrc.json # or eslint.config.mjs — strict TS + a11y + hooks
|
|
9
9
|
├── .gitignore
|
|
10
10
|
├── .husky/
|
|
@@ -12,10 +12,11 @@ This is the source-of-truth tree for a new project. Every file/folder listed her
|
|
|
12
12
|
├── .prettierrc
|
|
13
13
|
├── README.md
|
|
14
14
|
├── components.json # shadcn config; rsc: true, tsx: true
|
|
15
|
-
├──
|
|
15
|
+
├── docker-compose.yml # OPTIONAL — only when user picked "local Docker" DB host
|
|
16
|
+
├── middleware.ts # NextAuth-driven route gate. Project-root, framework convention.
|
|
16
17
|
├── next.config.ts
|
|
17
18
|
├── next-env.d.ts # generated; do not edit
|
|
18
|
-
├── package.json # scripts: dev, build, start, lint, typecheck, test, test:e2e
|
|
19
|
+
├── package.json # scripts: dev, build, start, lint, typecheck, test, test:e2e, db:*
|
|
19
20
|
├── playwright.config.ts
|
|
20
21
|
├── postcss.config.js
|
|
21
22
|
├── tailwind.config.ts
|
|
@@ -24,50 +25,74 @@ This is the source-of-truth tree for a new project. Every file/folder listed her
|
|
|
24
25
|
│
|
|
25
26
|
├── .github/
|
|
26
27
|
│ └── workflows/
|
|
27
|
-
│ └── ci.yml # install → lint → typecheck → test → build
|
|
28
|
+
│ └── ci.yml # install → lint → typecheck → test → build (with Postgres service)
|
|
29
|
+
│
|
|
30
|
+
├── prisma/
|
|
31
|
+
│ ├── schema.prisma # NextAuth tables (User/Account/Session/VerificationToken) + domain models
|
|
32
|
+
│ ├── migrations/ # generated; CHECKED IN — these are the source of truth
|
|
33
|
+
│ │ └── <timestamp>_init/
|
|
34
|
+
│ │ └── migration.sql
|
|
35
|
+
│ └── seed.ts # OPTIONAL — seeds test user(s) for Credentials provider
|
|
28
36
|
│
|
|
29
37
|
├── public/ # static assets only (favicons, robots.txt, og images)
|
|
30
38
|
│ └── favicon.ico
|
|
31
39
|
│
|
|
32
40
|
├── src/
|
|
41
|
+
│ ├── auth.ts # NextAuth (Auth.js v5) full config — adapter + providers + callbacks
|
|
42
|
+
│ ├── auth.config.ts # Edge-safe config (no DB / no adapter imports) — for middleware
|
|
43
|
+
│ │
|
|
33
44
|
│ ├── app/
|
|
34
|
-
│ │ ├── layout.tsx # SERVER. <html><body><Providers>
|
|
35
|
-
│ │ ├──
|
|
36
|
-
│ │ ├── error.tsx # global error boundary
|
|
45
|
+
│ │ ├── layout.tsx # SERVER — the ONE server component in the app. <html><body><Providers>
|
|
46
|
+
│ │ ├── error.tsx # global error boundary ('use client')
|
|
37
47
|
│ │ ├── not-found.tsx
|
|
38
48
|
│ │ ├── globals.css
|
|
39
49
|
│ │ ├── favicon.ico # (optional duplicate of public/)
|
|
40
50
|
│ │ │
|
|
51
|
+
│ │ ├── api/
|
|
52
|
+
│ │ │ ├── auth/
|
|
53
|
+
│ │ │ │ └── [...nextauth]/
|
|
54
|
+
│ │ │ │ └── route.ts # export { GET, POST } from NextAuth handlers
|
|
55
|
+
│ │ │ ├── <domain>/
|
|
56
|
+
│ │ │ │ ├── route.ts # GET (list) + POST (create) — both call requireSession()
|
|
57
|
+
│ │ │ │ └── [id]/
|
|
58
|
+
│ │ │ │ └── route.ts # GET + PATCH + DELETE
|
|
59
|
+
│ │ │ └── health/
|
|
60
|
+
│ │ │ └── route.ts # OPTIONAL — unauthenticated readiness probe
|
|
61
|
+
│ │ │
|
|
41
62
|
│ │ ├── (public)/ # unauthenticated routes
|
|
42
63
|
│ │ │ ├── layout.tsx # SERVER. minimal chrome (centered card).
|
|
43
64
|
│ │ │ └── auth/
|
|
44
65
|
│ │ │ ├── login/
|
|
45
|
-
│ │ │ │ ├── page.tsx
|
|
46
|
-
│ │ │ │ └── _components/LoginForm.tsx
|
|
47
|
-
│ │ │ ├── signup/
|
|
48
|
-
│ │ │
|
|
66
|
+
│ │ │ │ ├── page.tsx # 'use client'
|
|
67
|
+
│ │ │ │ └── _components/LoginForm.tsx # 'use client'; calls signIn('credentials', {...})
|
|
68
|
+
│ │ │ ├── signup/ # OPTIONAL — only when Credentials + signup enabled
|
|
69
|
+
│ │ │ │ ├── page.tsx # 'use client'
|
|
70
|
+
│ │ │ │ └── _components/SignupForm.tsx
|
|
71
|
+
│ │ │ └── error/
|
|
72
|
+
│ │ │ └── page.tsx # NextAuth /auth/error landing
|
|
49
73
|
│ │ │
|
|
50
74
|
│ │ ├── (app)/ # authenticated routes
|
|
51
|
-
│ │ │ ├── layout.tsx # SERVER.
|
|
75
|
+
│ │ │ ├── layout.tsx # SERVER. renders <AppShell> client child. No DB/fetch here.
|
|
52
76
|
│ │ │ ├── loading.tsx
|
|
53
77
|
│ │ │ ├── error.tsx
|
|
54
78
|
│ │ │ ├── _components/
|
|
55
79
|
│ │ │ │ ├── AppShell.tsx # 'use client'; Sidebar + Header
|
|
56
|
-
│ │ │ │
|
|
57
|
-
│ │ │
|
|
58
|
-
│ │ │ │ └── useSessionExpiryWarning.ts
|
|
80
|
+
│ │ │ │ ├── Sidebar.tsx # 'use client'
|
|
81
|
+
│ │ │ │ └── UserMenu.tsx # 'use client'; uses useSession() + signOut()
|
|
59
82
|
│ │ │ ├── dashboard/
|
|
60
|
-
│ │ │ │ ├── page.tsx
|
|
83
|
+
│ │ │ │ ├── page.tsx # 'use client'; uses RTK Query hooks
|
|
61
84
|
│ │ │ │ ├── loading.tsx
|
|
62
85
|
│ │ │ │ └── _components/
|
|
63
86
|
│ │ │ └── <feature>/
|
|
64
|
-
│ │ │ ├── page.tsx
|
|
87
|
+
│ │ │ ├── page.tsx # 'use client'
|
|
88
|
+
│ │ │ ├── new/page.tsx # 'use client'; renders <FeatureForm mode="create" />
|
|
89
|
+
│ │ │ ├── [id]/page.tsx # 'use client'; renders <FeatureForm mode="edit" />
|
|
65
90
|
│ │ │ ├── loading.tsx
|
|
66
|
-
│ │ │ ├── schema.ts
|
|
91
|
+
│ │ │ ├── schema.ts # Zod — SHARED with the matching Route Handler
|
|
67
92
|
│ │ │ ├── _components/
|
|
68
93
|
│ │ │ └── _hooks/
|
|
69
94
|
│ │ │
|
|
70
|
-
│ │ └── (admin)/ # OPTIONAL: role-gated. middleware enforces role.
|
|
95
|
+
│ │ └── (admin)/ # OPTIONAL: role-gated. middleware enforces role claim.
|
|
71
96
|
│ │ └── layout.tsx
|
|
72
97
|
│ │
|
|
73
98
|
│ ├── components/
|
|
@@ -86,24 +111,22 @@ This is the source-of-truth tree for a new project. Every file/folder listed her
|
|
|
86
111
|
│ │ │ ├── FormSelectField.tsx
|
|
87
112
|
│ │ │ ├── FormDatePickerField.tsx
|
|
88
113
|
│ │ │ └── FormTextareaField.tsx
|
|
89
|
-
│ │ ├── Notifications.tsx
|
|
90
|
-
│ │ ├── UnsavedChangesWarning.tsx
|
|
114
|
+
│ │ ├── Notifications.tsx
|
|
115
|
+
│ │ ├── UnsavedChangesWarning.tsx
|
|
91
116
|
│ │ └── AlertMessage.tsx
|
|
92
117
|
│ │
|
|
93
118
|
│ ├── redux/
|
|
94
|
-
│ │ ├── store.ts # typed; combined reducer;
|
|
95
|
-
│ │ ├── providers.tsx # 'use client'; <Provider
|
|
119
|
+
│ │ ├── store.ts # typed; combined reducer; signOut wipes cache via NextAuth event.
|
|
120
|
+
│ │ ├── providers.tsx # 'use client'; <SessionProvider><Provider><Toaster><ProgressBar>
|
|
96
121
|
│ │ ├── hooks.ts # typed useAppDispatch, useAppSelector
|
|
97
|
-
│ │
|
|
98
|
-
│ │
|
|
99
|
-
│ │
|
|
100
|
-
│ │
|
|
101
|
-
│ │ │ └── <domain>Api.ts
|
|
102
|
-
│ │ └── features/
|
|
103
|
-
│ │ ├── authSlice.ts
|
|
104
|
-
│ │ └── <other>Slice.ts # only for shared async/global state
|
|
122
|
+
│ │ └── api/
|
|
123
|
+
│ │ ├── api.ts # base createApi; baseUrl: '/api'; credentials: 'include'
|
|
124
|
+
│ │ ├── tags.ts # tag-type union
|
|
125
|
+
│ │ └── <domain>Api.ts # one file per domain — injectEndpoints
|
|
105
126
|
│ │
|
|
106
127
|
│ ├── lib/
|
|
128
|
+
│ │ ├── db.ts # PrismaClient SINGLETON (HMR-safe)
|
|
129
|
+
│ │ ├── api-auth.ts # requireSession() + ownership helpers for Route Handlers
|
|
107
130
|
│ │ ├── utils.ts # cn() — clsx + tailwind-merge
|
|
108
131
|
│ │ ├── zod-utils.ts # getDefaultValuesFromSchema, unwrapZodEffects, getMaxLengthsFromSchema
|
|
109
132
|
│ │ ├── formatters/
|
|
@@ -114,38 +137,48 @@ This is the source-of-truth tree for a new project. Every file/folder listed her
|
|
|
114
137
|
│ │
|
|
115
138
|
│ ├── config/
|
|
116
139
|
│ │ └── env.ts # Zod-parsed runtime env. Fail fast at startup.
|
|
140
|
+
│ │ # Required: AUTH_SECRET, DATABASE_URL. Plus OAuth keys if chosen.
|
|
117
141
|
│ │
|
|
118
|
-
│ └── types/
|
|
119
|
-
│
|
|
142
|
+
│ └── types/
|
|
143
|
+
│ ├── next-auth.d.ts # module augmentation — adds id, role to Session.user
|
|
144
|
+
│ └── api.ts # shared API envelope types (if any)
|
|
120
145
|
│
|
|
121
146
|
└── tests/
|
|
122
147
|
├── unit/
|
|
123
|
-
│ ├── auth.
|
|
124
|
-
│ ├──
|
|
148
|
+
│ ├── auth-helpers.test.ts # tests requireSession() with mocked auth()
|
|
149
|
+
│ ├── <feature>.schema.test.ts
|
|
125
150
|
│ └── zod-utils.test.ts
|
|
126
151
|
├── e2e/
|
|
127
|
-
│ └── auth.spec.ts
|
|
152
|
+
│ └── auth.spec.ts # log in (Credentials), land on dashboard, sign out
|
|
128
153
|
└── setup.ts # @testing-library/jest-dom, vi globals
|
|
129
154
|
```
|
|
130
155
|
|
|
131
156
|
## Rules that govern the layout
|
|
132
157
|
|
|
133
|
-
1. **
|
|
158
|
+
1. **The root `app/layout.tsx` is the only server component that matters.** Every `page.tsx` is `'use client'`. Group layouts (`(app)/layout.tsx`, `(public)/layout.tsx`) stay server components but only because they render zero data — they pass children through to a client shell. **No file under `src/app/**` outside `route.ts` calls `await db.*` or `fetch()` for data.**
|
|
159
|
+
|
|
160
|
+
2. **`src/app/api/**/route.ts` is the backend.** Every handler imports from `@/auth` (or `@/lib/api-auth`) and `@/lib/db`. Every handler authenticates first. Inputs validated with the feature's Zod schema. Responses are JSON.
|
|
161
|
+
|
|
162
|
+
3. **`_components/` and `_hooks/` are private.** Anything inside `_<name>/` may only be imported by files in the same route folder or its descendants. If two routes need the same component, lift it to `src/components/`.
|
|
163
|
+
|
|
164
|
+
4. **One RTK Query base; many injected slices.** `redux/api/api.ts` is the only file that calls `createApi(...)`. All other `*Api.ts` files import that `api` and call `api.injectEndpoints(...)`. `baseUrl` is `'/api'` (same-origin) and `credentials: 'include'` lets the NextAuth session cookie ride along.
|
|
165
|
+
|
|
166
|
+
5. **`middleware.ts` is the only auth gate at the network boundary.** It re-exports (or wraps) `auth` from `src/auth.config.ts` (Edge-safe — no DB imports). No `src/proxy.ts` or `src/middleware/*.ts` standing in for it.
|
|
134
167
|
|
|
135
|
-
|
|
168
|
+
6. **`src/auth.ts` vs `src/auth.config.ts`.** Two files because the Edge runtime (where `middleware.ts` runs) can't import the Prisma adapter or any Node-only code. `auth.config.ts` holds providers + callbacks that are Edge-safe. `auth.ts` extends that config with the adapter and re-exports `{ handlers, auth, signIn, signOut }`. The split is mandatory for v5 + Prisma — don't collapse it.
|
|
136
169
|
|
|
137
|
-
|
|
170
|
+
7. **Prisma migrations are committed.** `prisma/migrations/**` is checked in. CI runs `prisma migrate deploy`, not `prisma db push`. Never `prisma db push` in CI.
|
|
138
171
|
|
|
139
|
-
|
|
172
|
+
8. **Domain models live with their feature.** A `Customer` type used only by `(app)/customers/` lives in `(app)/customers/types.ts` or is inferred from the Zod schema. Cross-cutting types in `src/types/`.
|
|
140
173
|
|
|
141
|
-
|
|
174
|
+
9. **Feature schemas are local AND shared with handlers.** `(app)/<feature>/schema.ts` exports Zod schemas owned by that feature. The matching `app/api/<feature>/route.ts` imports the same schema for request validation. Same Zod, two sides.
|
|
142
175
|
|
|
143
|
-
|
|
176
|
+
10. **`tests/` mirrors `src/` loosely.** Tests organized by what they test, not 1:1 parity.
|
|
144
177
|
|
|
145
|
-
|
|
178
|
+
11. **`public/` holds only static assets.** No code.
|
|
146
179
|
|
|
147
|
-
|
|
180
|
+
12. **No duplicate-by-case paths.** Linux CI breaks what works on Windows/macOS.
|
|
148
181
|
|
|
149
|
-
|
|
182
|
+
13. **`components/ui/` is owned, not imported.** shadcn primitives are generated *into* the project.
|
|
150
183
|
|
|
151
|
-
|
|
184
|
+
14. **No `'use server'` directive anywhere.** This project doesn't use Server Actions. Mutations go through RTK Query → Route Handlers.
|