@dennisrongo/skills 0.1.3 → 0.3.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.
Files changed (30) hide show
  1. package/README.md +87 -38
  2. package/package.json +1 -1
  3. package/skills/code-review/SKILL.md +270 -0
  4. package/skills/codebase-explainer/SKILL.md +112 -0
  5. package/skills/diagnose/SKILL.md +31 -1
  6. package/skills/dotnet-onion-api/SKILL.md +1 -0
  7. package/skills/nextjs-app-router/SKILL.md +229 -175
  8. package/skills/nextjs-app-router/references/anti-patterns.md +163 -99
  9. package/skills/nextjs-app-router/references/folder-layout.md +79 -46
  10. package/skills/nextjs-app-router/references/good-patterns.md +233 -99
  11. package/skills/nextjs-app-router/references/templates/api-base.md +24 -21
  12. package/skills/nextjs-app-router/references/templates/auth-slice.md +82 -78
  13. package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +58 -6
  14. package/skills/nextjs-app-router/references/templates/db-client.md +48 -0
  15. package/skills/nextjs-app-router/references/templates/env-and-utils.md +90 -23
  16. package/skills/nextjs-app-router/references/templates/feature-slice.md +110 -47
  17. package/skills/nextjs-app-router/references/templates/form-with-zod.md +23 -15
  18. package/skills/nextjs-app-router/references/templates/middleware.md +69 -59
  19. package/skills/nextjs-app-router/references/templates/next-config.md +4 -14
  20. package/skills/nextjs-app-router/references/templates/nextauth-config.md +178 -0
  21. package/skills/nextjs-app-router/references/templates/package.md +35 -5
  22. package/skills/nextjs-app-router/references/templates/prisma-schema.md +162 -0
  23. package/skills/nextjs-app-router/references/templates/providers-and-store.md +35 -21
  24. package/skills/nextjs-app-router/references/templates/root-layout.md +99 -20
  25. package/skills/nextjs-app-router/references/templates/route-group-layouts.md +46 -6
  26. package/skills/nextjs-app-router/references/templates/route-handler.md +168 -0
  27. package/skills/nextjs-app-router/references/templates/testing.md +105 -31
  28. package/skills/plan-and-build/SKILL.md +45 -3
  29. package/skills/pr-review/SKILL.md +50 -3
  30. package/skills/tauri-2-app/SKILL.md +1 -0
@@ -1,34 +1,151 @@
1
1
  # Good Patterns to Keep
2
2
 
3
- These are the patterns the scaffolder must emit by default. Each one is here because it solves a recurring problem in real-world Next.js + Redux + Tailwind projects.
3
+ These are the patterns the scaffolder must emit by default. Each one is here because it solves a recurring problem in real-world Next.js + NextAuth + Prisma + Redux Toolkit projects.
4
4
 
5
5
  ## 1. Route groups for auth and role boundaries
6
6
 
7
- `app/(public)/`, `app/(app)/`, `app/(admin)/` carve the route tree by who is allowed in. Each group has its own `layout.tsx`, so the chrome (or lack of it) for unauthenticated pages doesn't bleed into authenticated pages. Groups don't appear in URLs they're purely an organizational tool.
7
+ `app/(public)/`, `app/(app)/`, `app/(admin)/` carve the route tree by who is allowed in. Each group has its own `layout.tsx`. The `middleware.ts` `config.matcher` mirrors the group structure and gates on the server.
8
8
 
9
- **Why it works:** auth boundaries become a property of the file system, not a runtime check sprinkled into every page. The `middleware.ts` `config.matcher` mirrors the group structure and gates on the server.
9
+ **Why it works:** auth boundaries become a property of the file system, not a runtime check sprinkled into every page.
10
10
 
11
- ## 2. Server-by-default with `'use client'` islands
11
+ ## 2. SPA-style pages, server root layout only
12
12
 
