@malamute/ai-rules 1.0.0 → 1.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 (145) hide show
  1. package/README.md +272 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/CLAUDE.md +52 -149
  4. package/configs/_shared/rules/conventions/documentation.md +324 -0
  5. package/configs/_shared/rules/conventions/git.md +265 -0
  6. package/configs/_shared/rules/conventions/npm.md +80 -0
  7. package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
  8. package/configs/_shared/rules/conventions/principles.md +334 -0
  9. package/configs/_shared/rules/devops/ci-cd.md +262 -0
  10. package/configs/_shared/rules/devops/docker.md +275 -0
  11. package/configs/_shared/rules/devops/nx.md +194 -0
  12. package/configs/_shared/rules/domain/backend/api-design.md +203 -0
  13. package/configs/_shared/rules/lang/csharp/async.md +220 -0
  14. package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
  15. package/configs/_shared/rules/lang/csharp/linq.md +210 -0
  16. package/configs/_shared/rules/lang/python/async.md +337 -0
  17. package/configs/_shared/rules/lang/python/celery.md +476 -0
  18. package/configs/_shared/rules/lang/python/config.md +339 -0
  19. package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
  20. package/configs/_shared/rules/lang/python/deployment.md +523 -0
  21. package/configs/_shared/rules/lang/python/error-handling.md +330 -0
  22. package/configs/_shared/rules/lang/python/migrations.md +421 -0
  23. package/configs/_shared/rules/lang/python/python.md +172 -0
  24. package/configs/_shared/rules/lang/python/repository.md +383 -0
  25. package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
  26. package/configs/_shared/rules/lang/typescript/async.md +447 -0
  27. package/configs/_shared/rules/lang/typescript/generics.md +356 -0
  28. package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
  29. package/configs/_shared/rules/quality/error-handling.md +48 -0
  30. package/configs/_shared/rules/quality/logging.md +45 -0
  31. package/configs/_shared/rules/quality/observability.md +240 -0
  32. package/configs/_shared/rules/quality/testing-patterns.md +65 -0
  33. package/configs/_shared/rules/security/secrets-management.md +222 -0
  34. package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
  35. package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
  36. package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
  37. package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  38. package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  39. package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  40. package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
  41. package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
  42. package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
  43. package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
  44. package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
  45. package/configs/angular/CLAUDE.md +24 -216
  46. package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
  47. package/configs/angular/rules/core/resource.md +285 -0
  48. package/configs/angular/rules/core/signals.md +323 -0
  49. package/configs/angular/rules/http.md +338 -0
  50. package/configs/angular/rules/routing.md +291 -0
  51. package/configs/angular/rules/ssr.md +312 -0
  52. package/configs/angular/rules/state/signal-store.md +408 -0
  53. package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
  54. package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
  55. package/configs/angular/rules/ui/aria.md +422 -0
  56. package/configs/angular/rules/ui/forms.md +424 -0
  57. package/configs/angular/rules/ui/pipes-directives.md +335 -0
  58. package/configs/angular/{.claude/settings.json → settings.json} +3 -0
  59. package/configs/dotnet/CLAUDE.md +53 -286
  60. package/configs/dotnet/rules/background-services.md +552 -0
  61. package/configs/dotnet/rules/configuration.md +426 -0
  62. package/configs/dotnet/rules/ddd.md +447 -0
  63. package/configs/dotnet/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/rules/mediatr.md +320 -0
  65. package/configs/dotnet/rules/middleware.md +489 -0
  66. package/configs/dotnet/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/rules/validation.md +388 -0
  68. package/configs/dotnet/settings.json +29 -0
  69. package/configs/fastapi/CLAUDE.md +144 -0
  70. package/configs/fastapi/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/rules/dependencies.md +170 -0
  72. package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
  73. package/configs/fastapi/rules/lifespan.md +274 -0
  74. package/configs/fastapi/rules/middleware.md +229 -0
  75. package/configs/fastapi/rules/pydantic.md +433 -0
  76. package/configs/fastapi/rules/responses.md +251 -0
  77. package/configs/fastapi/rules/routers.md +202 -0
  78. package/configs/fastapi/rules/security.md +222 -0
  79. package/configs/fastapi/rules/testing.md +251 -0
  80. package/configs/fastapi/rules/websockets.md +298 -0
  81. package/configs/fastapi/settings.json +35 -0
  82. package/configs/flask/CLAUDE.md +166 -0
  83. package/configs/flask/rules/blueprints.md +208 -0
  84. package/configs/flask/rules/cli.md +285 -0
  85. package/configs/flask/rules/configuration.md +281 -0
  86. package/configs/flask/rules/context.md +238 -0
  87. package/configs/flask/rules/error-handlers.md +278 -0
  88. package/configs/flask/rules/extensions.md +278 -0
  89. package/configs/flask/rules/flask.md +171 -0
  90. package/configs/flask/rules/marshmallow.md +206 -0
  91. package/configs/flask/rules/security.md +267 -0
  92. package/configs/flask/rules/testing.md +284 -0
  93. package/configs/flask/settings.json +35 -0
  94. package/configs/nestjs/CLAUDE.md +57 -215
  95. package/configs/nestjs/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/rules/filters.md +376 -0
  97. package/configs/nestjs/rules/interceptors.md +317 -0
  98. package/configs/nestjs/rules/middleware.md +321 -0
  99. package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
  100. package/configs/nestjs/rules/pipes.md +351 -0
  101. package/configs/nestjs/rules/websockets.md +451 -0
  102. package/configs/nestjs/settings.json +31 -0
  103. package/configs/nextjs/CLAUDE.md +69 -331
  104. package/configs/nextjs/rules/api-routes.md +358 -0
  105. package/configs/nextjs/rules/authentication.md +355 -0
  106. package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
  107. package/configs/nextjs/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/rules/database.md +400 -0
  109. package/configs/nextjs/rules/middleware.md +303 -0
  110. package/configs/nextjs/rules/routing.md +324 -0
  111. package/configs/nextjs/rules/seo.md +350 -0
  112. package/configs/nextjs/rules/server-actions.md +353 -0
  113. package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
  114. package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
  115. package/package.json +24 -9
  116. package/src/cli.js +218 -0
  117. package/src/config.js +63 -0
  118. package/src/index.js +4 -0
  119. package/src/installer.js +414 -0
  120. package/src/merge.js +109 -0
  121. package/src/tech-config.json +45 -0
  122. package/src/utils.js +88 -0
  123. package/configs/dotnet/.claude/settings.json +0 -9
  124. package/configs/nestjs/.claude/settings.json +0 -15
  125. package/configs/python/.claude/rules/flask.md +0 -332
  126. package/configs/python/.claude/settings.json +0 -18
  127. package/configs/python/CLAUDE.md +0 -273
  128. package/src/install.js +0 -315
  129. /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
  130. /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
  131. /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
  132. /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
  133. /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
  134. /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
  135. /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
  136. /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
  137. /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
  138. /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
  139. /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
  140. /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
  141. /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
  142. /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
  143. /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
  144. /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
  145. /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
