@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.
Files changed (23) hide show
  1. package/README.md +23 -13
  2. package/package.json +1 -1
  3. package/skills/nextjs-app-router/SKILL.md +228 -175
  4. package/skills/nextjs-app-router/references/anti-patterns.md +163 -99
  5. package/skills/nextjs-app-router/references/folder-layout.md +79 -46
  6. package/skills/nextjs-app-router/references/good-patterns.md +233 -99
  7. package/skills/nextjs-app-router/references/templates/api-base.md +24 -21
  8. package/skills/nextjs-app-router/references/templates/auth-slice.md +82 -78
  9. package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +58 -6
  10. package/skills/nextjs-app-router/references/templates/db-client.md +48 -0
  11. package/skills/nextjs-app-router/references/templates/env-and-utils.md +90 -23
  12. package/skills/nextjs-app-router/references/templates/feature-slice.md +110 -47
  13. package/skills/nextjs-app-router/references/templates/form-with-zod.md +23 -15
  14. package/skills/nextjs-app-router/references/templates/middleware.md +69 -59
  15. package/skills/nextjs-app-router/references/templates/next-config.md +4 -14
  16. package/skills/nextjs-app-router/references/templates/nextauth-config.md +178 -0
  17. package/skills/nextjs-app-router/references/templates/package.md +35 -5
  18. package/skills/nextjs-app-router/references/templates/prisma-schema.md +162 -0
  19. package/skills/nextjs-app-router/references/templates/providers-and-store.md +35 -21
  20. package/skills/nextjs-app-router/references/templates/root-layout.md +99 -20
  21. package/skills/nextjs-app-router/references/templates/route-group-layouts.md +46 -6
  22. package/skills/nextjs-app-router/references/templates/route-handler.md +168 -0
  23. package/skills/nextjs-app-router/references/templates/testing.md +105 -31
@@ -1,71 +1,66 @@
1
1
  # Feature slice template (Mode 2 — `add-feature`)
2
2
 
3
- A feature is a route folder under a route group, with its own private `_components/` and `_hooks/`, a local Zod `schema.ts`, and (usually) one or more RTK Query endpoints injected into the appropriate domain slice.
3
+ A feature is a route folder under a route group, with its own private `_components/` and `_hooks/`, a local Zod `schema.ts` (shared with the matching Route Handler), and one or more RTK Query endpoints injected into the appropriate domain slice. **Every `page.tsx` is `'use client'`**.
4
4
 
5
- ## Layout for feature `{{feature}}` under `(app)`
5
+ ## Layout for feature `{{feature}}` under `(app)` (list + create + edit)
6
6
 
7
7
  ```
8
- src/app/(app)/{{feature}}/
9
- ├── page.tsx # SERVER component (renders client child if interactive)
10
- ├── loading.tsx
11
- ├── error.tsx # optional but recommended
12
- ├── schema.ts # Zod schemas owned by this feature
13
- ├── _components/
14
- │ ├── {{Feature}}Table.tsx # 'use client' if it has interactions
15
- │ └── {{Feature}}Form.tsx # 'use client'
16
- └── _hooks/
17
- └── use{{Feature}}Filters.ts
18
- ```
19
-
20
- For a feature with sub-routes (list + detail + new):
21
-
22
- ```
23
- src/app/(app)/{{feature}}/
24
- ├── page.tsx # list
25
- ├── new/
26
- │ └── page.tsx # SERVER renders <{{Feature}}Form mode="create" />
8
+ src/app/(app)/{{feature}}s/
9
+ ├── page.tsx # 'use client' list view, uses useGet{{Feature}}sQuery
10
+ ├── new/page.tsx # 'use client' — renders <{{Feature}}Form mode="create" />
27
11
  ├── [id]/
28
- │ ├── page.tsx # SERVER renders <{{Feature}}Form mode="edit" id={id} />
12
+ │ ├── page.tsx # 'use client' — renders <{{Feature}}Form mode="edit" id={params.id} />
29
13
  │ ├── loading.tsx
30
- │ └── _components/{{Feature}}DetailPanel.tsx
31
- ├── schema.ts
14
+ │ └── _components/
15
+ ├── loading.tsx
16
+ ├── error.tsx
17
+ ├── schema.ts # Zod — SHARED with src/app/api/{{feature}}/route.ts
32
18
  ├── _components/
33
- │ ├── {{Feature}}Table.tsx
19
+ │ ├── {{Feature}}sTable.tsx
34
20
  │ └── {{Feature}}Form.tsx
35
21
  └── _hooks/
22
+ └── use{{Feature}}Filters.ts
23
+
24
+ src/app/api/{{feature}}s/
25
+ ├── route.ts # GET (list) + POST (create)
26
+ └── [id]/route.ts # GET + PATCH + DELETE
36
27
  ```
