@malamute/ai-rules 1.0.0 → 1.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 +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/auth/**"
|
|
4
|
+
- "**/login/**"
|
|
5
|
+
- "**/api/auth/**"
|
|
6
|
+
- "middleware.ts"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Next.js Authentication
|
|
10
|
+
|
|
11
|
+
## NextAuth.js v5 Setup
|
|
12
|
+
|
|
13
|
+
### Configuration
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// auth.ts
|
|
17
|
+
import NextAuth from 'next-auth';
|
|
18
|
+
import Credentials from 'next-auth/providers/credentials';
|
|
19
|
+
import Google from 'next-auth/providers/google';
|
|
20
|
+
import { authConfig } from './auth.config';
|
|
21
|
+
|
|
22
|
+
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
23
|
+
...authConfig,
|
|
24
|
+
providers: [
|
|
25
|
+
Google({
|
|
26
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
27
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
28
|
+
}),
|
|
29
|
+
Credentials({
|
|
30
|
+
async authorize(credentials) {
|
|
31
|
+
const { email, password } = credentials;
|
|
32
|
+
const user = await verifyCredentials(email, password);
|
|
33
|
+
if (!user) return null;
|
|
34
|
+
return user;
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Auth Config (for Edge)
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// auth.config.ts
|
|
45
|
+
import type { NextAuthConfig } from 'next-auth';
|
|
46
|
+
|
|
47
|
+
export const authConfig = {
|
|
48
|
+
pages: {
|
|
49
|
+
signIn: '/login',
|
|
50
|
+
error: '/auth/error',
|
|
51
|
+
},
|
|
52
|
+
callbacks: {
|
|
53
|
+
authorized({ auth, request: { nextUrl } }) {
|
|
54
|
+
const isLoggedIn = !!auth?.user;
|
|
55
|
+
const isProtected = nextUrl.pathname.startsWith('/dashboard');
|
|
56
|
+
|
|
57
|
+
if (isProtected && !isLoggedIn) {
|
|
58
|
+
return Response.redirect(new URL('/login', nextUrl));
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
},
|
|
62
|
+
jwt({ token, user }) {
|
|
63
|
+
if (user) {
|
|
64
|
+
token.id = user.id;
|
|
65
|
+
token.role = user.role;
|
|
66
|
+
}
|
|
67
|
+
return token;
|
|
68
|
+
},
|
|
69
|
+
session({ session, token }) {
|
|
70
|
+
session.user.id = token.id as string;
|
|
71
|
+
session.user.role = token.role as string;
|
|
72
|
+
return session;
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
providers: [], // Configured in auth.ts
|
|
76
|
+
} satisfies NextAuthConfig;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Route Handlers
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// app/api/auth/[...nextauth]/route.ts
|
|
83
|
+
import { handlers } from '@/auth';
|
|
84
|
+
export const { GET, POST } = handlers;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Middleware Protection
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// middleware.ts
|
|
91
|
+
import { auth } from './auth';
|
|
92
|
+
|
|
93
|
+
export default auth((req) => {
|
|
94
|
+
const { nextUrl } = req;
|
|
95
|
+
const isLoggedIn = !!req.auth;
|
|
96
|
+
|
|
97
|
+
const publicPaths = ['/login', '/register', '/'];
|
|
98
|
+
const isPublic = publicPaths.includes(nextUrl.pathname);
|
|
99
|
+
|
|
100
|
+
if (!isLoggedIn && !isPublic) {
|
|
101
|
+
return Response.redirect(new URL('/login', nextUrl));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Role-based protection
|
|
105
|
+
if (nextUrl.pathname.startsWith('/admin')) {
|
|
106
|
+
if (req.auth?.user?.role !== 'admin') {
|
|
107
|
+
return Response.redirect(new URL('/forbidden', nextUrl));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const config = {
|
|
113
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Server Components
|
|
118
|
+
|
|
119
|
+
### Get Session
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// app/dashboard/page.tsx
|
|
123
|
+
import { auth } from '@/auth';
|
|
124
|
+
import { redirect } from 'next/navigation';
|
|
125
|
+
|
|
126
|
+
export default async function DashboardPage() {
|
|
127
|
+
const session = await auth();
|
|
128
|
+
|
|
129
|
+
if (!session) {
|
|
130
|
+
redirect('/login');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div>
|
|
135
|
+
<h1>Welcome, {session.user.name}</h1>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Protected Layout
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// app/(protected)/layout.tsx
|
|
145
|
+
import { auth } from '@/auth';
|
|
146
|
+
import { redirect } from 'next/navigation';
|
|
147
|
+
|
|
148
|
+
export default async function ProtectedLayout({
|
|
149
|
+
children,
|
|
150
|
+
}: {
|
|
151
|
+
children: React.ReactNode;
|
|
152
|
+
}) {
|
|
153
|
+
const session = await auth();
|
|
154
|
+
|
|
155
|
+
if (!session) {
|
|
156
|
+
redirect('/login');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return <>{children}</>;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Client Components
|
|
164
|
+
|
|
165
|
+
### Sign In/Out
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
'use client';
|
|
169
|
+
|
|
170
|
+
import { signIn, signOut } from 'next-auth/react';
|
|
171
|
+
|
|
172
|
+
export function LoginButton() {
|
|
173
|
+
return (
|
|
174
|
+
<button onClick={() => signIn('google', { callbackUrl: '/dashboard' })}>
|
|
175
|
+
Sign in with Google
|
|
176
|
+
</button>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function LogoutButton() {
|
|
181
|
+
return (
|
|
182
|
+
<button onClick={() => signOut({ callbackUrl: '/' })}>
|
|
183
|
+
Sign out
|
|
184
|
+
</button>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Use Session Hook
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
'use client';
|
|
193
|
+
|
|
194
|
+
import { useSession } from 'next-auth/react';
|
|
195
|
+
|
|
196
|
+
export function UserProfile() {
|
|
197
|
+
const { data: session, status } = useSession();
|
|
198
|
+
|
|
199
|
+
if (status === 'loading') {
|
|
200
|
+
return <Skeleton />;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!session) {
|
|
204
|
+
return <LoginButton />;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div>
|
|
209
|
+
<img src={session.user.image} alt={session.user.name} />
|
|
210
|
+
<span>{session.user.name}</span>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Server Actions
|
|
217
|
+
|
|
218
|
+
### Login Action
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// app/login/actions.ts
|
|
222
|
+
'use server';
|
|
223
|
+
|
|
224
|
+
import { signIn } from '@/auth';
|
|
225
|
+
import { AuthError } from 'next-auth';
|
|
226
|
+
|
|
227
|
+
export async function loginAction(
|
|
228
|
+
prevState: { error?: string } | undefined,
|
|
229
|
+
formData: FormData
|
|
230
|
+
) {
|
|
231
|
+
try {
|
|
232
|
+
await signIn('credentials', {
|
|
233
|
+
email: formData.get('email'),
|
|
234
|
+
password: formData.get('password'),
|
|
235
|
+
redirectTo: '/dashboard',
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error instanceof AuthError) {
|
|
239
|
+
switch (error.type) {
|
|
240
|
+
case 'CredentialsSignin':
|
|
241
|
+
return { error: 'Invalid credentials' };
|
|
242
|
+
default:
|
|
243
|
+
return { error: 'Something went wrong' };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Login Form
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
'use client';
|
|
255
|
+
|
|
256
|
+
import { useActionState } from 'react';
|
|
257
|
+
import { loginAction } from './actions';
|
|
258
|
+
|
|
259
|
+
export function LoginForm() {
|
|
260
|
+
const [state, action, isPending] = useActionState(loginAction, undefined);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<form action={action}>
|
|
264
|
+
<input name="email" type="email" required />
|
|
265
|
+
<input name="password" type="password" required />
|
|
266
|
+
|
|
267
|
+
{state?.error && <p className="error">{state.error}</p>}
|
|
268
|
+
|
|
269
|
+
<button type="submit" disabled={isPending}>
|
|
270
|
+
{isPending ? 'Signing in...' : 'Sign in'}
|
|
271
|
+
</button>
|
|
272
|
+
</form>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Type Extensions
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// types/next-auth.d.ts
|
|
281
|
+
import 'next-auth';
|
|
282
|
+
|
|
283
|
+
declare module 'next-auth' {
|
|
284
|
+
interface User {
|
|
285
|
+
role: string;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface Session {
|
|
289
|
+
user: User & {
|
|
290
|
+
id: string;
|
|
291
|
+
role: string;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
declare module 'next-auth/jwt' {
|
|
297
|
+
interface JWT {
|
|
298
|
+
id: string;
|
|
299
|
+
role: string;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Session Provider
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
// app/providers.tsx
|
|
308
|
+
'use client';
|
|
309
|
+
|
|
310
|
+
import { SessionProvider } from 'next-auth/react';
|
|
311
|
+
|
|
312
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
313
|
+
return <SessionProvider>{children}</SessionProvider>;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// app/layout.tsx
|
|
317
|
+
import { Providers } from './providers';
|
|
318
|
+
|
|
319
|
+
export default function RootLayout({ children }) {
|
|
320
|
+
return (
|
|
321
|
+
<html>
|
|
322
|
+
<body>
|
|
323
|
+
<Providers>{children}</Providers>
|
|
324
|
+
</body>
|
|
325
|
+
</html>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Anti-patterns
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// BAD: Checking auth in client component for protection
|
|
334
|
+
'use client';
|
|
335
|
+
export function ProtectedPage() {
|
|
336
|
+
const { data: session } = useSession();
|
|
337
|
+
if (!session) return <Redirect />; // Too late, page already loaded
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// GOOD: Check in middleware or server component
|
|
341
|
+
export default async function ProtectedPage() {
|
|
342
|
+
const session = await auth();
|
|
343
|
+
if (!session) redirect('/login');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// BAD: Exposing secrets
|
|
347
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
348
|
+
return { ...user, password: user.password }; // Leaking password!
|
|
349
|
+
|
|
350
|
+
// GOOD: Select only needed fields
|
|
351
|
+
const user = await db.user.findUnique({
|
|
352
|
+
where: { id },
|
|
353
|
+
select: { id: true, name: true, email: true },
|
|
354
|
+
});
|
|
355
|
+
```
|
|
@@ -209,3 +209,55 @@ export default function Page() {}
|
|
|
209
209
|
- Inline styles (use CSS modules or Tailwind)
|
|
210
210
|
- Anonymous components
|
|
211
211
|
- Props spreading without type safety
|
|
212
|
+
|
|
213
|
+
## Error and Loading UI
|
|
214
|
+
|
|
215
|
+
### Error Boundary (error.tsx)
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
'use client';
|
|
219
|
+
|
|
220
|
+
interface ErrorProps {
|
|
221
|
+
error: Error & { digest?: string };
|
|
222
|
+
reset: () => void;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default function Error({ error, reset }: ErrorProps) {
|
|
226
|
+
return (
|
|
227
|
+
<div className="error-container">
|
|
228
|
+
<h2>Something went wrong!</h2>
|
|
229
|
+
<p>{error.message}</p>
|
|
230
|
+
<button onClick={reset}>Try again</button>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Loading State (loading.tsx)
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
export default function Loading() {
|
|
240
|
+
return (
|
|
241
|
+
<div className="loading-container">
|
|
242
|
+
<Spinner />
|
|
243
|
+
<p>Loading...</p>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Not Found (not-found.tsx)
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
import Link from 'next/link';
|
|
253
|
+
|
|
254
|
+
export default function NotFound() {
|
|
255
|
+
return (
|
|
256
|
+
<div>
|
|
257
|
+
<h2>Not Found</h2>
|
|
258
|
+
<p>Could not find the requested resource.</p>
|
|
259
|
+
<Link href="/">Return Home</Link>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "app/**/*.tsx"
|
|
4
|
+
- "app/**/*.ts"
|
|
5
|
+
- "**/actions.ts"
|
|
6
|
+
- "**/actions/*.ts"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Data Fetching & Server Actions
|
|
10
|
+
|
|
11
|
+
## Server Components - Data Fetching
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// app/users/page.tsx
|
|
15
|
+
async function getUsers(): Promise<User[]> {
|
|
16
|
+
const response = await fetch('https://api.example.com/users', {
|
|
17
|
+
cache: 'no-store', // Dynamic - always fresh
|
|
18
|
+
// cache: 'force-cache', // Static (default)
|
|
19
|
+
// next: { revalidate: 60 }, // ISR - revalidate every 60s
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error('Failed to fetch users');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return response.json();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default async function UsersPage() {
|
|
30
|
+
const users = await getUsers();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<ul>
|
|
34
|
+
{users.map((user) => (
|
|
35
|
+
<li key={user.id}>{user.name}</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Cache Options
|
|
43
|
+
|
|
44
|
+
| Option | Behavior |
|
|
45
|
+
|--------|----------|
|
|
46
|
+
| `cache: 'force-cache'` | Static, cached indefinitely (default) |
|
|
47
|
+
| `cache: 'no-store'` | Dynamic, never cached |
|
|
48
|
+
| `next: { revalidate: N }` | ISR, revalidate every N seconds |
|
|
49
|
+
| `next: { tags: ['users'] }` | Tagged for on-demand revalidation |
|
|
50
|
+
|
|
51
|
+
## Server Actions
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// app/users/actions.ts
|
|
55
|
+
'use server';
|
|
56
|
+
|
|
57
|
+
import { revalidatePath, revalidateTag } from 'next/cache';
|
|
58
|
+
import { redirect } from 'next/navigation';
|
|
59
|
+
import { z } from 'zod';
|
|
60
|
+
|
|
61
|
+
const CreateUserSchema = z.object({
|
|
62
|
+
name: z.string().min(1),
|
|
63
|
+
email: z.string().email(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export async function createUser(formData: FormData) {
|
|
67
|
+
// Validate
|
|
68
|
+
const parsed = CreateUserSchema.safeParse({
|
|
69
|
+
name: formData.get('name'),
|
|
70
|
+
email: formData.get('email'),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!parsed.success) {
|
|
74
|
+
return { error: 'Invalid data' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create
|
|
78
|
+
await db.user.create({ data: parsed.data });
|
|
79
|
+
|
|
80
|
+
// Revalidate and redirect
|
|
81
|
+
revalidatePath('/users');
|
|
82
|
+
redirect('/users');
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Form with Server Action
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// app/users/_components/user-form.tsx
|
|
90
|
+
'use client';
|
|
91
|
+
|
|
92
|
+
import { useActionState } from 'react';
|
|
93
|
+
import { createUser } from '../actions';
|
|
94
|
+
|
|
95
|
+
const initialState = { error: null };
|
|
96
|
+
|
|
97
|
+
export function UserForm() {
|
|
98
|
+
const [state, formAction, isPending] = useActionState(
|
|
99
|
+
createUser,
|
|
100
|
+
initialState
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<form action={formAction}>
|
|
105
|
+
<input name="name" required disabled={isPending} />
|
|
106
|
+
<input name="email" type="email" required disabled={isPending} />
|
|
107
|
+
|
|
108
|
+
{state.error && <p className="error">{state.error}</p>}
|
|
109
|
+
|
|
110
|
+
<button type="submit" disabled={isPending}>
|
|
111
|
+
{isPending ? 'Creating...' : 'Create User'}
|
|
112
|
+
</button>
|
|
113
|
+
</form>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Optimistic Updates
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
'use client';
|
|
122
|
+
|
|
123
|
+
import { useOptimistic } from 'react';
|
|
124
|
+
import { addItem } from './actions';
|
|
125
|
+
|
|
126
|
+
export function ItemList({ items }: { items: Item[] }) {
|
|
127
|
+
const [optimisticItems, addOptimisticItem] = useOptimistic(
|
|
128
|
+
items,
|
|
129
|
+
(state, newItem: Item) => [...state, newItem]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
async function handleAdd(formData: FormData) {
|
|
133
|
+
const newItem = {
|
|
134
|
+
id: crypto.randomUUID(),
|
|
135
|
+
name: formData.get('name') as string,
|
|
136
|
+
pending: true,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
addOptimisticItem(newItem);
|
|
140
|
+
await addItem(formData);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
<form action={handleAdd}>
|
|
146
|
+
<input name="name" />
|
|
147
|
+
<button type="submit">Add</button>
|
|
148
|
+
</form>
|
|
149
|
+
|
|
150
|
+
<ul>
|
|
151
|
+
{optimisticItems.map((item) => (
|
|
152
|
+
<li key={item.id} style={{ opacity: item.pending ? 0.5 : 1 }}>
|
|
153
|
+
{item.name}
|
|
154
|
+
</li>
|
|
155
|
+
))}
|
|
156
|
+
</ul>
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Parallel Data Fetching
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// Parallel - faster
|
|
166
|
+
export default async function Page() {
|
|
167
|
+
const [users, products] = await Promise.all([
|
|
168
|
+
getUsers(),
|
|
169
|
+
getProducts(),
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
return (/* ... */);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Sequential - slower, avoid
|
|
176
|
+
export default async function Page() {
|
|
177
|
+
const users = await getUsers();
|
|
178
|
+
const products = await getProducts(); // Waits for users
|
|
179
|
+
return (/* ... */);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Data Fetching with Suspense
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { Suspense } from 'react';
|
|
187
|
+
|
|
188
|
+
export default function Page() {
|
|
189
|
+
return (
|
|
190
|
+
<div>
|
|
191
|
+
<h1>Dashboard</h1>
|
|
192
|
+
|
|
193
|
+
<Suspense fallback={<UsersSkeleton />}>
|
|
194
|
+
<UsersSection />
|
|
195
|
+
</Suspense>
|
|
196
|
+
|
|
197
|
+
<Suspense fallback={<ProductsSkeleton />}>
|
|
198
|
+
<ProductsSection />
|
|
199
|
+
</Suspense>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function UsersSection() {
|
|
205
|
+
const users = await getUsers();
|
|
206
|
+
return <UserList users={users} />;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Revalidation Patterns
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
'use server';
|
|
214
|
+
|
|
215
|
+
// Path-based
|
|
216
|
+
revalidatePath('/users'); // Revalidate specific path
|
|
217
|
+
revalidatePath('/users', 'layout'); // Revalidate layout and children
|
|
218
|
+
|
|
219
|
+
// Tag-based
|
|
220
|
+
revalidateTag('users'); // Revalidate all with this tag
|
|
221
|
+
|
|
222
|
+
// Usage with tags
|
|
223
|
+
await fetch('/api/users', {
|
|
224
|
+
next: { tags: ['users'] }
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Error Handling in Server Actions
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
'use server';
|
|
232
|
+
|
|
233
|
+
export async function createUser(formData: FormData) {
|
|
234
|
+
try {
|
|
235
|
+
const user = await db.user.create({
|
|
236
|
+
data: { name: formData.get('name') as string }
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
revalidatePath('/users');
|
|
240
|
+
return { success: true, user };
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// Log for debugging
|
|
243
|
+
console.error('Failed to create user:', error);
|
|
244
|
+
|
|
245
|
+
// Return user-friendly error
|
|
246
|
+
return { success: false, error: 'Failed to create user' };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|