13
- `app/layout.tsx`, `app/page.tsx`, every group `layout.tsx`, and every feature `page.tsx` is a server component. The interactive parts forms, table state, sidebars with togglesare extracted into `_components/Something.tsx` files marked `'use client'` and imported from the server page.
13
+ `src/app/layout.tsx` is a server component that renders `<html><body><Providers>{children}</Providers></body></html>`. **Every other `page.tsx` is `'use client'`** and uses RTK Query hooks for data. Per-route metadata lives at the layout level (server) pages can't export `metadata` because they're client. This is the deliberate "API-driven, not SSR" choice.
14
14
 
15
- **Why it works:** SSR for free, streaming, smaller client JS, `metadata` exports work, server-only data sources (cookies, DB calls) are reachable from the page.
15
+ **Why it works:** one mental model for data flow (UI RTK Query → /api → DB), trivial mocking in tests, no "server or client?" cargo culting, no second mutation path via Server Actions.
16
16
 
17
- ## 3. Server `redirect()` and middleware-driven navigation
17
+ ## 3. NextAuth (Auth.js v5) as the single auth source
18
18
 
19
- Top-level redirects (e.g. `/` → `/dashboard`) are server calls inside a server `page.tsx`. Auth-conditional redirects live in `middleware.ts`. The client `useRouter().push()` is reserved for in-app navigation triggered by user actions (button clicks, form submissions).
19
+ ```ts
20
+ // src/auth.ts
21
+ import NextAuth from 'next-auth';
22
+ import { PrismaAdapter } from '@auth/prisma-adapter';
23
+ import { db } from '@/lib/db';
24
+ import authConfig from './auth.config';
25
+
26
+ export const { handlers, auth, signIn, signOut } = NextAuth({
27
+ ...authConfig,
28
+ adapter: PrismaAdapter(db),
29
+ session: { strategy: 'jwt' }, // required when Credentials provider is present
30
+ });
31
+ ```
20
32
 
21
- ## 4. One RTK Query base, many injected slices
33
+ ```ts
34
+ // src/auth.config.ts — EDGE-SAFE (no DB imports)
35
+ import type { NextAuthConfig } from 'next-auth';
36
+ import Credentials from 'next-auth/providers/credentials';
37
+
38
+ export default {
39
+ providers: [Credentials({ /* authorize: dynamically imported in auth.ts */ })],
40
+ pages: { signIn: '/auth/login' },
41
+ callbacks: {
42
+ async jwt({ token, user }) { if (user) token.id = user.id; return token; },
43
+ async session({ session, token }) { if (token?.id) session.user.id = token.id as string; return session; },
44
+ },
45
+ } satisfies NextAuthConfig;
46
+ ```
47
+
48
+ ```ts
49
+ // middleware.ts
50
+ import NextAuth from 'next-auth';
51
+ import authConfig from '@/auth.config';
52
+ export const { auth: middleware } = NextAuth(authConfig);
53
+ export const config = { matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico).*)'] };
54
+ ```
55
+
56
+ **Why split into two files:** middleware runs on the Edge runtime, which can't import the Prisma adapter. `auth.config.ts` is Edge-safe; `auth.ts` extends it with the adapter and is only imported from Node code (Route Handlers, server components).
57
+
58
+ ## 4. Route Handlers as the backend
59
+
60
+ ```ts
61
+ // src/app/api/customers/route.ts
62
+ import { NextResponse } from 'next/server';
63
+ import { db } from '@/lib/db';
64
+ import { requireSession } from '@/lib/api-auth';
65
+ import { customerSchema } from '@/app/(app)/customers/schema';
66
+
67
+ export async function GET() {
68
+ const session = await requireSession();
69
+ const customers = await db.customer.findMany({ where: { userId: session.user.id } });
70
+ return NextResponse.json(customers);
71
+ }
72
+
73
+ export async function POST(req: Request) {
74
+ const session = await requireSession();
75
+ const parsed = customerSchema.safeParse(await req.json());
76
+ if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
77
+ const created = await db.customer.create({ data: { ...parsed.data, userId: session.user.id } });
78
+ return NextResponse.json(created, { status: 201 });
79
+ }
80
+ ```
81
+
82
+ **Why it works:** every backend operation has one home, one auth check, one Zod validation pass against the same schema the form uses, one Prisma call. Ownership is enforced via `userId: session.user.id`.
83
+
84
+ ## 5. `requireSession()` helper for handlers
85
+
86
+ ```ts
87
+ // src/lib/api-auth.ts
88
+ import { auth } from '@/auth';
89
+
90
+ export async function requireSession() {
91
+ const session = await auth();
92
+ if (!session?.user?.id) {
93
+ throw new Response(JSON.stringify({ error: 'Unauthorized' }), {
94
+ status: 401,
95
+ headers: { 'content-type': 'application/json' },
96
+ });
97
+ }
98
+ return session as typeof session & { user: { id: string } };
99
+ }
100
+ ```
101
+
102
+ Handlers `await requireSession()`; on failure, the thrown `Response` bubbles up as a 401 (Next handles thrown Responses in handlers cleanly in v14+; if not, wrap in try/catch and return). Saves five lines in every handler and guarantees the check exists.
103
+
104
+ ## 6. Prisma singleton
105
+
106
+ ```ts
107
+ // src/lib/db.ts
108
+ import { PrismaClient } from '@prisma/client';
109
+
110
+ const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
111
+
112
+ export const db = globalForPrisma.prisma ?? new PrismaClient();
113
+
114
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
115
+ ```
116
+
117
+ **Why it works:** dev hot-reload creates new modules but reuses the global. Without this, you exhaust your Postgres connection pool in five minutes of editing.
118
+
119
+ ## 7. One RTK Query base, many injected slices
22
120
 
