@dennisrongo/skills 0.1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/bin/claude-skills.js +189 -0
  4. package/lib/install.js +111 -0
  5. package/lib/list.js +63 -0
  6. package/lib/paths.js +39 -0
  7. package/lib/remove.js +52 -0
  8. package/lib/skills.js +121 -0
  9. package/package.json +48 -0
  10. package/skills/_template/SKILL.md +34 -0
  11. package/skills/conventional-commits/SKILL.md +136 -0
  12. package/skills/diagnose/SKILL.md +140 -0
  13. package/skills/dotnet-onion-api/SKILL.md +267 -0
  14. package/skills/dotnet-onion-api/references/anti-patterns.md +155 -0
  15. package/skills/dotnet-onion-api/references/solution-layout.md +113 -0
  16. package/skills/dotnet-onion-api/references/templates/appsettings.md +75 -0
  17. package/skills/dotnet-onion-api/references/templates/base-controller.cs.md +90 -0
  18. package/skills/dotnet-onion-api/references/templates/csproj-files.md +178 -0
  19. package/skills/dotnet-onion-api/references/templates/dbcontext.cs.md +149 -0
  20. package/skills/dotnet-onion-api/references/templates/exception-middleware.cs.md +101 -0
  21. package/skills/dotnet-onion-api/references/templates/feature-slice.md +349 -0
  22. package/skills/dotnet-onion-api/references/templates/program-cs.md +171 -0
  23. package/skills/dotnet-onion-api/references/templates/worker-program.cs.md +166 -0
  24. package/skills/grill-with-docs/SKILL.md +203 -0
  25. package/skills/handoff/SKILL.md +155 -0
  26. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  27. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  28. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  29. package/skills/improve-codebase-architecture/SKILL.md +121 -0
  30. package/skills/nextjs-app-router/SKILL.md +328 -0
  31. package/skills/nextjs-app-router/references/anti-patterns.md +203 -0
  32. package/skills/nextjs-app-router/references/folder-layout.md +151 -0
  33. package/skills/nextjs-app-router/references/good-patterns.md +212 -0
  34. package/skills/nextjs-app-router/references/templates/api-base.md +62 -0
  35. package/skills/nextjs-app-router/references/templates/api-slice.md +75 -0
  36. package/skills/nextjs-app-router/references/templates/auth-slice.md +95 -0
  37. package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +94 -0
  38. package/skills/nextjs-app-router/references/templates/components-json.md +41 -0
  39. package/skills/nextjs-app-router/references/templates/env-and-utils.md +110 -0
  40. package/skills/nextjs-app-router/references/templates/eslint-prettier.md +82 -0
  41. package/skills/nextjs-app-router/references/templates/feature-slice.md +184 -0
  42. package/skills/nextjs-app-router/references/templates/form-with-zod.md +192 -0
  43. package/skills/nextjs-app-router/references/templates/middleware.md +88 -0
  44. package/skills/nextjs-app-router/references/templates/next-config.md +42 -0
  45. package/skills/nextjs-app-router/references/templates/package.md +99 -0
  46. package/skills/nextjs-app-router/references/templates/providers-and-store.md +79 -0
  47. package/skills/nextjs-app-router/references/templates/root-layout.md +146 -0
  48. package/skills/nextjs-app-router/references/templates/route-group-layouts.md +90 -0
  49. package/skills/nextjs-app-router/references/templates/tailwind-config.md +89 -0
  50. package/skills/nextjs-app-router/references/templates/testing.md +137 -0
  51. package/skills/nextjs-app-router/references/templates/tsconfig.md +40 -0
  52. package/skills/plan-and-build/SKILL.md +214 -0
  53. package/skills/pr-review/SKILL.md +132 -0
  54. package/skills/write-a-skill/SKILL.md +191 -0