37
28
 
38
- ## `page.tsx` for the list view (SERVER)
29
+ ## `page.tsx` for the list view (CLIENT)
39
30
 
40
31
  ```tsx
41
- import { Suspense } from 'react';
42
- import { {{Feature}}Table } from './_components/{{Feature}}Table';
32
+ 'use client';
43
33
 
44
- export const metadata = { title: '{{Feature}}s' };
34
+ import Link from 'next/link';
35
+ import { Button } from '@/components/ui/button';
36
+ import { {{Feature}}sTable } from './_components/{{Feature}}sTable';
45
37
 
46
38
  export default function {{Feature}}sPage() {
47
39
  return (
48
40
  <section className="space-y-4">
49
41
  <header className="flex items-center justify-between">
50
42
  <h1 className="text-2xl font-semibold">{{Feature}}s</h1>
43
+ <Button asChild>
44
+ <Link href="/app/{{feature}}s/new">New {{feature}}</Link>
45
+ </Button>
51
46
  </header>
52
- <Suspense fallback={<div>Loading…</div>}>
53
- <{{Feature}}Table />
54
- </Suspense>
47
+ <{{Feature}}sTable />
55
48
  </section>
56
49
  );
57
50
  }
58
51
  ```
59
52
 
60
- ## `_components/{{Feature}}Table.tsx` (CLIENT)
53
+ Per-route metadata for client pages lives at the layout level (server). If this feature needs a specific `<title>`, lift it into a `(app)/{{feature}}s/layout.tsx` server component with a `metadata` export.
54
+
55
+ ## `_components/{{Feature}}sTable.tsx` (CLIENT)
61
56
 
62
57
  ```tsx
63
58
  'use client';
64
59
 
65
- import { use{{Feature}}sQuery } from '@/redux/api/{{feature}}sApi';
60
+ import { useGet{{Feature}}sQuery } from '@/redux/api/{{feature}}sApi';
66
61
 
67
62
  export function {{Feature}}sTable() {
68
- const { data, isLoading, error } = use{{Feature}}sQuery();
63
+ const { data, isLoading, error } = useGet{{Feature}}sQuery();
69
64
 
70
65
  if (isLoading) return <p className="text-muted-foreground">Loading…</p>;
71
66
  if (error) return <p className="text-destructive">Failed to load.</p>;
@@ -92,7 +87,7 @@ export function {{Feature}}sTable() {
92
87
  }
93
88
  ```
94
89
 
95
- ## `schema.ts`
90
+ ## `schema.ts` (SHARED with Route Handler)
96
91
 
97
92
  ```ts
98
93
  import { z } from 'zod';
@@ -105,6 +100,44 @@ export const {{feature}}Schema = z.object({
105
100
  export type {{Feature}}FormValues = z.infer<typeof {{feature}}Schema>;
106
101
  ```
107
102
 