23
121
  ```ts
24
122
  // redux/api/api.ts
123
+ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
124
+ import { signOut } from 'next-auth/react';
125
+ import { TAG_TYPES } from './tags';
126
+
127
+ const rawBaseQuery = fetchBaseQuery({
128
+ baseUrl: '/api',
129
+ credentials: 'include', // ride the NextAuth session cookie (same-origin)
130
+ });
131
+
132
+ export const baseQueryWithAuth: BaseQueryFn<...> = async (args, api, extra) => {
133
+ const result = await rawBaseQuery(args, api, extra);
134
+ if (result.error?.status === 401) {
135
+ void signOut({ callbackUrl: '/auth/login', redirect: true });
136
+ }
137
+ return result;
138
+ };
139
+
25
140
  export const api = createApi({
26
141
  reducerPath: 'api',
27
- baseQuery: authAwareBaseQuery,
142
+ baseQuery: baseQueryWithAuth,
28
143
  tagTypes: TAG_TYPES,
29
144
  endpoints: () => ({}),
30
145
  });
146
+ ```
31
147
 
148
+ ```ts
32
149
  // redux/api/customersApi.ts
33
150
  export const customersApi = api.injectEndpoints({
34
151
  endpoints: (build) => ({
@@ -44,37 +161,39 @@ export const customersApi = api.injectEndpoints({
44
161
  });
45
162
  ```
46
163
 