@@ -0,0 +1,151 @@
1
+ # Canonical Folder Layout
2
+
3
+ This is the source-of-truth tree for a new project. Every file/folder listed here exists in a scaffolded project unless the user opted out. The skill must not reorganize this layout when adding features.
4
+
5
+ ```
6
+ {{project-name}}/
7
+ ├── .env.example # committed; documents required env vars
8
+ ├── .eslintrc.json # or eslint.config.mjs — strict TS + a11y + hooks
9
+ ├── .gitignore
10
+ ├── .husky/
11
+ │ └── pre-commit # runs lint-staged
12
+ ├── .prettierrc
13
+ ├── README.md
14
+ ├── components.json # shadcn config; rsc: true, tsx: true
15
+ ├── middleware.ts # AUTH GUARD. Wired via Next.js convention.
16
+ ├── next.config.ts
17
+ ├── next-env.d.ts # generated; do not edit
18
+ ├── package.json # scripts: dev, build, start, lint, typecheck, test, test:e2e
19
+ ├── playwright.config.ts
20
+ ├── postcss.config.js
21
+ ├── tailwind.config.ts
22
+ ├── tsconfig.json # strict + @/* alias
23
+ ├── vitest.config.ts
24
+
25
+ ├── .github/
26
+ │ └── workflows/
27
+ │ └── ci.yml # install → lint → typecheck → test → build
28
+
29
+ ├── public/ # static assets only (favicons, robots.txt, og images)
30
+ │ └── favicon.ico
31
+
32
+ ├── src/
33
+ │ ├── app/
34
+ │ │ ├── layout.tsx # SERVER. <html><body><Providers>{children}
35
+ │ │ ├── page.tsx # SERVER. redirect('/dashboard') or render landing.
36
+ │ │ ├── error.tsx # global error boundary
37
+ │ │ ├── not-found.tsx
38
+ │ │ ├── globals.css
39
+ │ │ ├── favicon.ico # (optional duplicate of public/)
40
+ │ │ │
41
+ │ │ ├── (public)/ # unauthenticated routes
42
+ │ │ │ ├── layout.tsx # SERVER. minimal chrome (centered card).
43
+ │ │ │ └── auth/
44
+ │ │ │ ├── login/
45
+ │ │ │ │ ├── page.tsx
46
+ │ │ │ │ └── _components/LoginForm.tsx # 'use client'
47
+ │ │ │ ├── signup/
48
+ │ │ │ └── forgot-password/
49
+ │ │ │
50
+ │ │ ├── (app)/ # authenticated routes
51
+ │ │ │ ├── layout.tsx # SERVER. reads session, renders shell.
52
+ │ │ │ ├── loading.tsx
53
+ │ │ │ ├── error.tsx
54
+ │ │ │ ├── _components/
55
+ │ │ │ │ ├── AppShell.tsx # 'use client'; Sidebar + Header
56
+ │ │ │ │ └── Sidebar.tsx
57
+ │ │ │ ├── _hooks/
58
+ │ │ │ │ └── useSessionExpiryWarning.ts
59
+ │ │ │ ├── dashboard/
60
+ │ │ │ │ ├── page.tsx
61
+ │ │ │ │ ├── loading.tsx
62
+ │ │ │ │ └── _components/
63
+ │ │ │ └── <feature>/
64
+ │ │ │ ├── page.tsx
65
+ │ │ │ ├── loading.tsx
66
+ │ │ │ ├── schema.ts
67
+ │ │ │ ├── _components/
68
+ │ │ │ └── _hooks/
69
+ │ │ │
70
+ │ │ └── (admin)/ # OPTIONAL: role-gated. middleware enforces role.
71
+ │ │ └── layout.tsx
72
+ │ │
73
+ │ ├── components/
74
+ │ │ ├── ui/ # shadcn primitives. OWNED by the project.
75
+ │ │ │ ├── button.tsx
76
+ │ │ │ ├── input.tsx
77
+ │ │ │ ├── label.tsx
78
+ │ │ │ ├── form.tsx
79
+ │ │ │ ├── dialog.tsx
80
+ │ │ │ ├── toast.tsx
81
+ │ │ │ ├── toaster.tsx
82
+ │ │ │ └── use-toast.ts
83
+ │ │ ├── forms/ # composed form fields built on ui/form.tsx
84
+ │ │ │ ├── FormInputField.tsx
85
+ │ │ │ ├── FormPasswordField.tsx
86
+ │ │ │ ├── FormSelectField.tsx
87
+ │ │ │ ├── FormDatePickerField.tsx
88
+ │ │ │ └── FormTextareaField.tsx
89
+ │ │ ├── Notifications.tsx # toast-driven notification surface
90
+ │ │ ├── UnsavedChangesWarning.tsx # in-app + beforeunload guard
91
+ │ │ └── AlertMessage.tsx
92
+ │ │
93
+ │ ├── redux/
94
+ │ │ ├── store.ts # typed; combined reducer; logout resets state.
95
+ │ │ ├── providers.tsx # 'use client'; <Provider> + <Toaster> + <ProgressBar>
96
+ │ │ ├── hooks.ts # typed useAppDispatch, useAppSelector
97
+ │ │ ├── api/
98
+ │ │ │ ├── api.ts # base createApi + auth-aware baseQuery
99
+ │ │ │ ├── tags.ts # tag-type union
100
+ │ │ │ ├── authApi.ts # injectEndpoints
101
+ │ │ │ └── <domain>Api.ts
102
+ │ │ └── features/
103
+ │ │ ├── authSlice.ts
104
+ │ │ └── <other>Slice.ts # only for shared async/global state
105
+ │ │
106
+ │ ├── lib/
107
+ │ │ ├── utils.ts # cn() — clsx + tailwind-merge
108
+ │ │ ├── zod-utils.ts # getDefaultValuesFromSchema, unwrapZodEffects, getMaxLengthsFromSchema
109
+ │ │ ├── formatters/
110
+ │ │ │ ├── date.ts # date-fns wrappers ONLY. No moment.
111
+ │ │ │ └── currency.ts
112
+ │ │ └── hooks/
113
+ │ │ └── useDebouncedValue.ts
114
+ │ │
115
+ │ ├── config/
116
+ │ │ └── env.ts # Zod-parsed runtime env. Fail fast at startup.
117
+ │ │
118
+ │ └── types/ # cross-cutting TS types only.
119
+ │ └── api.ts # shared API envelope types if any
120
+
121
+ └── tests/
122
+ ├── unit/
123
+ │ ├── auth.slice.test.ts
124
+ │ ├── login-form.test.tsx
125
+ │ └── zod-utils.test.ts
126
+ ├── e2e/
127
+ │ └── auth.spec.ts
128
+ └── setup.ts # @testing-library/jest-dom, vi globals
129
+ ```
130
+
131
+ ## Rules that govern the layout
132
+
133
+ 1. **Server-by-default in `app/`.** A file gets `'use client'` only if it itself uses a client API. A page can be a server component that renders a `'use client'` child from `_components/`.
134
+
135
+ 2. **`_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/`.
136
+
137
+ 3. **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(...)`. Multiple `createApi` instances are forbidden — they fragment the cache and complicate invalidation.
138
+
139
+ 4. **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. Only cross-cutting types belong in `src/types/`. Avoid a global `src/models/` dumping ground.
140
+
141
+ 5. **`middleware.ts` is the only auth gate at the network boundary.** No `src/proxy.ts` or `src/middleware/*.ts` standing in for it. Client guards exist only to handle session expiry during an active session.
142
+
143
+ 6. **`tests/` mirrors `src/` loosely, not strictly.** Tests are organized by what they test, not by 1:1 directory parity.
144
+
145
+ 7. **`public/` holds only static assets.** No code. Image components import from there via the standard `next/image` workflow.
146
+
147
+ 8. **No duplicate-by-case paths.** Linux CI will break what works on Windows/macOS. The linter cannot catch this; the scaffolder must.
148
+
149
+ 9. **Feature schemas are local.** `(app)/<feature>/schema.ts` exports Zod schemas owned by that feature. They are not re-exported from `src/lib/`.
150
+
151
+ 10. **`components/ui/` is owned, not imported.** shadcn-style primitives are generated *into* the project and modified as needed. They are not a node_modules dependency.
@@ -0,0 +1,212 @@
1
+ # Good Patterns to Keep
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.
4
+
5
+ ## 1. Route groups for auth and role boundaries
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.
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.
10
+
11
+ ## 2. Server-by-default with `'use client'` islands
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 toggles — are extracted into `_components/Something.tsx` files marked `'use client'` and imported from the server page.
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.
16
+
17
+ ## 3. Server `redirect()` and middleware-driven navigation
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).
20
+
21
+ ## 4. One RTK Query base, many injected slices
22
+
23
+ ```ts
24
+ // redux/api/api.ts
25
+ export const api = createApi({
26
+ reducerPath: 'api',
27
+ baseQuery: authAwareBaseQuery,
28
+ tagTypes: TAG_TYPES,
29
+ endpoints: () => ({}),
30
+ });
31
+
32
+ // redux/api/customersApi.ts
33
+ export const customersApi = api.injectEndpoints({
34
+ endpoints: (build) => ({
35
+ getCustomers: build.query<Customer[], void>({
36
+ query: () => '/customers',
37
+ providesTags: [{ type: 'Customers', id: 'LIST' }],
38
+ }),
39
+ createCustomer: build.mutation<Customer, NewCustomer>({
40
+ query: (body) => ({ url: '/customers', method: 'POST', body }),
41
+ invalidatesTags: [{ type: 'Customers', id: 'LIST' }],
42
+ }),
43
+ }),
44
+ });
45
+ ```
46
+
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
+ });
69
+ ```
70
+
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`.
72
+
73
+ ## 7. Typed Redux hooks
74
+
75
+ `redux/hooks.ts` exports `useAppDispatch` and `useAppSelector` typed against `RootState` and `AppDispatch`. Components never import the raw `useDispatch` / `useSelector` from `react-redux`.
76
+
77
+ ## 8. React Hook Form + Zod with schema-introspected defaults
78
+
79
+ ```ts
80
+ const schema = z.object({ name: z.string().min(1), age: z.number().min(0) });
81
+
82
+ const form = useForm<z.infer<typeof schema>>({
83
+ resolver: zodResolver(schema),
84
+ defaultValues: getDefaultValuesFromSchema(unwrapZodEffects(schema)),
85
+ });
86
+ ```
87
+
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>`
95
+
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.
97
+
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.
99
+
100
+ ## 10. `UnsavedChangesWarning` tied to RHF `formState.isDirty`
101
+
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.
103
+
104
+ ## 11. `error.tsx`, `loading.tsx`, `not-found.tsx` at meaningful segments
105
+
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.
107
+
108
+ ## 12. Runtime env validation
109
+
110
+ ```ts
111
+ // src/config/env.ts
112
+ import { z } from 'zod';
113
+
114
+ const envSchema = z.object({
115
+ NEXT_PUBLIC_API_BASE_URL: z.string().url(),
116
+ NEXT_PUBLIC_PUSHER_KEY: z.string().optional(),
117
+ });
118
+
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
+ });
123
+ ```
124
+
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.
126
+
127
+ ## 13. shadcn primitives owned by the project
128
+
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.
130
+
131
+ ## 14. `cn()` in `lib/utils.ts`
132
+
133
+ ```ts
134
+ import { clsx, type ClassValue } from 'clsx';
135
+ import { twMerge } from 'tailwind-merge';
136
+ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
137
+ ```
138
+
139
+ Single source of class merging across the app.
140
+
141
+ ## 15. Per-feature `_components/` and `_hooks/`
142
+
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.
144
+
145
+ ## 16. One date library: `date-fns`
146
+
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.
148
+
149
+ ## 17. Strict TypeScript
150
+
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.
152
+
153
+ ## 18. Strict ESLint
154
+
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.
156
+
157
+ ## 19. `middleware.ts` as the auth gate
158
+
159
+ ```ts
160
+ // 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
+ }
173
+
174
+ export const config = {
175
+ matcher: ['/((?!_next/static|_next/image|favicon.ico|api/public).*)'],
176
+ };
177
+ ```
178
+
179
+ (The matcher syntax here is illustrative — the scaffolder picks one that mirrors the project's actual route groups.)
180
+
181
+ ## 20. Vitest + RTL for unit, Playwright for E2E
182
+
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).
187
+
188
+ Three tests is enough to make CI meaningful.
189
+
190
+ ## 21. Husky + lint-staged
191
+
192
+ ```jsonc
193
+ // package.json
194
+ "lint-staged": {
195
+ "*.{ts,tsx}": ["prettier --write", "eslint --fix"],
196
+ "*.{json,md,css}": ["prettier --write"]
197
+ }
198
+ ```
199
+
200
+ Pre-commit hook keeps formatting and lint clean without anyone having to remember.
201
+
202
+ ## 22. GitHub Actions CI
203
+
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.
205
+
206
+ ## 23. `next-nprogress-bar` for top loading bar
207
+
208
+ One small, well-maintained library that hooks into Next.js's router events. Configured once in `providers.tsx`.
209
+
210
+ ## 24. Toaster via shadcn `use-toast` (or `react-toastify`, not both)
211
+
212
+ Pick one toast library and wrap it. Composed `Notifications.tsx` is what features call; the underlying library can change without touching feature code.
@@ -0,0 +1,62 @@
1
+ # Base RTK Query API and tag-types
2
+
3
+ ## `src/redux/api/tags.ts`
4
+
5
+ ```ts
6
+ // Single source of truth for cache tags. Every domain adds to this union.
7
+ export const TAG_TYPES = [
8
+ 'Auth',
9
+ // Add domain tags here as features are added, e.g. 'Customers', 'Customer', 'Invoices', 'Invoice'.
10
+ ] as const;
11
+
12
+ export type TagType = (typeof TAG_TYPES)[number];
13
+ ```
14
+
15
+ ## `src/redux/api/api.ts`
16
+
17
+ ```ts
18
+ import { createApi, fetchBaseQuery, type BaseQueryFn, type FetchArgs, type FetchBaseQueryError } from '@reduxjs/toolkit/query/react';
19
+ import { env } from '@/config/env';
20
+ import { TAG_TYPES } from './tags';
21
+ import { logout } from '@/redux/features/authSlice';
22
+
23
+ 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.
26
+ 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
+ });
35
+
36
+ export const baseQueryWithAuth: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (
37
+ args,
38
+ api,
39
+ extraOptions,
40
+ ) => {
41
+ const result = await rawBaseQuery(args, api, extraOptions);
42
+
43
+ if (result.error?.status === 401) {
44
+ // Clean logout: dispatch action → root reducer resets state → middleware/router handles redirect.
45
+ api.dispatch(logout());
46
+ }
47
+
48
+ return result;
49
+ };
50
+
51
+ export const api = createApi({
52
+ reducerPath: 'api',
53
+ baseQuery: baseQueryWithAuth,
54
+ tagTypes: TAG_TYPES,
55
+ endpoints: () => ({}),
56
+ });
57
+ ```
58
+
59
+ **Notes:**
60
+ - `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.
62
+ - `tagTypes` reads from the shared `TAG_TYPES` constant — domains add to that one array, not here.
@@ -0,0 +1,75 @@
1
+ # Domain API slice template (injected endpoints)
2
+
3
+ One file per domain. Imports the shared `api` and calls `api.injectEndpoints(...)`. Never calls `createApi`.
4
+
5
+ ## Example: `src/redux/api/customersApi.ts`
6
+
7
+ ```ts
8
+ import { api } from './api';
9
+
10
+ export interface Customer {
11
+ id: string;
12
+ name: string;
13
+ email: string;
14
+ createdAt: string;
15
+ }
16
+
17
+ export type NewCustomer = Omit<Customer, 'id' | 'createdAt'>;
18
+ export type UpdateCustomer = Partial<NewCustomer>;
19
+
20
+ export const customersApi = api.injectEndpoints({
21
+ endpoints: (build) => ({
22
+ getCustomers: build.query<Customer[], void>({
23
+ query: () => '/customers',
24
+ providesTags: (result) =>
25
+ result
26
+ ? [
27
+ ...result.map(({ id }) => ({ type: 'Customer' as const, id })),
28
+ { type: 'Customers' as const, id: 'LIST' },
29
+ ]
30
+ : [{ type: 'Customers' as const, id: 'LIST' }],
31
+ }),
32
+
33
+ getCustomer: build.query<Customer, string>({
34
+ query: (id) => `/customers/${id}`,
35
+ providesTags: (_result, _err, id) => [{ type: 'Customer', id }],
36
+ }),
37
+
38
+ createCustomer: build.mutation<Customer, NewCustomer>({
39
+ query: (body) => ({ url: '/customers', method: 'POST', body }),
40
+ invalidatesTags: [{ type: 'Customers', id: 'LIST' }],
41
+ }),
42
+
43
+ updateCustomer: build.mutation<Customer, { id: string; patch: UpdateCustomer }>({
44
+ query: ({ id, patch }) => ({ url: `/customers/${id}`, method: 'PATCH', body: patch }),
45
+ invalidatesTags: (_result, _err, { id }) => [
46
+ { type: 'Customer', id },
47
+ { type: 'Customers', id: 'LIST' },
48
+ ],
49
+ }),
50
+
51
+ deleteCustomer: build.mutation<void, string>({
52
+ query: (id) => ({ url: `/customers/${id}`, method: 'DELETE' }),
53
+ invalidatesTags: (_result, _err, id) => [
54
+ { type: 'Customer', id },
55
+ { type: 'Customers', id: 'LIST' },
56
+ ],
57
+ }),
58
+ }),
59
+ });
60
+
61
+ export const {
62
+ useGetCustomersQuery,
63
+ useGetCustomerQuery,
64
+ useCreateCustomerMutation,
65
+ useUpdateCustomerMutation,
66
+ useDeleteCustomerMutation,
67
+ } = customersApi;
68
+ ```
69
+
70
+ **Don't forget:** add `'Customer'` and `'Customers'` to `TAG_TYPES` in `src/redux/api/tags.ts`. TypeScript will error on the `providesTags` / `invalidatesTags` lines until you do.
71
+
72
+ **Forbidden:**
73
+ - `createApi(...)` in this file — only the base does that.
74
+ - `any` in the generic slots (`build.query<any, any>`). Type both sides.
75
+ - `transformResponse` that does data mangling that should be the backend's job. Use it only when the contract truly demands it (renaming a field that the backend can't change, unwrapping a `{ data: ... }` envelope, etc.).
@@ -0,0 +1,95 @@
1
+ # Auth slice and auth API template
2
+
3
+ ## `src/redux/features/authSlice.ts`
4
+
5
+ ```ts
6
+ import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
7
+ import { authApi } from '@/redux/api/authApi';
8
+
9
+ export interface AuthUser {
10
+ id: string;
11
+ email: string;
12
+ name: string;
13
+ roles: string[];
14
+ }
15
+
16
+ interface AuthState {
17
+ user: AuthUser | null;
18
+ token: string | null; // only populated for bearer-flow projects; null for httpOnly-cookie flows
19
+ expiresAt: number | null;
20
+ }
21
+
22
+ const initialState: AuthState = {
23
+ user: null,
24
+ token: null,
25
+ expiresAt: null,
26
+ };
27
+
28
+ export const authSlice = createSlice({
29
+ name: 'auth',
30
+ initialState,
31
+ reducers: {
32
+ logout: () => initialState,
33
+ setUser: (state, action: PayloadAction<AuthUser>) => {
34
+ state.user = action.payload;
35
+ },
36
+ },
37
+ extraReducers: (builder) => {
38
+ builder.addMatcher(authApi.endpoints.login.matchFulfilled, (state, { payload }) => {
39
+ state.user = payload.user;
40
+ state.token = payload.token ?? null;
41
+ state.expiresAt = payload.expiresAt ?? null;
42
+ });
43
+ builder.addMatcher(authApi.endpoints.getMe.matchFulfilled, (state, { payload }) => {
44
+ state.user = payload;
45
+ });
46
+ },
47
+ });
48
+
49
+ export const { logout, setUser } = authSlice.actions;
50
+ ```
51
+
52
+ **Note:** the store's `rootReducer` already wipes the entire store on `authApi.endpoints.logout.matchFulfilled` — so a `logout()` action dispatched here from the 401 handler still triggers a full state reset via the wrapped reducer in `store.ts`. The slice's own `logout` reducer is there for cases where the user explicitly clicks "Sign out" without hitting the logout endpoint.
53
+
54
+ ## `src/redux/api/authApi.ts`
55
+
56
+ ```ts
57
+ import { api } from './api';
58
+ import type { AuthUser } from '@/redux/features/authSlice';
59
+
60
+ export interface LoginRequest {
61
+ email: string;
62
+ password: string;
63
+ }
64
+
65
+ export interface LoginResponse {
66
+ user: AuthUser;
67
+ /** Populated only for bearer-token flows. httpOnly-cookie flows leave this undefined. */
68
+ token?: string;
69
+ expiresAt?: number;
70
+ }
71
+
72
+ export const authApi = api.injectEndpoints({
73
+ endpoints: (build) => ({
74
+ login: build.mutation<LoginResponse, LoginRequest>({
75
+ query: (body) => ({ url: '/auth/login', method: 'POST', body }),
76
+ invalidatesTags: ['Auth'],
77
+ }),
78
+ logout: build.mutation<void, void>({
79
+ query: () => ({ url: '/auth/logout', method: 'POST' }),
80
+ invalidatesTags: ['Auth'],
81
+ }),
82
+ getMe: build.query<AuthUser, void>({
83
+ query: () => '/auth/me',
84
+ providesTags: ['Auth'],
85
+ }),
86
+ }),
87
+ });
88
+
89
+ export const { useLoginMutation, useLogoutMutation, useGetMeQuery } = authApi;
90
+ ```
91
+
92
+ **Forbidden:**
93
+ - Storing the token via `js-cookie` inside the slice (`Cookies.set('token', ...)`). For `httpOnly` flows, the backend sets the cookie; for bearer flows, the token lives in Redux state and the `prepareHeaders` callback attaches it. Never both.
94
+ - Base64-encoding the user and writing it to a cookie (`Cookies.set('user', btoa(JSON.stringify(...)))`). It's not encryption, it's obfuscation. If the server needs the user, the server reads it from the session cookie / database.
95
+ - Decoding the JWT on the client to drive auth decisions. Decode only for non-sensitive display (e.g. showing the user's name pulled from a claim) — never as the gate. The gate is the server.