@@ -0,0 +1,350 @@
1
+ ---
2
+ paths:
3
+ - "**/layout.tsx"
4
+ - "**/page.tsx"
5
+ - "app/sitemap.ts"
6
+ - "app/robots.ts"
7
+ ---
8
+
9
+ # Next.js SEO
10
+
11
+ ## Metadata API
12
+
13
+ ### Static Metadata
14
+
15
+ ```typescript
16
+ // app/page.tsx
17
+ import type { Metadata } from 'next';
18
+
19
+ export const metadata: Metadata = {
20
+ title: 'Home | My App',
21
+ description: 'Welcome to my application',
22
+ keywords: ['next.js', 'react', 'web development'],
23
+ authors: [{ name: 'John Doe' }],
24
+ openGraph: {
25
+ title: 'Home | My App',
26
+ description: 'Welcome to my application',
27
+ url: 'https://example.com',
28
+ siteName: 'My App',
29
+ images: [
30
+ {
31
+ url: 'https://example.com/og-image.png',
32
+ width: 1200,
33
+ height: 630,
34
+ alt: 'My App',
35
+ },
36
+ ],
37
+ locale: 'en_US',
38
+ type: 'website',
39
+ },
40
+ twitter: {
41
+ card: 'summary_large_image',
42
+ title: 'Home | My App',
43
+ description: 'Welcome to my application',
44
+ images: ['https://example.com/og-image.png'],
45
+ },
46
+ robots: {
47
+ index: true,
48
+ follow: true,
49
+ },
50
+ };
51
+ ```
52
+
53
+ ### Dynamic Metadata
54
+
55
+ ```typescript
56
+ // app/blog/[slug]/page.tsx
57
+ import type { Metadata } from 'next';
58
+
59
+ type Props = {
60
+ params: Promise<{ slug: string }>;
61
+ };
62
+
63
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
64
+ const { slug } = await params;
65
+ const post = await getPost(slug);
66
+
67
+ return {
68
+ title: `${post.title} | Blog`,
69
+ description: post.excerpt,
70
+ openGraph: {
71
+ title: post.title,
72
+ description: post.excerpt,
73
+ type: 'article',
74
+ publishedTime: post.publishedAt,
75
+ authors: [post.author.name],
76
+ images: [
77
+ {
78
+ url: post.featuredImage,
79
+ width: 1200,
80
+ height: 630,
81
+ alt: post.title,
82
+ },
83
+ ],
84
+ },
85
+ };
86
+ }
87
+ ```
88
+
89
+ ### Layout Metadata (Template)
90
+
91
+ ```typescript
92
+ // app/layout.tsx
93
+ import type { Metadata } from 'next';
94
+
95
+ export const metadata: Metadata = {
96
+ metadataBase: new URL('https://example.com'),
97
+ title: {
98
+ template: '%s | My App',
99
+ default: 'My App',
100
+ },
101
+ description: 'My application description',
102
+ };
103
+
104
+ // Child page: title: 'Blog' → renders as 'Blog | My App'
105
+ ```
106
+
107
+ ## Sitemap
108
+
109
+ ```typescript
110
+ // app/sitemap.ts
111
+ import type { MetadataRoute } from 'next';
112
+
113
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
114
+ const baseUrl = 'https://example.com';
115
+
116
+ // Static pages
117
+ const staticPages = [
118
+ { url: baseUrl, lastModified: new Date(), changeFrequency: 'daily' as const, priority: 1 },
119
+ { url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.8 },
120
+ { url: `${baseUrl}/contact`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.5 },
121
+ ];
122
+
123
+ // Dynamic pages
124
+ const posts = await db.post.findMany({
125
+ select: { slug: true, updatedAt: true },
126
+ });
127
+
128
+ const postPages = posts.map(post => ({
129
+ url: `${baseUrl}/blog/${post.slug}`,
130
+ lastModified: post.updatedAt,
131
+ changeFrequency: 'weekly' as const,
132
+ priority: 0.7,
133
+ }));
134
+
135
+ return [...staticPages, ...postPages];
136
+ }
137
+ ```
138
+
139
+ ### Large Sitemap (Multiple Files)
140
+
141
+ ```typescript
142
+ // app/sitemap/[id]/route.ts
143
+ import { getPostsBatch } from '@/lib/posts';
144
+
145
+ export async function GET(
146
+ request: Request,
147
+ { params }: { params: Promise<{ id: string }> }
148
+ ) {
149
+ const { id } = await params;
150
+ const posts = await getPostsBatch(parseInt(id));
151
+
152
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
153
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
154
+ ${posts.map(post => `
155
+ <url>
156
+ <loc>https://example.com/blog/${post.slug}</loc>
157
+ <lastmod>${post.updatedAt.toISOString()}</lastmod>
158
+ </url>
159
+ `).join('')}
160
+ </urlset>`;
161
+
162
+ return new Response(sitemap, {
163
+ headers: { 'Content-Type': 'application/xml' },
164
+ });
165
+ }
166
+ ```
167
+
168
+ ## Robots.txt
169
+
170
+ ```typescript
171
+ // app/robots.ts
172
+ import type { MetadataRoute } from 'next';
173
+
174
+ export default function robots(): MetadataRoute.Robots {
175
+ return {
176
+ rules: [
177
+ {
178
+ userAgent: '*',
179
+ allow: '/',
180
+ disallow: ['/admin/', '/api/', '/private/'],
181
+ },
182
+ {
183
+ userAgent: 'Googlebot',
184
+ allow: '/',
185
+ },
186
+ ],
187
+ sitemap: 'https://example.com/sitemap.xml',
188
+ };
189
+ }
190
+ ```
191
+
192
+ ## JSON-LD Structured Data
193
+
194
+ ```typescript
195
+ // components/structured-data.tsx
196
+ export function ArticleJsonLd({
197
+ title,
198
+ description,
199
+ publishedTime,
200
+ author,
201
+ image,
202
+ url,
203
+ }: ArticleJsonLdProps) {
204
+ const jsonLd = {
205
+ '@context': 'https://schema.org',
206
+ '@type': 'Article',
207
+ headline: title,
208
+ description,
209
+ image,
210
+ datePublished: publishedTime,
211
+ author: {
212
+ '@type': 'Person',
213
+ name: author,
214
+ },
215
+ publisher: {
216
+ '@type': 'Organization',
217
+ name: 'My App',
218
+ logo: {
219
+ '@type': 'ImageObject',
220
+ url: 'https://example.com/logo.png',
221
+ },
222
+ },
223
+ mainEntityOfPage: url,
224
+ };
225
+
226
+ return (
227
+ <script
228
+ type="application/ld+json"
229
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
230
+ />
231
+ );
232
+ }
233
+
234
+ // Usage in page
235
+ export default function BlogPost({ post }) {
236
+ return (
237
+ <>
238
+ <ArticleJsonLd
239
+ title={post.title}
240
+ description={post.excerpt}
241
+ publishedTime={post.publishedAt}
242
+ author={post.author.name}
243
+ image={post.featuredImage}
244
+ url={`https://example.com/blog/${post.slug}`}
245
+ />
246
+ <article>...</article>
247
+ </>
248
+ );
249
+ }
250
+ ```
251
+
252
+ ### Organization Schema
253
+
254
+ ```typescript
255
+ export function OrganizationJsonLd() {
256
+ const jsonLd = {
257
+ '@context': 'https://schema.org',
258
+ '@type': 'Organization',
259
+ name: 'My Company',
260
+ url: 'https://example.com',
261
+ logo: 'https://example.com/logo.png',
262
+ sameAs: [
263
+ 'https://twitter.com/mycompany',
264
+ 'https://linkedin.com/company/mycompany',
265
+ ],
266
+ contactPoint: {
267
+ '@type': 'ContactPoint',
268
+ telephone: '+1-800-555-1234',
269
+ contactType: 'customer service',
270
+ },
271
+ };
272
+
273
+ return (
274
+ <script
275
+ type="application/ld+json"
276
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
277
+ />
278
+ );
279
+ }
280
+ ```
281
+
282
+ ## Canonical URLs
283
+
284
+ ```typescript
285
+ export const metadata: Metadata = {
286
+ alternates: {
287
+ canonical: 'https://example.com/page',
288
+ languages: {
289
+ 'en-US': 'https://example.com/en-US/page',
290
+ 'fr-FR': 'https://example.com/fr-FR/page',
291
+ },
292
+ },
293
+ };
294
+ ```
295
+
296
+ ## Performance (Core Web Vitals)
297
+
298
+ ```typescript
299
+ // next.config.js
300
+ module.exports = {
301
+ images: {
302
+ formats: ['image/avif', 'image/webp'],
303
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
304
+ },
305
+ experimental: {
306
+ optimizeCss: true,
307
+ },
308
+ };
309
+
310
+ // Image component
311
+ import Image from 'next/image';
312
+
313
+ <Image
314
+ src="/hero.jpg"
315
+ alt="Hero image"
316
+ width={1200}
317
+ height={630}
318
+ priority // For LCP images
319
+ placeholder="blur"
320
+ blurDataURL="data:image/jpeg;base64,..."
321
+ />
322
+ ```
323
+
324
+ ## Anti-patterns
325
+
326
+ ```typescript
327
+ // BAD: Missing metadata
328
+ export default function Page() {
329
+ return <div>Content</div>;
330
+ }
331
+
332
+ // GOOD: Always include metadata
333
+ export const metadata: Metadata = {
334
+ title: 'Page Title',
335
+ description: 'Page description',
336
+ };
337
+
338
+ // BAD: Duplicate content without canonical
339
+ // page-1 and page-2 have same content
340
+
341
+ // GOOD: Set canonical
342
+ export const metadata: Metadata = {
343
+ alternates: { canonical: '/page-1' },
344
+ };
345
+
346
+ // BAD: Blocking indexing of important pages
347
+ export const metadata: Metadata = {
348
+ robots: { index: false }, // Accidentally blocking!
349
+ };
350
+ ```
@@ -0,0 +1,353 @@
1
+ ---
2
+ paths:
3
+ - "apps/**/app/**/*.tsx"
4
+ - "apps/**/app/**/*.ts"
5
+ - "app/**/*.tsx"
6
+ - "app/**/*.ts"
7
+ - "**/actions.ts"
8
+ - "**/actions/*.ts"
9
+ ---
10
+
11
+ # Next.js Server Actions
12
+
13
+ ## Basic Server Action
14
+
15
+ ```typescript
16
+ // app/actions.ts
17
+ 'use server';
18
+
19
+ import { revalidatePath } from 'next/cache';
20
+
21
+ export async function createPost(formData: FormData) {
22
+ const title = formData.get('title') as string;
23
+ const content = formData.get('content') as string;
24
+
25
+ await db.post.create({ data: { title, content } });
26
+
27
+ revalidatePath('/posts');
28
+ }
29
+ ```
30
+
31
+ ## With Validation (Zod)
32
+
33
+ ```typescript
34
+ // app/actions.ts
35
+ 'use server';
36
+
37
+ import { z } from 'zod';
38
+ import { revalidatePath } from 'next/cache';
39
+
40
+ const CreatePostSchema = z.object({
41
+ title: z.string().min(1).max(100),
42
+ content: z.string().min(1),
43
+ });
44
+
45
+ export async function createPost(formData: FormData) {
46
+ const parsed = CreatePostSchema.safeParse({
47
+ title: formData.get('title'),
48
+ content: formData.get('content'),
49
+ });
50
+
51
+ if (!parsed.success) {
52
+ return { error: parsed.error.flatten().fieldErrors };
53
+ }
54
+
55
+ await db.post.create({ data: parsed.data });
56
+ revalidatePath('/posts');
57
+
58
+ return { success: true };
59
+ }
60
+ ```
61
+
62
+ ## Return Type Pattern
63
+
64
+ ```typescript
65
+ // Type-safe action results
66
+ type ActionResult<T> =
67
+ | { success: true; data: T }
68
+ | { success: false; error: string; fieldErrors?: Record<string, string[]> };
69
+
70
+ export async function createUser(formData: FormData): Promise<ActionResult<User>> {
71
+ const parsed = UserSchema.safeParse(Object.fromEntries(formData));
72
+
73
+ if (!parsed.success) {
74
+ return {
75
+ success: false,
76
+ error: 'Validation failed',
77
+ fieldErrors: parsed.error.flatten().fieldErrors,
78
+ };
79
+ }
80
+
81
+ try {
82
+ const user = await db.user.create({ data: parsed.data });
83
+ revalidatePath('/users');
84
+ return { success: true, data: user };
85
+ } catch (error) {
86
+ return { success: false, error: 'Failed to create user' };
87
+ }
88
+ }
89
+ ```
90
+
91
+ ## useActionState (React 19)
92
+
93
+ ```tsx
94
+ 'use client';
95
+
96
+ import { useActionState } from 'react';
97
+ import { createPost } from './actions';
98
+
99
+ export function PostForm() {
100
+ const [state, formAction, isPending] = useActionState(createPost, null);
101
+
102
+ return (
103
+ <form action={formAction}>
104
+ <input name="title" disabled={isPending} />
105
+ {state?.fieldErrors?.title && (
106
+ <span className="error">{state.fieldErrors.title}</span>
107
+ )}
108
+
109
+ <textarea name="content" disabled={isPending} />
110
+ {state?.fieldErrors?.content && (
111
+ <span className="error">{state.fieldErrors.content}</span>
112
+ )}
113
+
114
+ <button type="submit" disabled={isPending}>
115
+ {isPending ? 'Creating...' : 'Create Post'}
116
+ </button>
117
+
118
+ {state?.error && <div className="error">{state.error}</div>}
119
+ </form>
120
+ );
121
+ }
122
+ ```
123
+
124
+ ## useOptimistic
125
+
126
+ ```tsx
127
+ 'use client';
128
+
129
+ import { useOptimistic } from 'react';
130
+ import { toggleLike } from './actions';
131
+
132
+ interface Post {
133
+ id: string;
134
+ likes: number;
135
+ isLiked: boolean;
136
+ }
137
+
138
+ export function LikeButton({ post }: { post: Post }) {
139
+ const [optimisticPost, addOptimistic] = useOptimistic(
140
+ post,
141
+ (state, newLiked: boolean) => ({
142
+ ...state,
143
+ isLiked: newLiked,
144
+ likes: newLiked ? state.likes + 1 : state.likes - 1,
145
+ })
146
+ );
147
+
148
+ async function handleClick() {
149
+ addOptimistic(!optimisticPost.isLiked);
150
+ await toggleLike(post.id);
151
+ }
152
+
153
+ return (
154
+ <button onClick={handleClick}>
155
+ {optimisticPost.isLiked ? '❤️' : '🤍'} {optimisticPost.likes}
156
+ </button>
157
+ );
158
+ }
159
+ ```
160
+
161
+ ## Revalidation Strategies
162
+
163
+ ```typescript
164
+ 'use server';
165
+
166
+ import { revalidatePath, revalidateTag } from 'next/cache';
167
+
168
+ export async function createPost(data: PostData) {
169
+ await db.post.create({ data });
170
+
171
+ // Revalidate specific path
172
+ revalidatePath('/posts');
173
+
174
+ // Revalidate dynamic path
175
+ revalidatePath(`/posts/${data.slug}`);
176
+
177
+ // Revalidate layout (all child pages)
178
+ revalidatePath('/posts', 'layout');
179
+
180
+ // Revalidate by cache tag
181
+ revalidateTag('posts');
182
+ }
183
+
184
+ // In data fetching, use tags
185
+ async function getPosts() {
186
+ return fetch('/api/posts', {
187
+ next: { tags: ['posts'] },
188
+ });
189
+ }
190
+ ```
191
+
192
+ ## Redirect After Action
193
+
194
+ ```typescript
195
+ 'use server';
196
+
197
+ import { redirect } from 'next/navigation';
198
+
199
+ export async function createPost(formData: FormData) {
200
+ const post = await db.post.create({
201
+ data: {
202
+ title: formData.get('title') as string,
203
+ content: formData.get('content') as string,
204
+ },
205
+ });
206
+
207
+ redirect(`/posts/${post.slug}`);
208
+ }
209
+ ```
210
+
211
+ ## With Authentication
212
+
213
+ ```typescript
214
+ 'use server';
215
+
216
+ import { auth } from '@/lib/auth';
217
+ import { redirect } from 'next/navigation';
218
+
219
+ export async function createPost(formData: FormData) {
220
+ const session = await auth();
221
+
222
+ if (!session?.user) {
223
+ redirect('/login');
224
+ }
225
+
226
+ await db.post.create({
227
+ data: {
228
+ title: formData.get('title') as string,
229
+ content: formData.get('content') as string,
230
+ authorId: session.user.id,
231
+ },
232
+ });
233
+
234
+ revalidatePath('/posts');
235
+ }
236
+ ```
237
+
238
+ ## File Upload
239
+
240
+ ```typescript
241
+ 'use server';
242
+
243
+ export async function uploadFile(formData: FormData) {
244
+ const file = formData.get('file') as File;
245
+
246
+ if (!file || file.size === 0) {
247
+ return { error: 'No file provided' };
248
+ }
249
+
250
+ // Validate file type
251
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
252
+ if (!allowedTypes.includes(file.type)) {
253
+ return { error: 'Invalid file type' };
254
+ }
255
+
256
+ // Validate file size (5MB)
257
+ if (file.size > 5 * 1024 * 1024) {
258
+ return { error: 'File too large' };
259
+ }
260
+
261
+ const bytes = await file.arrayBuffer();
262
+ const buffer = Buffer.from(bytes);
263
+
264
+ // Save to storage (S3, local, etc.)
265
+ const url = await uploadToStorage(buffer, file.name);
266
+
267
+ return { success: true, url };
268
+ }
269
+ ```
270
+
271
+ ## Progressive Enhancement
272
+
273
+ ```tsx
274
+ // Works without JavaScript (form submits normally)
275
+ // Enhanced with JavaScript (no page reload)
276
+
277
+ export function ContactForm() {
278
+ return (
279
+ <form action={sendMessage}>
280
+ <input name="email" type="email" required />
281
+ <textarea name="message" required />
282
+ <button type="submit">Send</button>
283
+ </form>
284
+ );
285
+ }
286
+ ```
287
+
288
+ ## Error Handling
289
+
290
+ ```typescript
291
+ 'use server';
292
+
293
+ export async function riskyAction(formData: FormData) {
294
+ try {
295
+ await someRiskyOperation();
296
+ return { success: true };
297
+ } catch (error) {
298
+ // Log server-side
299
+ console.error('Action failed:', error);
300
+
301
+ // Return safe error to client
302
+ if (error instanceof KnownError) {
303
+ return { success: false, error: error.message };
304
+ }
305
+
306
+ return { success: false, error: 'An unexpected error occurred' };
307
+ }
308
+ }
309
+ ```
310
+
311
+ ## Binding Arguments
312
+
313
+ ```tsx
314
+ // Pass additional data to action
315
+ import { updatePost } from './actions';
316
+
317
+ export function EditButton({ postId }: { postId: string }) {
318
+ const updateWithId = updatePost.bind(null, postId);
319
+
320
+ return (
321
+ <form action={updateWithId}>
322
+ <input name="title" />
323
+ <button type="submit">Update</button>
324
+ </form>
325
+ );
326
+ }
327
+
328
+ // actions.ts
329
+ 'use server';
330
+
331
+ export async function updatePost(postId: string, formData: FormData) {
332
+ const title = formData.get('title') as string;
333
+ await db.post.update({ where: { id: postId }, data: { title } });
334
+ }
335
+ ```
336
+
337
+ ## Non-Form Usage
338
+
339
+ ```tsx
340
+ 'use client';
341
+
342
+ import { deletePost } from './actions';
343
+
344
+ export function DeleteButton({ postId }: { postId: string }) {
345
+ async function handleDelete() {
346
+ if (confirm('Are you sure?')) {
347
+ await deletePost(postId);
348
+ }
349
+ }
350
+
351
+ return <button onClick={handleDelete}>Delete</button>;
352
+ }
353
+ ```