47
- **Why it works:** one cache, one middleware, one set of tag types. Cross-domain invalidation just works (a mutation in `invoicesApi` can invalidate `Customers` tags). The base file stays small; domain logic lives next to the domain.
48
-
49
- ## 5. Auth-aware `baseQuery` with clean 401 handling
50
-
51
- `baseQuery` injects the session into requests (via cookies for `httpOnly` flows, or via a `prepareHeaders` callback that reads from state for bearer flows). On a 401, it dispatches a typed logout action that the root reducer uses to reset state, then signals the router (not `window.location`) to navigate to `/auth/login`.
52
-
53
- **Why it works:** no hard reloads, no race conditions when multiple in-flight requests 401, and the redirect target stays consistent with whatever `middleware.ts` would have done.
54
-
55
- ## 6. State-reset on logout via wrapped root reducer
56
-
57
- ```ts
58
- const combinedReducer = combineReducers({ ... });
59
-
60
- export const store = configureStore({
61
- reducer: (state, action) => {
62
- if (authApi.endpoints.logout.matchFulfilled(action)) {
63
- return combinedReducer(undefined, action);
64
- }
65
- return combinedReducer(state, action);
66
- },
67
- // ...
68
- });
164
+ **Why it works:** one cache, one middleware, one set of tag types. Cross-domain invalidation just works. `baseUrl: '/api'` keeps requests same-origin so the NextAuth session cookie rides along.
165
+
166
+ ## 8. SessionProvider + Redux Provider co-located
167
+
168
+ ```tsx
169
+ // src/redux/providers.tsx
170
+ 'use client';
171
+ import { SessionProvider } from 'next-auth/react';
172
+ import { Provider } from 'react-redux';
173
+ import { AppProgressBar as ProgressBar } from 'next-nprogress-bar';
174
+ import { Toaster } from '@/components/ui/toaster';
175
+ import { store } from './store';
176
+
177
+ export function Providers({ children }: { children: React.ReactNode }) {
178
+ return (
179
+ <SessionProvider>
180
+ <Provider store={store}>
181
+ {children}
182
+ <Toaster />
183
+ <ProgressBar height="3px" color="hsl(var(--primary))" shallowRouting options={{ showSpinner: false }} />
184
+ </Provider>
185
+ </SessionProvider>
186
+ );
187
+ }
69
188
  ```
70
189
 
71
- **Why it works:** logout wipes every slice (including RTK Query cache) in one place. No per-slice `extraReducers` listening for the logout action. Fully typed — no `@ts-ignore`.
190
+ **Why this order:** `SessionProvider` outside so any component (including Redux-connected ones) can `useSession()`. Redux inside so RTK Query mutations can dispatch.
72
191
 
73
- ## 7. Typed Redux hooks
192
+ ## 9. Typed Redux hooks
74
193
 
75
- `redux/hooks.ts` exports `useAppDispatch` and `useAppSelector` typed against `RootState` and `AppDispatch`. Components never import the raw `useDispatch` / `useSelector` from `react-redux`.
194
+ `redux/hooks.ts` exports `useAppDispatch` and `useAppSelector` typed against `RootState` and `AppDispatch`. Components never import the raw `useDispatch` / `useSelector`.
76
195
 
77
- ## 8. React Hook Form + Zod with schema-introspected defaults
196
+ ## 10. React Hook Form + Zod with schema-introspected defaults
78
197
 
79
198
  ```ts
80
199
  const schema = z.object({ name: z.string().min(1), age: z.number().min(0) });
@@ -85,50 +204,44 @@ const form = useForm<z.infer<typeof schema>>({
85
204
  });