103
+ The matching `src/app/api/{{feature}}s/route.ts` imports `{{feature}}Schema` and calls `{{feature}}Schema.safeParse(await req.json())`. One schema, two sides.
104
+
105
+ ## `new/page.tsx` (CLIENT)
106
+
107
+ ```tsx
108
+ 'use client';
109
+
110
+ import { {{Feature}}Form } from '../_components/{{Feature}}Form';
111
+
112
+ export default function New{{Feature}}Page() {
113
+ return (
114
+ <section className="space-y-4">
115
+ <h1 className="text-2xl font-semibold">New {{feature}}</h1>
116
+ <{{Feature}}Form mode="create" />
117
+ </section>
118
+ );
119
+ }
120
+ ```
121
+
122
+ ## `[id]/page.tsx` (CLIENT)
123
+
124
+ ```tsx
125
+ 'use client';
126
+
127
+ import { useParams } from 'next/navigation';
128
+ import { {{Feature}}Form } from '../_components/{{Feature}}Form';
129
+
130
+ export default function Edit{{Feature}}Page() {
131
+ const params = useParams<{ id: string }>();
132
+ return (
133
+ <section className="space-y-4">
134
+ <h1 className="text-2xl font-semibold">Edit {{feature}}</h1>
135
+ <{{Feature}}Form mode="edit" id={params.id} />
136
+ </section>
137
+ );
138
+ }
139
+ ```
140
+
108
141
  ## `_components/{{Feature}}Form.tsx` (CLIENT)
109
142
 
110
143
  ```tsx
@@ -118,25 +151,47 @@ import { Button } from '@/components/ui/button';
118
151
  import { FormInputField } from '@/components/forms/FormInputField';
119
152
  import { UnsavedChangesWarning } from '@/components/UnsavedChangesWarning';
120
153
  import { useToast } from '@/components/ui/use-toast';
121
- import { useCreate{{Feature}}Mutation } from '@/redux/api/{{feature}}sApi';
154
+ import {
155
+ useCreate{{Feature}}Mutation,
156
+ useUpdate{{Feature}}Mutation,
157
+ useGet{{Feature}}Query,
158
+ } from '@/redux/api/{{feature}}sApi';
122
159
  import { getDefaultValuesFromSchema, unwrapZodEffects } from '@/lib/zod-utils';
123
160
  import { {{feature}}Schema, type {{Feature}}FormValues } from '../schema';
124
161
 
125
- export function {{Feature}}Form() {
162
+ interface Props {
163
+ mode: 'create' | 'edit';
164
+ id?: string;
165
+ }
166
+
167
+ export function {{Feature}}Form({ mode, id }: Props) {
126
168
  const router = useRouter();
127
169
  const { toast } = useToast();
128
- const [create, { isLoading }] = useCreate{{Feature}}Mutation();
170
+
171
+ const { data: existing } = useGet{{Feature}}Query(id ?? '', { skip: mode !== 'edit' || !id });
172
+ const [create, { isLoading: isCreating }] = useCreate{{Feature}}Mutation();
173
+ const [update, { isLoading: isUpdating }] = useUpdate{{Feature}}Mutation();
129
174
 
130
175
  const form = useForm<{{Feature}}FormValues>({
131
176
  resolver: zodResolver({{feature}}Schema),
132
- defaultValues: getDefaultValuesFromSchema(unwrapZodEffects({{feature}}Schema)) as {{Feature}}FormValues,
177
+ defaultValues:
178
+ existing ??
179
+ (getDefaultValuesFromSchema(unwrapZodEffects({{feature}}Schema)) as {{Feature}}FormValues),
180
+ values: existing,
133
181
  });
134
182
 
183
+ const isSubmitting = isCreating || isUpdating;
184
+
135
185
  const onSubmit = async (values: {{Feature}}FormValues) => {
136
186
  try {
137
- const created = await create(values).unwrap();
138
- toast({ title: '{{Feature}} created.' });
139
- router.replace(`/app/{{feature}}s/${created.id}`);
187
+ if (mode === 'create') {
188
+ const created = await create(values).unwrap();
189
+ toast({ title: '{{Feature}} created.' });
190
+ router.replace(`/app/{{feature}}s/${created.id}`);
191
+ } else if (id) {
192
+ await update({ id, patch: values }).unwrap();
193
+ toast({ title: '{{Feature}} updated.' });
194
+ }
140
195
  } catch {
141
196
  toast({ title: 'Could not save', variant: 'destructive' });
142
197
  }
@@ -149,8 +204,12 @@ export function {{Feature}}Form() {
149
204
  <FormInputField form={form} schema={{feature}}Schema} fieldName="name" label="Name" />
150
205
  <FormInputField form={form} schema={{feature}}Schema} fieldName="email" label="Email" type="email" />
151
206
  <div className="flex gap-2">
152
- <Button type="submit" disabled={isLoading}>{isLoading ? 'Saving…' : 'Save'}</Button>
153
- <Button type="button" variant="ghost" onClick={() => router.back()}>Cancel</Button>
207
+ <Button type="submit" disabled={isSubmitting}>
208
+ {isSubmitting ? 'Saving…' : 'Save'}
209
+ </Button>
210
+ <Button type="button" variant="ghost" onClick={() => router.back()}>
211
+ Cancel
212
+ </Button>
154
213
  </div>
155
214
  </form>
156
215
  </Form>
@@ -179,6 +238,10 @@ describe('{{feature}}Schema', () => {
179
238
  });
180
239
  ```
