@devmunna/agent-skillkit 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.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/bin/ai-skills.js +5 -0
- package/dist/cli/commands/add.d.ts +2 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/add.js +66 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +33 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +145 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +55 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +49 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +2 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +22 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/prompts/agent-selector.d.ts +3 -0
- package/dist/cli/prompts/agent-selector.d.ts.map +1 -0
- package/dist/cli/prompts/agent-selector.js +23 -0
- package/dist/cli/prompts/agent-selector.js.map +1 -0
- package/dist/cli/prompts/stack-selector.d.ts +3 -0
- package/dist/cli/prompts/stack-selector.d.ts.map +1 -0
- package/dist/cli/prompts/stack-selector.js +60 -0
- package/dist/cli/prompts/stack-selector.js.map +1 -0
- package/dist/core/config-manager.d.ts +20 -0
- package/dist/core/config-manager.d.ts.map +1 -0
- package/dist/core/config-manager.js +107 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/detector.d.ts +3 -0
- package/dist/core/detector.d.ts.map +1 -0
- package/dist/core/detector.js +50 -0
- package/dist/core/detector.js.map +1 -0
- package/dist/core/doctor.d.ts +12 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +102 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/skill-registry.d.ts +11 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +174 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/skill-resolver.d.ts +3 -0
- package/dist/core/skill-resolver.d.ts.map +1 -0
- package/dist/core/skill-resolver.js +36 -0
- package/dist/core/skill-resolver.js.map +1 -0
- package/dist/core/validator.d.ts +13 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +99 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/generators/agent-installer.d.ts +5 -0
- package/dist/generators/agent-installer.d.ts.map +1 -0
- package/dist/generators/agent-installer.js +20 -0
- package/dist/generators/agent-installer.js.map +1 -0
- package/dist/generators/agents-md.d.ts +3 -0
- package/dist/generators/agents-md.d.ts.map +1 -0
- package/dist/generators/agents-md.js +70 -0
- package/dist/generators/agents-md.js.map +1 -0
- package/dist/generators/claude-md.d.ts +3 -0
- package/dist/generators/claude-md.d.ts.map +1 -0
- package/dist/generators/claude-md.js +47 -0
- package/dist/generators/claude-md.js.map +1 -0
- package/dist/generators/skill-generator.d.ts +5 -0
- package/dist/generators/skill-generator.d.ts.map +1 -0
- package/dist/generators/skill-generator.js +34 -0
- package/dist/generators/skill-generator.js.map +1 -0
- package/dist/generators/workflows.d.ts +3 -0
- package/dist/generators/workflows.d.ts.map +1 -0
- package/dist/generators/workflows.js +57 -0
- package/dist/generators/workflows.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/file-utils.d.ts +12 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +39 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +73 -0
- package/skills/clean-architecture/SKILL.md +324 -0
- package/skills/express-mvc-prisma/SKILL.md +168 -0
- package/skills/express-mvc-prisma/references/auth.md +190 -0
- package/skills/express-mvc-prisma/references/boilerplate.md +196 -0
- package/skills/express-mvc-prisma/references/error-handling.md +121 -0
- package/skills/express-mvc-prisma/references/module-scaffold.md +253 -0
- package/skills/express-mvc-prisma/references/prisma-setup.md +97 -0
- package/skills/express-mvc-prisma/references/response-helpers.md +157 -0
- package/skills/express-mvc-prisma/references/zod-validation.md +157 -0
- package/skills/fastify-rest/SKILL.md +287 -0
- package/skills/mongoose-odm/SKILL.md +281 -0
- package/skills/nextjs-fullstack/SKILL.md +328 -0
- package/skills/nextjs-fullstack/references/auth.md +270 -0
- package/skills/nextjs-fullstack/references/caching.md +157 -0
- package/skills/nextjs-fullstack/references/route-handlers.md +194 -0
- package/skills/nextjs-fullstack/references/server-actions.md +214 -0
- package/skills/nextjs-fullstack/references/server-components.md +190 -0
- package/skills/node-base/SKILL.md +139 -0
- package/skills/prisma-orm/SKILL.md +334 -0
- package/skills/react-feature-arch/SKILL.md +208 -0
- package/skills/react-feature-arch/references/api-layer.md +110 -0
- package/skills/react-feature-arch/references/components.md +192 -0
- package/skills/react-feature-arch/references/data-fetching.md +198 -0
- package/skills/react-feature-arch/references/forms.md +194 -0
- package/skills/react-feature-arch/references/routing.md +148 -0
- package/skills/react-feature-arch/references/state-management.md +107 -0
- package/skills/tailwind-css/SKILL.md +236 -0
- package/skills/tailwind-css/references/components.md +340 -0
- package/skills/tailwind-css/references/design-tokens.md +230 -0
- package/skills/tailwind-css/references/patterns.md +375 -0
- package/skills/tailwind-css/references/setup.md +165 -0
- package/skills/zod-validation/SKILL.md +267 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Caching Reference — Next.js 15
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Caching Layers (top to bottom)
|
|
6
|
+
|
|
7
|
+
| Layer | Scope | How to control |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `use cache` | Function / module level | `cacheTag()`, `cacheLife()` |
|
|
10
|
+
| `fetch()` cache | Individual fetch calls | `cache`, `next.revalidate`, `next.tags` |
|
|
11
|
+
| Page-level | Full page / layout | `export const revalidate`, `export const dynamic` |
|
|
12
|
+
| On-demand | After mutations | `revalidateTag()`, `revalidatePath()` |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## `use cache` Directive (Next.js 15) — Preferred
|
|
17
|
+
|
|
18
|
+
Tag cached functions for on-demand invalidation:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// features/users/users.queries.ts
|
|
22
|
+
'use cache';
|
|
23
|
+
|
|
24
|
+
import { cacheTag, cacheLife } from 'next/cache';
|
|
25
|
+
import { db } from '@/lib/db';
|
|
26
|
+
|
|
27
|
+
// Cache entire user list, invalidate with 'users' tag
|
|
28
|
+
export async function getUsers() {
|
|
29
|
+
cacheTag('users');
|
|
30
|
+
cacheLife('hours'); // seconds | 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'max'
|
|
31
|
+
return db.user.findMany({ orderBy: { createdAt: 'desc' } });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Per-record cache
|
|
35
|
+
export async function getUserById(id: string) {
|
|
36
|
+
cacheTag('users', `user-${id}`);
|
|
37
|
+
cacheLife('minutes');
|
|
38
|
+
return db.user.findUnique({
|
|
39
|
+
where: { id },
|
|
40
|
+
select: { id: true, name: true, email: true, role: true },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Invalidate from a Server Action or Route Handler:
|
|
46
|
+
```ts
|
|
47
|
+
import { revalidateTag } from 'next/cache';
|
|
48
|
+
|
|
49
|
+
revalidateTag('users'); // all list caches
|
|
50
|
+
revalidateTag(`user-${id}`); // specific record cache
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Page-Level Caching
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// Revalidate the whole page every 60 seconds (ISR)
|
|
59
|
+
export const revalidate = 60;
|
|
60
|
+
|
|
61
|
+
// Always render fresh — opt out of caching entirely
|
|
62
|
+
export const dynamic = 'force-dynamic';
|
|
63
|
+
|
|
64
|
+
// Static generation only — never dynamic
|
|
65
|
+
export const dynamic = 'force-static';
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## fetch() Caching
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// Revalidate every 30 seconds + tag for on-demand invalidation
|
|
74
|
+
const res = await fetch('/api/data', {
|
|
75
|
+
next: { revalidate: 30, tags: ['data'] },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Never cache
|
|
79
|
+
const res = await fetch('/api/live', { cache: 'no-store' });
|
|
80
|
+
|
|
81
|
+
// Cache indefinitely until manually revalidated
|
|
82
|
+
const res = await fetch('/api/config', { cache: 'force-cache' });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## On-Demand Revalidation
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// In Server Action or Route Handler — called after mutations
|
|
91
|
+
import { revalidateTag, revalidatePath } from 'next/cache';
|
|
92
|
+
|
|
93
|
+
// Prefer revalidateTag (more precise, works with use cache)
|
|
94
|
+
revalidateTag('users');
|
|
95
|
+
revalidateTag(`user-${id}`);
|
|
96
|
+
|
|
97
|
+
// revalidatePath — revalidates all data for a given URL
|
|
98
|
+
revalidatePath('/users');
|
|
99
|
+
revalidatePath('/users/[id]', 'page'); // all dynamic [id] pages
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## `after()` — Non-Blocking Post-Response Work
|
|
105
|
+
|
|
106
|
+
Run side effects (emails, analytics, audit logs) after the response is sent to the user:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { after } from 'next/server';
|
|
110
|
+
|
|
111
|
+
export async function createUserAction(formData: FormData) {
|
|
112
|
+
const user = await db.user.create({ /* ... */ });
|
|
113
|
+
|
|
114
|
+
// These run AFTER the redirect/response — user doesn't wait
|
|
115
|
+
after(async () => {
|
|
116
|
+
await sendWelcomeEmail(user.email);
|
|
117
|
+
await logAuditEvent('user.created', { userId: user.id });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
revalidateTag('users');
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## unstable_cache (legacy — prefer `use cache`)
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { unstable_cache } from 'next/cache';
|
|
130
|
+
|
|
131
|
+
// Still works in Next.js 15 but use cache directive is preferred
|
|
132
|
+
const getCachedUsers = unstable_cache(
|
|
133
|
+
async () => db.user.findMany(),
|
|
134
|
+
['users-list'],
|
|
135
|
+
{ revalidate: 3600, tags: ['users'] },
|
|
136
|
+
);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Common Patterns
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// Static data — config, reference lists (cache forever)
|
|
145
|
+
cacheLife('max');
|
|
146
|
+
|
|
147
|
+
// User content — pages, posts (cache for hours)
|
|
148
|
+
cacheLife('hours');
|
|
149
|
+
|
|
150
|
+
// Dashboard stats (cache for minutes)
|
|
151
|
+
cacheLife('minutes');
|
|
152
|
+
|
|
153
|
+
// Live data — prices, inventory (no cache)
|
|
154
|
+
export const dynamic = 'force-dynamic';
|
|
155
|
+
// or
|
|
156
|
+
const data = await fetch(url, { cache: 'no-store' });
|
|
157
|
+
```
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Route Handlers Reference
|
|
2
|
+
|
|
3
|
+
Use Route Handlers for: public REST APIs consumed by external clients, file uploads, webhooks from third parties, or complex streaming responses. For internal Next.js mutations, prefer Server Actions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## File Conventions
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
app/api/
|
|
11
|
+
├── users/
|
|
12
|
+
│ ├── route.ts # GET /api/users, POST /api/users
|
|
13
|
+
│ └── [id]/
|
|
14
|
+
│ └── route.ts # GET, PATCH, DELETE /api/users/:id
|
|
15
|
+
└── webhooks/
|
|
16
|
+
└── stripe/
|
|
17
|
+
└── route.ts # POST /api/webhooks/stripe
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Collection Route — route.ts
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// app/api/users/route.ts
|
|
26
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
27
|
+
import { db } from '@/lib/db';
|
|
28
|
+
import { auth } from '@/lib/auth';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
|
|
31
|
+
const listSchema = z.object({
|
|
32
|
+
page: z.coerce.number().int().positive().default(1),
|
|
33
|
+
limit: z.coerce.number().int().positive().max(100).default(10),
|
|
34
|
+
search: z.string().optional(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const createSchema = z.object({
|
|
38
|
+
name: z.string().min(2),
|
|
39
|
+
email: z.string().email(),
|
|
40
|
+
role: z.enum(['USER', 'ADMIN', 'MANAGER']).default('USER'),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export async function GET(req: NextRequest) {
|
|
44
|
+
const session = await auth();
|
|
45
|
+
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
46
|
+
|
|
47
|
+
const { searchParams } = req.nextUrl;
|
|
48
|
+
const parsed = listSchema.safeParse(Object.fromEntries(searchParams));
|
|
49
|
+
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
return NextResponse.json(
|
|
52
|
+
{ error: parsed.error.issues[0].message },
|
|
53
|
+
{ status: 422 },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { page, limit, search } = parsed.data;
|
|
58
|
+
const skip = (page - 1) * limit;
|
|
59
|
+
const where = search ? { name: { contains: search, mode: 'insensitive' as const } } : {};
|
|
60
|
+
|
|
61
|
+
const [data, total] = await Promise.all([
|
|
62
|
+
db.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' } }),
|
|
63
|
+
db.user.count({ where }),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
return NextResponse.json({
|
|
67
|
+
data,
|
|
68
|
+
meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function POST(req: NextRequest) {
|
|
73
|
+
const session = await auth();
|
|
74
|
+
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
75
|
+
|
|
76
|
+
const body = await req.json();
|
|
77
|
+
const parsed = createSchema.safeParse(body);
|
|
78
|
+
|
|
79
|
+
if (!parsed.success) {
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: parsed.error.issues[0].message },
|
|
82
|
+
{ status: 422 },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const user = await db.user.create({ data: parsed.data });
|
|
88
|
+
return NextResponse.json(user, { status: 201 });
|
|
89
|
+
} catch (err: any) {
|
|
90
|
+
if (err.code === 'P2002') {
|
|
91
|
+
return NextResponse.json({ error: 'Email already registered' }, { status: 409 });
|
|
92
|
+
}
|
|
93
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Item Route — [id]/route.ts
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// app/api/users/[id]/route.ts
|
|
104
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
105
|
+
import { db } from '@/lib/db';
|
|
106
|
+
import { auth } from '@/lib/auth';
|
|
107
|
+
import { z } from 'zod';
|
|
108
|
+
|
|
109
|
+
interface Ctx { params: Promise<{ id: string }> } // Next.js 15 — params is a Promise
|
|
110
|
+
|
|
111
|
+
const updateSchema = z.object({
|
|
112
|
+
name: z.string().min(2).optional(),
|
|
113
|
+
email: z.string().email().optional(),
|
|
114
|
+
}).strict();
|
|
115
|
+
|
|
116
|
+
export async function GET(_req: NextRequest, { params }: Ctx) {
|
|
117
|
+
const { id } = await params;
|
|
118
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
119
|
+
if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
120
|
+
return NextResponse.json(user);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function PATCH(req: NextRequest, { params }: Ctx) {
|
|
124
|
+
const session = await auth();
|
|
125
|
+
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
126
|
+
|
|
127
|
+
const { id } = await params;
|
|
128
|
+
const body = await req.json();
|
|
129
|
+
const parsed = updateSchema.safeParse(body);
|
|
130
|
+
|
|
131
|
+
if (!parsed.success) {
|
|
132
|
+
return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 422 });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const user = await db.user.update({ where: { id }, data: parsed.data });
|
|
136
|
+
return NextResponse.json(user);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function DELETE(_req: NextRequest, { params }: Ctx) {
|
|
140
|
+
const session = await auth();
|
|
141
|
+
if (!session?.user || session.user.role !== 'ADMIN') {
|
|
142
|
+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { id } = await params;
|
|
146
|
+
await db.user.delete({ where: { id } });
|
|
147
|
+
return new NextResponse(null, { status: 204 });
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Webhook Route (raw body, no auth middleware)
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// app/api/webhooks/stripe/route.ts
|
|
157
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
158
|
+
import Stripe from 'stripe';
|
|
159
|
+
|
|
160
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
161
|
+
|
|
162
|
+
export async function POST(req: NextRequest) {
|
|
163
|
+
const body = await req.text();
|
|
164
|
+
const signature = req.headers.get('stripe-signature')!;
|
|
165
|
+
|
|
166
|
+
let event: Stripe.Event;
|
|
167
|
+
try {
|
|
168
|
+
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
|
|
169
|
+
} catch {
|
|
170
|
+
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
switch (event.type) {
|
|
174
|
+
case 'payment_intent.succeeded':
|
|
175
|
+
// handle payment
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return NextResponse.json({ received: true });
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Response Helpers
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// Consistent JSON responses
|
|
189
|
+
return NextResponse.json(data); // 200
|
|
190
|
+
return NextResponse.json(data, { status: 201 }); // 201
|
|
191
|
+
return NextResponse.json({ error: 'msg' }, { status: 400 }); // 400
|
|
192
|
+
return new NextResponse(null, { status: 204 }); // 204 no content
|
|
193
|
+
return NextResponse.redirect(new URL('/login', req.url)); // redirect
|
|
194
|
+
```
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Server Actions Reference
|
|
2
|
+
|
|
3
|
+
Server Actions run on the server. They can be called from Client Components via `action=` prop or event handlers. No separate API route needed for simple mutations.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Action File Pattern — {feature}.actions.ts
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// features/users/users.actions.ts
|
|
11
|
+
'use server';
|
|
12
|
+
|
|
13
|
+
import { db } from '@/lib/db';
|
|
14
|
+
import { auth } from '@/lib/auth';
|
|
15
|
+
import { revalidateTag } from 'next/cache';
|
|
16
|
+
import { redirect } from 'next/navigation';
|
|
17
|
+
import { after } from 'next/server';
|
|
18
|
+
import { createUserSchema, updateUserSchema } from './users.schema';
|
|
19
|
+
|
|
20
|
+
// Create — returns state object for useActionState
|
|
21
|
+
export async function createUserAction(_prev: unknown, formData: FormData) {
|
|
22
|
+
const session = await auth();
|
|
23
|
+
if (!session) return { error: 'Unauthorized' };
|
|
24
|
+
|
|
25
|
+
const result = createUserSchema.safeParse({
|
|
26
|
+
name: formData.get('name'),
|
|
27
|
+
email: formData.get('email'),
|
|
28
|
+
role: formData.get('role'),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
return { error: result.error.issues[0].message };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const user = await db.user.create({ data: result.data });
|
|
37
|
+
after(() => sendWelcomeEmail(user.email)); // fire-and-forget after response
|
|
38
|
+
} catch (err: any) {
|
|
39
|
+
if (err.code === 'P2002') return { error: 'Email already registered' };
|
|
40
|
+
return { error: 'Failed to create user' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
revalidateTag('users');
|
|
44
|
+
redirect('/users');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Update — accepts structured args (not FormData)
|
|
48
|
+
export async function updateUserAction(id: string, data: { name?: string; email?: string }) {
|
|
49
|
+
const session = await auth();
|
|
50
|
+
if (!session) throw new Error('Unauthorized');
|
|
51
|
+
|
|
52
|
+
const result = updateUserSchema.safeParse(data);
|
|
53
|
+
if (!result.success) throw new Error(result.error.issues[0].message);
|
|
54
|
+
|
|
55
|
+
await db.user.update({ where: { id }, data: result.data });
|
|
56
|
+
|
|
57
|
+
revalidateTag('users');
|
|
58
|
+
revalidateTag(`user-${id}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Delete — called from event handler in Client Component
|
|
62
|
+
export async function deleteUserAction(id: string) {
|
|
63
|
+
const session = await auth();
|
|
64
|
+
if (!session?.user || session.user.role !== 'ADMIN') {
|
|
65
|
+
throw new Error('Forbidden');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await db.user.delete({ where: { id } });
|
|
69
|
+
|
|
70
|
+
revalidateTag('users');
|
|
71
|
+
revalidateTag(`user-${id}`);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Client Form — useActionState (React 19)
|
|
78
|
+
|
|
79
|
+
`useActionState` is the primary way to wire a Server Action to a form with pending + error state:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// features/users/components/CreateUserForm.tsx
|
|
83
|
+
'use client';
|
|
84
|
+
|
|
85
|
+
import { useActionState } from 'react';
|
|
86
|
+
import { createUserAction } from '../users.actions';
|
|
87
|
+
|
|
88
|
+
export function CreateUserForm() {
|
|
89
|
+
const [state, formAction, isPending] = useActionState(createUserAction, null);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<form action={formAction}>
|
|
93
|
+
<div>
|
|
94
|
+
<label>Name</label>
|
|
95
|
+
<input name="name" required />
|
|
96
|
+
</div>
|
|
97
|
+
<div>
|
|
98
|
+
<label>Email</label>
|
|
99
|
+
<input name="email" type="email" required />
|
|
100
|
+
</div>
|
|
101
|
+
<select name="role" defaultValue="USER">
|
|
102
|
+
<option value="USER">User</option>
|
|
103
|
+
<option value="ADMIN">Admin</option>
|
|
104
|
+
</select>
|
|
105
|
+
|
|
106
|
+
{state?.error && <p className="error">{state.error}</p>}
|
|
107
|
+
|
|
108
|
+
<button type="submit" disabled={isPending}>
|
|
109
|
+
{isPending ? 'Creating...' : 'Create User'}
|
|
110
|
+
</button>
|
|
111
|
+
</form>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## useOptimistic — Optimistic Updates
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
'use client';
|
|
122
|
+
|
|
123
|
+
import { useOptimistic, useTransition } from 'react';
|
|
124
|
+
import { deleteUserAction } from '../users.actions';
|
|
125
|
+
import type { User } from '../users.schema';
|
|
126
|
+
|
|
127
|
+
export function UserList({ users }: { users: User[] }) {
|
|
128
|
+
const [optimisticUsers, removeOptimistic] = useOptimistic(
|
|
129
|
+
users,
|
|
130
|
+
(state, removedId: string) => state.filter((u) => u.id !== removedId),
|
|
131
|
+
);
|
|
132
|
+
const [isPending, startTransition] = useTransition();
|
|
133
|
+
|
|
134
|
+
const handleDelete = (id: string) => {
|
|
135
|
+
startTransition(async () => {
|
|
136
|
+
removeOptimistic(id); // instant UI update
|
|
137
|
+
await deleteUserAction(id); // real delete on server
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<ul>
|
|
143
|
+
{optimisticUsers.map((user) => (
|
|
144
|
+
<li key={user.id}>
|
|
145
|
+
{user.name}
|
|
146
|
+
<button onClick={() => handleDelete(user.id)} disabled={isPending}>
|
|
147
|
+
Delete
|
|
148
|
+
</button>
|
|
149
|
+
</li>
|
|
150
|
+
))}
|
|
151
|
+
</ul>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Calling Actions from Event Handlers
|
|
159
|
+
|
|
160
|
+
Actions can also be called directly (not through a form `action=`):
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
'use client';
|
|
164
|
+
|
|
165
|
+
import { useTransition } from 'react';
|
|
166
|
+
import { updateUserAction } from '../users.actions';
|
|
167
|
+
|
|
168
|
+
export function StatusToggle({ user }: { user: User }) {
|
|
169
|
+
const [isPending, startTransition] = useTransition();
|
|
170
|
+
|
|
171
|
+
const toggle = () => {
|
|
172
|
+
startTransition(async () => {
|
|
173
|
+
await updateUserAction(user.id, { active: !user.active });
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<button onClick={toggle} disabled={isPending}>
|
|
179
|
+
{user.active ? 'Deactivate' : 'Activate'}
|
|
180
|
+
</button>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## useFormStatus — Pending State in Child Components
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
'use client';
|
|
191
|
+
|
|
192
|
+
import { useFormStatus } from 'react-dom';
|
|
193
|
+
|
|
194
|
+
// Reads pending state from the nearest parent <form>
|
|
195
|
+
export function SubmitButton({ label }: { label: string }) {
|
|
196
|
+
const { pending } = useFormStatus();
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<button type="submit" disabled={pending}>
|
|
200
|
+
{pending ? 'Saving...' : label}
|
|
201
|
+
</button>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Rules
|
|
209
|
+
|
|
210
|
+
- Always authenticate at the top of every Server Action — never trust the caller
|
|
211
|
+
- Return `{ error: string }` from `useActionState` actions (never throw — it breaks the state pattern)
|
|
212
|
+
- Throw from actions called via `startTransition` — error boundary catches it
|
|
213
|
+
- Use `revalidateTag` after mutations, never `revalidatePath` for cacheable data
|
|
214
|
+
- Use `after()` for non-critical side effects (emails, logs) — keeps the action fast
|