86
205
  ```
87
206
 
88
- `getDefaultValuesFromSchema` walks the Zod tree and produces a defaults object that matches the schema shape (empty string for `z.string()`, `null` for nullable, `0` for numeric, etc.). `unwrapZodEffects` peels `.refine()` / `.transform()` wrappers so the introspection works.
89
-
90
- `getMaxLengthsFromSchema` reads `z.string().max(N)` and feeds the `maxLength` attribute to the rendered `<Input>`.
91
-
92
- **Why it works:** the schema is the single source of truth for both validation and field configuration. No drift between "what the form accepts" and "what the API will accept."
93
-
94
- ## 9. Composed form fields built on shadcn `<Form>`
207
+ `getDefaultValuesFromSchema` walks the Zod tree and produces a defaults object matching the schema shape. **The same `schema` is imported in `app/api/<feature>/route.ts` and used to validate request bodies** one source of truth for form + handler validation.
95
208
 
96
- `components/forms/FormInputField.tsx` and siblings take `form`, `schema`, `fieldName`, `formLabel` props and render a fully wired `<FormField>`, `<FormItem>`, `<FormLabel>`, `<FormControl>`, `<FormMessage>` block. Every form in the app reuses these.
209
+ ## 11. Composed form fields built on shadcn `<Form>`
97
210
 
98
- **Why it works:** form layout/spacing/error display is consistent across the app, and changes to the shared field components propagate everywhere. New forms are mostly schema + a list of fields.
211
+ `components/forms/FormInputField.tsx` and siblings take `form`, `schema`, `fieldName`, `formLabel` props and render a fully wired `<FormField>`/`<FormItem>`/`<FormLabel>`/`<FormControl>`/`<FormMessage>` block. Every form reuses these.
99
212
 
100
- ## 10. `UnsavedChangesWarning` tied to RHF `formState.isDirty`
213
+ ## 12. `UnsavedChangesWarning` tied to RHF `formState.isDirty`
101
214
 
102
- A small component that hooks into in-app navigation (Next router events) and `window.beforeunload` to warn when a form has unsaved changes. Used inside every non-trivial form.
215
+ A small component that hooks into `window.beforeunload` (and Next router events when the API stabilizes) to warn when a form has unsaved changes.
103
216
 
104
- ## 11. `error.tsx`, `loading.tsx`, `not-found.tsx` at meaningful segments
217
+ ## 13. `error.tsx`, `loading.tsx`, `not-found.tsx` at meaningful segments
105
218
 
106
- At minimum: global ones at `app/`, group-level ones at `(app)/`, and feature-level `loading.tsx` for any feature that fetches data via Suspense.
219
+ At minimum: global ones at `app/`, group-level ones at `(app)/`, and feature-level `loading.tsx` for any feature that fetches data.
107
220
 
108
- ## 12. Runtime env validation
221
+ ## 14. Runtime env validation
109
222
 
110
223
  ```ts
111
224
  // src/config/env.ts
112
225
  import { z } from 'zod';
113
226
 
114
227
  const envSchema = z.object({
115
- NEXT_PUBLIC_API_BASE_URL: z.string().url(),
116
- NEXT_PUBLIC_PUSHER_KEY: z.string().optional(),
228
+ AUTH_SECRET: z.string().min(32),
229
+ AUTH_URL: z.string().url().optional(), // required in prod, optional locally
230
+ DATABASE_URL: z.string().url(),
231
+ AUTH_GITHUB_ID: z.string().optional(),
232
+ AUTH_GITHUB_SECRET: z.string().optional(),
117
233
  });
118
234
 
119
- export const env = envSchema.parse({
120
- NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
121
- NEXT_PUBLIC_PUSHER_KEY: process.env.NEXT_PUBLIC_PUSHER_KEY,
122
- });
235
+ export const env = envSchema.parse(process.env);
123
236
  ```
124
237
 
125
- **Why it works:** misconfiguration crashes the app at startup with a clear error, not deep inside a request handler with a cryptic `undefined` later.
238
+ **Why:** misconfiguration crashes the app at startup with a clear error, not deep inside a Route Handler with a cryptic `undefined`.
126
239
 
127
- ## 13. shadcn primitives owned by the project
240
+ ## 15. shadcn primitives owned by the project
128
241
 
129
- `components/ui/button.tsx`, `input.tsx`, `form.tsx`, etc. live in the repo and are editable. They are not pulled from `node_modules`. When the design system evolves, you edit these files; you don't fork a library.
242
+ `components/ui/button.tsx`, `input.tsx`, `form.tsx`, etc. live in the repo and are editable. Not pulled from `node_modules`.
130
243
 
131
- ## 14. `cn()` in `lib/utils.ts`
244
+ ## 16. `cn()` in `lib/utils.ts`
132
245
 
133
246
  ```ts