181
240
 
182
- ## Endpoint additions
241
+ ## Route Handlers
242
+
243
+ Always generate the matching `src/app/api/{{feature}}s/route.ts` and `[id]/route.ts` alongside the client-side feature. See [`route-handler.md`](route-handler.md).
244
+
245
+ ## Endpoint additions to the RTK slice
183
246
 
184
- If the domain slice doesn't exist yet, run Mode 3 first (`add-api-slice`). Then add the feature's query/mutation pair to that slice — see [`api-slice.md`](api-slice.md).
247
+ If the domain slice doesn't exist yet, run Mode 3 (`add-api-slice`). Then add the feature's query/mutation pair — see [`api-slice.md`](api-slice.md).
@@ -122,10 +122,14 @@ export function FormInputField<TForm extends FieldValues>({
122
122
 
123
123
  ## A real form — `src/app/(public)/auth/login/_components/LoginForm.tsx`
124
124
 
125
+ The login form uses NextAuth's `signIn` directly — there is no `useLoginMutation` because auth is not a domain in the RTK Query slice.
126
+
125
127
  ```tsx
126
128
  'use client';
127
129
 
130
+ import { useState } from 'react';
128
131
  import { useRouter, useSearchParams } from 'next/navigation';
132
+ import { signIn } from 'next-auth/react';
129
133
  import { useForm } from 'react-hook-form';
130
134
  import { zodResolver } from '@hookform/resolvers/zod';
131
135
  import { z } from 'zod';
@@ -133,22 +137,20 @@ import { z } from 'zod';
133
137
  import { Form } from '@/components/ui/form';
134
138
  import { Button } from '@/components/ui/button';
135
139
  import { FormInputField } from '@/components/forms/FormInputField';
136
- import { useLoginMutation } from '@/redux/api/authApi';
137
- import { useToast } from '@/components/ui/use-toast';
140
+ import { FormPasswordField } from '@/components/forms/FormPasswordField';
138
141
  import { getDefaultValuesFromSchema, unwrapZodEffects } from '@/lib/zod-utils';
139
142
 
140
143
  const loginSchema = z.object({
141
144
  email: z.string().email(),
142
- password: z.string().min(8).max(128),
145
+ password: z.string().min(1, 'Required'),
143
146
  });
144
147
  type LoginValues = z.infer<typeof loginSchema>;
145
148
 
146
149
  export function LoginForm() {
147
150
  const router = useRouter();
148
151
  const searchParams = useSearchParams();
149
- const returnTo = searchParams.get('returnTo') ?? '/dashboard';
150
- const { toast } = useToast();
151
- const [login, { isLoading }] = useLoginMutation();
152
+ const returnTo = searchParams.get('returnTo') ?? '/app/dashboard';
153
+ const [error, setError] = useState<string | null>(null);
152
154
 
153
155
  const form = useForm<LoginValues>({
154
156
  resolver: zodResolver(loginSchema),
@@ -156,21 +158,25 @@ export function LoginForm() {
156
158
  });
157
159
 
158
160
  const onSubmit = async (values: LoginValues) => {
159
- try {
160
- await login(values).unwrap();
161
- router.replace(returnTo);
162
- } catch (err) {
163
- toast({ title: 'Sign in failed', description: String((err as { data?: { message?: string } }).data?.message ?? 'Try again.'), variant: 'destructive' });
161
+ setError(null);
162
+ const res = await signIn('credentials', { ...values, redirect: false });
163
+ if (res?.error) {
164
+ // Don't reveal which field was wrong — security best practice.
165
+ setError('Invalid email or password.');
166
+ return;
164
167
  }
168
+ router.replace(returnTo);
169
+ router.refresh();
165
170
  };
166
171
 
167
172
  return (
168
173
  <Form {...form}>
169
174
  <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
170
- <FormInputField form={form} schema={loginSchema} fieldName="email" label="Email" type="email" />
171
- <FormInputField form={form} schema={loginSchema} fieldName="password" label="Password" type="password" />
172
- <Button type="submit" disabled={isLoading} className="w-full">
173
- {isLoading ? 'Signing in…' : 'Sign in'}
175
+ {error && <p className="text-sm text-destructive">{error}</p>}
176
+ <FormInputField form={form} schema={loginSchema} fieldName="email" label="Email" type="email" autoComplete="email" />
177
+ <FormPasswordField form={form} schema={loginSchema} fieldName="password" label="Password" autoComplete="current-password" />
178
+ <Button type="submit" disabled={form.formState.isSubmitting} className="w-full">
179
+ {form.formState.isSubmitting ? 'Signing in…' : 'Sign in'}
174
180
  </Button>
175
181
  </form>
176
182
  </Form>
@@ -178,6 +184,8 @@ export function LoginForm() {
178
184
  }
179
185
  ```
180
186
 
187
+ **Why no `useLoginMutation`:** auth flows go through NextAuth (`signIn` / `signOut`), not through RTK Query. RTK Query is for domain data. Keeping these separate avoids the temptation to re-implement session state inside Redux.
188
+
181
189
  ## `UnsavedChangesWarning` integration
182
190
 
183
191
  For non-trivial forms, wrap the submit area with the guard:
@@ -1,88 +1,98 @@
1
- # `middleware.ts` template
1
+ # `middleware.ts` template (NextAuth-driven)
2
2
 
3
3
  This is **the** auth gate. Place at the project root (not in `src/`). Next.js requires this exact filename and location.
4
4
 
5
- ## Variant A — `httpOnly` cookie set by the backend on login
5
+ ## Variant A — Plain NextAuth `auth` (covers most projects)
6
6
 
7
7
  ```ts
8
8
  // middleware.ts
9
- import { NextResponse, type NextRequest } from 'next/server';
9
+ import NextAuth from 'next-auth';
10
+ import authConfig from '@/auth.config';
10
11
 
11
- const PROTECTED_PREFIXES = ['/app', '/admin'];
12
- const AUTH_PATH = '/auth/login';
12
+ export const { auth: middleware } = NextAuth(authConfig);
13
13
 
14
- export function middleware(req: NextRequest) {
15
- const { pathname, search } = req.nextUrl;
16
- const session = req.cookies.get('session')?.value;
17
- const isProtected = PROTECTED_PREFIXES.some((p) => pathname === p || pathname.startsWith(`${p}/`));
14
+ // Exclude NextAuth's own API routes and static assets.
15
+ export const config = {
16
+ matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)'],
17
+ };
18
+ ```
18
19
 
19
- // Already authenticated user hitting an auth page bounce to /app.
20
- if (session && pathname.startsWith('/auth')) {
21
- const url = req.nextUrl.clone();
22
- url.pathname = '/app/dashboard';
23
- url.search = '';
24
- return NextResponse.redirect(url);
25
- }
20
+ NextAuth's `auth` returns 302s to `pages.signIn` for unauthenticated requests to anything matched by the `authorized` callback in `auth.config.ts`. If `authorized` isn't defined, all matched routes require a session.
26
21
 
27
- // Unauthenticated user hitting a protected route → redirect with returnTo.
28
- if (!session && isProtected) {
29
- const url = req.nextUrl.clone();
30
- url.pathname = AUTH_PATH;
31
- url.search = '';
32
- url.searchParams.set('returnTo', pathname + search);
33
- return NextResponse.redirect(url);
34
- }
22
+ The `authorized` callback in `auth.config.ts`:
35
23
 
36
- return NextResponse.next();
24
+ ```ts
25
+ // inside src/auth.config.ts
26
+ callbacks: {
27
+ authorized({ auth, request: { nextUrl } }) {
28
+ const isLoggedIn = !!auth?.user;
29
+ const isOnPublic = nextUrl.pathname.startsWith('/auth');
30
+ if (isOnPublic) {
31
+ return isLoggedIn
32
+ ? Response.redirect(new URL('/app/dashboard', nextUrl))
33
+ : true;
34
+ }
35
+ return isLoggedIn; // false → redirected to pages.signIn
36
+ },
37
37
  }
38
-
39
- export const config = {
40
- // Run on everything except Next internals and static assets.
41
- matcher: ['/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)'],
42
- };
43
38
  ```
44
39
 
45
- ## Variant B — JS-readable cookie (decode the JWT here)
40
+ ## Variant B — Custom wrapper for role/path logic
46
41
 
47
- If the backend cannot set an `httpOnly` cookie and the project must use a JS-readable token, decode the JWT in middleware to check expiry — never trust the client to have done so:
42
+ When `(admin)` exists or `/` needs role-based routing:
48
43
 
49
44
  ```ts
50
- import { NextResponse, type NextRequest } from 'next/server';
51
- import { jwtVerify } from 'jose'; // Edge-compatible; do not use jwt-decode here.
45
+ // middleware.ts
46
+ import NextAuth from 'next-auth';
47
+ import { NextResponse } from 'next/server';
48
+ import authConfig from '@/auth.config';
52
49
 
53
- const PROTECTED_PREFIXES = ['/app', '/admin'];
54
- const SECRET = new TextEncoder().encode(process.env.JWT_VERIFY_KEY!);
50
+ const { auth } = NextAuth(authConfig);
55
51
 
56
- export async function middleware(req: NextRequest) {
57
- const token = req.cookies.get('token')?.value;
58
- const { pathname, search } = req.nextUrl;
59
- const isProtected = PROTECTED_PREFIXES.some((p) => pathname === p || pathname.startsWith(`${p}/`));
52
+ export default auth((req) => {
53
+ const { pathname } = req.nextUrl;
54
+ const session = req.auth;
60
55
 
61
- if (!isProtected) return NextResponse.next();
62
- if (!token) return redirectToLogin(req);
56
+ // Root redirect
57
+ if (pathname === '/') {
58
+ const url = req.nextUrl.clone();
59
+ url.pathname = session ? '/app/dashboard' : '/auth/login';
60
+ return NextResponse.redirect(url);
61
+ }
63
62
 
64
- try {
65
- await jwtVerify(token, SECRET);
66
- return NextResponse.next();
67
- } catch {
68
- return redirectToLogin(req);
63
+ // Admin gate
64
+ if (pathname.startsWith('/admin')) {
65
+ if (!session) {
66
+ const url = req.nextUrl.clone();
67
+ url.pathname = '/auth/login';
68
+ url.searchParams.set('returnTo', pathname);
69
+ return NextResponse.redirect(url);
70
+ }
71
+ if (session.user?.role !== 'admin') {
72
+ return new NextResponse('Forbidden', { status: 403 });
73
+ }
69
74
  }
70
- }
71
75
 
72
- function redirectToLogin(req: NextRequest) {
73
- const url = req.nextUrl.clone();
74
- url.pathname = '/auth/login';
75
- url.searchParams.set('returnTo', req.nextUrl.pathname + req.nextUrl.search);
76
- return NextResponse.redirect(url);
77
- }
76
+ // App gate
77
+ if (pathname.startsWith('/app') && !session) {
78
+ const url = req.nextUrl.clone();
79
+ url.pathname = '/auth/login';
80
+ url.searchParams.set('returnTo', pathname);
81
+ return NextResponse.redirect(url);
82
+ }
83
+
84
+ return NextResponse.next();
85
+ });
78
86
 
79
87
  export const config = {
80
- matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
88
+ matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)'],
81
89
  };
82
90
  ```
83
91
 
84
92
  **Notes:**
85
- - `jose` is Edge-runtime compatible. `jsonwebtoken` and `jwt-decode` are not — they assume Node.
86
- - The matcher must exclude `_next/static`, `_next/image`, and `favicon.ico`. Add other public assets as needed.
87
- - Do **not** add a `src/proxy.ts` or `src/middleware.ts` Next.js only looks at `middleware.ts` at the project root.
88
- - Role-gating for `/admin` happens here too: decode the role claim and 403 (or redirect) when the user is not allowed.
93
+
94
+ - `auth.config.ts` must be **Edge-safe**: no Prisma adapter, no `@/lib/db`, no Node-only imports. The middleware runs on the Edge runtime; importing Node-only code crashes the production build with a confusing "module not found in edge runtime" error.
95
+ - The matcher excludes `api/auth` so NextAuth's own `/api/auth/[...nextauth]` route is reachable without recursion.
96
+ - Other `/api/**` routes (your Route Handlers) are excluded *from middleware redirects* because the matcher targets pages handlers run their own `requireSession()` check and return JSON 401s rather than 302s.
97
+ - Role claims: add them in the `jwt` callback inside `auth.config.ts` (or `auth.ts` if the role comes from the DB). Module-augment `Session` and `JWT` in `src/types/next-auth.d.ts`.
98
+ - Do **not** add a `src/proxy.ts` or `src/middleware/*.ts` — Next.js only looks at `middleware.ts` at the project root.
@@ -3,23 +3,12 @@
3
3
  ```ts
4
4
  import type { NextConfig } from 'next';
5
5
 
6
- const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL;
7
-
8
6
  const nextConfig: NextConfig = {
9
7
  reactStrictMode: true,
10
8
  poweredByHeader: false,
11
9
 
12
- // Forward /api/* to the backend in production; in dev, hit the backend directly via NEXT_PUBLIC_API_BASE_URL.
13
- // Configure only if the user opted into the rewrite pattern in Step 2.
14
- async rewrites() {
15
- if (!apiBase) return [];
16
- return [
17
- {
18
- source: '/api/:path*',
19
- destination: `${apiBase}/:path*`,
20
- },
21
- ];
22
- },
10
+ // The backend is in-app under src/app/api/**/route.ts. No rewrites are needed.
11
+ // (Do not add a rewrite forwarding /api/* to an external host this project is fullstack.)
23
12
 
24
13
  images: {
25
14
  remotePatterns: [
@@ -36,7 +25,8 @@ export default nextConfig;
36
25
  ```
37
26
 
38
27
  **Notes:**
28
+ - The app is fullstack: `/api/*` routes are Route Handlers in `src/app/api/**/route.ts`, not proxies to an external backend. **Do not** add a `rewrites()` entry forwarding `/api/*` to another host — that breaks NextAuth (which expects `/api/auth/*` to land on the in-app handlers) and removes the same-origin guarantee RTK Query relies on for `credentials: 'include'`.
39
29
  - Do not enable `output: 'standalone'` by default; only when the user is deploying to a container.
40
- - Do not enable `output: 'export'` (static export) unless the user explicitly asks — it disables middleware, server components data fetching, and image optimization.
30
+ - Do not enable `output: 'export'` (static export) — it disables middleware, Route Handlers, and image optimization. The skill's whole architecture depends on those.
41
31
  - `reactStrictMode: true` is non-negotiable for a new project.
42
32
  - `poweredByHeader: false` removes the `X-Powered-By` header.