134
247
  import { clsx, type ClassValue } from 'clsx';
@@ -136,77 +249,98 @@ import { twMerge } from 'tailwind-merge';
136
249
  export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
137
250
  ```
138
251
 
139
- Single source of class merging across the app.
252
+ Single source of class merging.
140
253
 
141
- ## 15. Per-feature `_components/` and `_hooks/`
254
+ ## 17. Per-feature `_components/` and `_hooks/`
142
255
 
143
- Co-locate the implementation details with the route. When a feature is deleted, the entire folder goes with it; no orphaned files in a shared directory. Promote to `src/components/` only when a second feature genuinely needs the same code.
256
+ Co-locate implementation details with the route. When a feature is deleted, the entire folder goes with it. Promote to `src/components/` only when a second feature genuinely needs it.
144
257
 
145
- ## 16. One date library: `date-fns`
258
+ ## 18. One date library: `date-fns`
146
259
 
147
- Tree-shakeable, immutable, ESM-friendly. Format helpers live in `lib/formatters/date.ts` so the rest of the codebase doesn't import `date-fns` directly — the wrapper makes it easy to change the underlying library later.
260
+ Tree-shakeable, immutable, ESM-friendly. Format helpers live in `lib/formatters/date.ts`.
148
261
 
149
- ## 17. Strict TypeScript
262
+ ## 19. Strict TypeScript
150
263
 
151
- `tsconfig.json` with `"strict": true`, `"noUncheckedIndexedAccess": true`, `"forceConsistentCasingInFileNames": true`. The last one catches the duplicate-by-case folder bug at compile time, which is the only place to catch it before Linux production.
264
+ `"strict": true`, `"noUncheckedIndexedAccess": true`, `"forceConsistentCasingInFileNames": true`. The last one catches the duplicate-by-case folder bug at compile time.
152
265
 
153
- ## 18. Strict ESLint
266
+ ## 20. Strict ESLint
154
267
 
155
- `@typescript-eslint/recommended`, `plugin:react-hooks/recommended`, `plugin:jsx-a11y/recommended`, plus `no-console: warn`, `no-explicit-any: error`, `exhaustive-deps: error`. Pre-commit hook runs `eslint --fix` so the rules actually get enforced.
268
+ `@typescript-eslint/recommended`, `react-hooks/recommended`, `jsx-a11y/recommended`, plus `no-console: warn`, `no-explicit-any: error`, `exhaustive-deps: error`. Pre-commit hook runs `eslint --fix`.
156
269
 
157
- ## 19. `middleware.ts` as the auth gate
270
+ ## 21. `middleware.ts` as the auth gate
158
271
 
159
272
  ```ts
160
273
  // middleware.ts
161
- import { NextResponse, type NextRequest } from 'next/server';
162
-
163
- export function middleware(req: NextRequest) {
164
- const session = req.cookies.get('session')?.value;
165
- if (!session && req.nextUrl.pathname.startsWith('/app')) {
166
- const url = req.nextUrl.clone();
167
- url.pathname = '/auth/login';
168
- url.searchParams.set('returnTo', req.nextUrl.pathname);
169
- return NextResponse.redirect(url);
170
- }
171
- return NextResponse.next();
172
- }
274
+ import NextAuth from 'next-auth';
275
+ import authConfig from '@/auth.config';
276
+
277
+ export const { auth: middleware } = NextAuth(authConfig);
173
278
 
174
279
  export const config = {
175
- matcher: ['/((?!_next/static|_next/image|favicon.ico|api/public).*)'],
280
+ // /api/auth is NextAuth's own; static assets excluded.
281
+ matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)'],
176
282
  };
177
283
  ```
178
284
 
179
- (The matcher syntax here is illustrative the scaffolder picks one that mirrors the project's actual route groups.)
285
+ Path-specific redirects (e.g. `/` `/app/dashboard` for signed-in users) live in a custom wrapper around `auth()` if needed.
180
286
 
181
- ## 20. Vitest + RTL for unit, Playwright for E2E
287
+ ## 22. Prisma migrations checked in
182
288
 
183
- A new project gets at least:
184
- - One reducer/slice test (catches accidental shape changes).
185
- - One form-renders-and-submits test (catches RHF/zod wiring regressions).
186
- - One Playwright spec that boots the app, logs in, and asserts the dashboard renders (catches middleware and provider regressions).
289
+ `prisma/migrations/**` is committed. CI runs `prisma migrate deploy`. `prisma db push` is only for throwaway prototyping.
290
+
291
+ ## 23. Module augmentation for `Session.user.id` / role
292
+
293
+ ```ts
294
+ // src/types/next-auth.d.ts
295
+ import 'next-auth';
296
+
297
+ declare module 'next-auth' {
298
+ interface Session {
299
+ user: { id: string; email: string; name?: string | null; image?: string | null; role?: string };
300
+ }
301
+ }
302
+
303
+ declare module 'next-auth/jwt' {
304
+ interface JWT { id: string; role?: string }
305
+ }
306
+ ```
187
307
 
188
- Three tests is enough to make CI meaningful.
308
+ Without this, `session.user.id` is `undefined` in TypeScript even though the runtime callback added it.
189
309
 
190
- ## 21. Husky + lint-staged
310
+ ## 24. Vitest + RTL for unit, Playwright for E2E
311
+
312
+ A new project gets at least:
313
+ - One reducer/slice test.
314
+ - One Zod schema test (validates valid + invalid payloads).
315
+ - One form-renders test.
316
+ - One Playwright spec that logs in (Credentials), lands on dashboard, signs out.
317
+
318
+ ## 25. Husky + lint-staged
191
319
 
192
320
  ```jsonc
193
- // package.json
194
321
  "lint-staged": {
195
322
  "*.{ts,tsx}": ["prettier --write", "eslint --fix"],
196
323
  "*.{json,md,css}": ["prettier --write"]
197
324
  }
198
325
  ```
199
326
 
200
- Pre-commit hook keeps formatting and lint clean without anyone having to remember.
327
+ ## 26. GitHub Actions CI with a Postgres service
201
328
 
202
- ## 22. GitHub Actions CI
329
+ ```yaml
330
+ services:
331
+ postgres:
332
+ image: postgres:16
333
+ env: { POSTGRES_PASSWORD: postgres }
334
+ ports: ['5432:5432']
335
+ options: --health-cmd pg_isready --health-interval 10s
336
+ ```
203
337
 
204
- A single `ci.yml` that runs install → lint → typecheck → test → build on every PR. Build catches RSC/server-component boundary errors that `tsc --noEmit` misses.
338
+ Steps: install `prisma migrate deploy` → lint → typecheck → test → build.
205
339
 
206
- ## 23. `next-nprogress-bar` for top loading bar
340
+ ## 27. `next-nprogress-bar` for top loading bar
207
341
 
208
- One small, well-maintained library that hooks into Next.js's router events. Configured once in `providers.tsx`.
342
+ One small, well-maintained library. Configured in `providers.tsx`.
209
343
 
210
- ## 24. Toaster via shadcn `use-toast` (or `react-toastify`, not both)
344
+ ## 28. Toaster via shadcn `use-toast`
211
345
 
212
- Pick one toast library and wrap it. Composed `Notifications.tsx` is what features call; the underlying library can change without touching feature code.
346
+ One toast library. Composed `Notifications.tsx` wraps it; features call the wrapper.
@@ -5,7 +5,6 @@
5
5
  ```ts
6
6
  // Single source of truth for cache tags. Every domain adds to this union.
7
7
  export const TAG_TYPES = [
8
- 'Auth',
9
8
  // Add domain tags here as features are added, e.g. 'Customers', 'Customer', 'Invoices', 'Invoice'.
10
9
  ] as const;
11
10
 
@@ -15,34 +14,34 @@ export type TagType = (typeof TAG_TYPES)[number];
15
14
  ## `src/redux/api/api.ts`
16
15
 
17
16
  ```ts
18
- import { createApi, fetchBaseQuery, type BaseQueryFn, type FetchArgs, type FetchBaseQueryError } from '@reduxjs/toolkit/query/react';
19
- import { env } from '@/config/env';
17
+ import {
18
+ createApi,
19
+ fetchBaseQuery,
20
+ type BaseQueryFn,
21
+ type FetchArgs,
22
+ type FetchBaseQueryError,
23
+ } from '@reduxjs/toolkit/query/react';
24
+ import { signOut } from 'next-auth/react';
20
25
  import { TAG_TYPES } from './tags';
21
- import { logout } from '@/redux/features/authSlice';
26
+ import { signOutEvent } from '../store';
22
27
 
23
28
  const rawBaseQuery = fetchBaseQuery({
24
- baseUrl: env.NEXT_PUBLIC_API_BASE_URL,
25
- // For httpOnly cookie auth, this is mandatory. Drop it only for bearer-token-only flows.
29
+ baseUrl: '/api',
30
+ // Same-origin the NextAuth session cookie rides along automatically.
26
31
  credentials: 'include',
27
- prepareHeaders: (headers, { getState }) => {
28
- // If the project uses a JS-readable bearer token, attach it here.
29
- // For httpOnly cookie flows, leave this empty — the browser sends the cookie.
30
- const token = (getState() as { auth?: { token?: string } }).auth?.token;
31
- if (token) headers.set('Authorization', `Bearer ${token}`);
32
- return headers;
33
- },
34
32
  });
35
33
 
36
- export const baseQueryWithAuth: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (
37
- args,
38
- api,
39
- extraOptions,
40
- ) => {
34
+ export const baseQueryWithAuth: BaseQueryFn<
35
+ string | FetchArgs,
36
+ unknown,
37
+ FetchBaseQueryError
38
+ > = async (args, api, extraOptions) => {
41
39
  const result = await rawBaseQuery(args, api, extraOptions);
42
40
 
43
41
  if (result.error?.status === 401) {
44
- // Clean logout: dispatch action root reducer resets state middleware/router handles redirect.
45
- api.dispatch(logout());
42
+ // Wipe Redux state first, then let NextAuth clear the cookie and redirect.
43
+ api.dispatch(signOutEvent);
44
+ void signOut({ callbackUrl: '/auth/login', redirect: true });
46
45
  }
47
46
 
48
47
  return result;
@@ -57,6 +56,10 @@ export const api = createApi({
57
56
  ```
58
57
 
59
58
  **Notes:**
59
+
60
+ - `baseUrl: '/api'` — requests go to the in-app Route Handlers under `src/app/api/**`. Same-origin means the NextAuth session cookie is sent automatically.
61
+ - `credentials: 'include'` — required so the cookie rides on every request. Don't drop it.
60
62
  - `createApi` is called **once** in this file. Every other `*Api.ts` file uses `api.injectEndpoints(...)`.
61
- - The 401 handler dispatches a plain action; it does **not** call `window.location.href = '...'`. Navigation is handled by middleware on the next request or by an in-app guard listening to the auth state.
63
+ - The 401 handler dispatches `signOutEvent` (resets Redux state via the root reducer) then calls NextAuth's `signOut` which clears the cookie and routes to `/auth/login`. **Never** `window.location.href = '...'` here.
62
64
  - `tagTypes` reads from the shared `TAG_TYPES` constant — domains add to that one array, not here.
65
+ - No `prepareHeaders` injecting an `Authorization: Bearer` token. NextAuth uses an `httpOnly` session cookie; the browser handles it.