claudient 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/.claude-plugin/plugin.json +42 -0
- package/CONTEXT.md +58 -0
- package/README.md +165 -0
- package/agents/build-resolvers/de/python-resolver.md +64 -0
- package/agents/build-resolvers/de/typescript-resolver.md +65 -0
- package/agents/build-resolvers/es/python-resolver.md +64 -0
- package/agents/build-resolvers/es/typescript-resolver.md +65 -0
- package/agents/build-resolvers/fr/python-resolver.md +64 -0
- package/agents/build-resolvers/fr/typescript-resolver.md +65 -0
- package/agents/build-resolvers/nl/python-resolver.md +64 -0
- package/agents/build-resolvers/nl/typescript-resolver.md +65 -0
- package/agents/build-resolvers/python-resolver.md +62 -0
- package/agents/build-resolvers/typescript-resolver.md +63 -0
- package/agents/core/architect.md +64 -0
- package/agents/core/code-reviewer.md +78 -0
- package/agents/core/de/architect.md +66 -0
- package/agents/core/de/code-reviewer.md +80 -0
- package/agents/core/de/planner.md +63 -0
- package/agents/core/de/security-reviewer.md +93 -0
- package/agents/core/es/architect.md +66 -0
- package/agents/core/es/code-reviewer.md +80 -0
- package/agents/core/es/planner.md +63 -0
- package/agents/core/es/security-reviewer.md +93 -0
- package/agents/core/fr/architect.md +66 -0
- package/agents/core/fr/code-reviewer.md +80 -0
- package/agents/core/fr/planner.md +63 -0
- package/agents/core/fr/security-reviewer.md +93 -0
- package/agents/core/nl/architect.md +66 -0
- package/agents/core/nl/code-reviewer.md +80 -0
- package/agents/core/nl/planner.md +63 -0
- package/agents/core/nl/security-reviewer.md +93 -0
- package/agents/core/planner.md +61 -0
- package/agents/core/security-reviewer.md +91 -0
- package/guides/agent-orchestration.md +231 -0
- package/guides/de/agent-orchestration.md +174 -0
- package/guides/de/getting-started.md +164 -0
- package/guides/de/hooks-cookbook.md +160 -0
- package/guides/de/memory-management.md +153 -0
- package/guides/de/security.md +180 -0
- package/guides/de/skill-authoring.md +214 -0
- package/guides/de/token-optimization.md +156 -0
- package/guides/es/agent-orchestration.md +174 -0
- package/guides/es/getting-started.md +164 -0
- package/guides/es/hooks-cookbook.md +160 -0
- package/guides/es/memory-management.md +153 -0
- package/guides/es/security.md +180 -0
- package/guides/es/skill-authoring.md +214 -0
- package/guides/es/token-optimization.md +156 -0
- package/guides/fr/agent-orchestration.md +174 -0
- package/guides/fr/getting-started.md +164 -0
- package/guides/fr/hooks-cookbook.md +227 -0
- package/guides/fr/memory-management.md +169 -0
- package/guides/fr/security.md +180 -0
- package/guides/fr/skill-authoring.md +214 -0
- package/guides/fr/token-optimization.md +158 -0
- package/guides/getting-started.md +164 -0
- package/guides/hooks-cookbook.md +423 -0
- package/guides/memory-management.md +192 -0
- package/guides/nl/agent-orchestration.md +174 -0
- package/guides/nl/getting-started.md +164 -0
- package/guides/nl/hooks-cookbook.md +160 -0
- package/guides/nl/memory-management.md +153 -0
- package/guides/nl/security.md +180 -0
- package/guides/nl/skill-authoring.md +214 -0
- package/guides/nl/token-optimization.md +156 -0
- package/guides/security.md +229 -0
- package/guides/skill-authoring.md +226 -0
- package/guides/token-optimization.md +169 -0
- package/hooks/lifecycle/cost-tracker.md +49 -0
- package/hooks/lifecycle/cost-tracker.sh +59 -0
- package/hooks/lifecycle/pre-compact-save.md +56 -0
- package/hooks/lifecycle/pre-compact-save.sh +37 -0
- package/hooks/lifecycle/session-start.md +50 -0
- package/hooks/lifecycle/session-start.sh +47 -0
- package/hooks/post-tool-use/audit-log.md +53 -0
- package/hooks/post-tool-use/audit-log.sh +53 -0
- package/hooks/post-tool-use/prettier.md +53 -0
- package/hooks/post-tool-use/prettier.sh +49 -0
- package/hooks/pre-tool-use/block-dangerous.md +48 -0
- package/hooks/pre-tool-use/block-dangerous.sh +76 -0
- package/hooks/pre-tool-use/git-push-confirm.md +46 -0
- package/hooks/pre-tool-use/git-push-confirm.sh +36 -0
- package/mcp/configs/github.json +11 -0
- package/mcp/configs/postgres.json +11 -0
- package/mcp/de/recommended-servers.md +170 -0
- package/mcp/es/recommended-servers.md +170 -0
- package/mcp/fr/recommended-servers.md +170 -0
- package/mcp/nl/recommended-servers.md +170 -0
- package/mcp/recommended-servers.md +168 -0
- package/package.json +45 -0
- package/prompts/project-starters/de/fastapi-project.md +62 -0
- package/prompts/project-starters/de/nextjs-project.md +82 -0
- package/prompts/project-starters/es/fastapi-project.md +62 -0
- package/prompts/project-starters/es/nextjs-project.md +82 -0
- package/prompts/project-starters/fastapi-project.md +60 -0
- package/prompts/project-starters/fr/fastapi-project.md +62 -0
- package/prompts/project-starters/fr/nextjs-project.md +82 -0
- package/prompts/project-starters/nextjs-project.md +80 -0
- package/prompts/project-starters/nl/fastapi-project.md +62 -0
- package/prompts/project-starters/nl/nextjs-project.md +82 -0
- package/prompts/system-prompts/ai-product.md +80 -0
- package/prompts/system-prompts/data-pipeline.md +76 -0
- package/prompts/system-prompts/de/ai-product.md +82 -0
- package/prompts/system-prompts/de/data-pipeline.md +78 -0
- package/prompts/system-prompts/de/saas-backend.md +71 -0
- package/prompts/system-prompts/es/ai-product.md +82 -0
- package/prompts/system-prompts/es/data-pipeline.md +78 -0
- package/prompts/system-prompts/es/saas-backend.md +71 -0
- package/prompts/system-prompts/fr/ai-product.md +82 -0
- package/prompts/system-prompts/fr/data-pipeline.md +78 -0
- package/prompts/system-prompts/fr/saas-backend.md +71 -0
- package/prompts/system-prompts/nl/ai-product.md +82 -0
- package/prompts/system-prompts/nl/data-pipeline.md +78 -0
- package/prompts/system-prompts/nl/saas-backend.md +71 -0
- package/prompts/system-prompts/saas-backend.md +69 -0
- package/prompts/task-specific/changelog.md +81 -0
- package/prompts/task-specific/de/changelog.md +83 -0
- package/prompts/task-specific/de/debugging.md +78 -0
- package/prompts/task-specific/de/pr-description.md +69 -0
- package/prompts/task-specific/debugging.md +76 -0
- package/prompts/task-specific/es/changelog.md +83 -0
- package/prompts/task-specific/es/debugging.md +78 -0
- package/prompts/task-specific/es/pr-description.md +69 -0
- package/prompts/task-specific/fr/changelog.md +83 -0
- package/prompts/task-specific/fr/debugging.md +78 -0
- package/prompts/task-specific/fr/pr-description.md +69 -0
- package/prompts/task-specific/nl/changelog.md +83 -0
- package/prompts/task-specific/nl/debugging.md +78 -0
- package/prompts/task-specific/nl/pr-description.md +69 -0
- package/prompts/task-specific/pr-description.md +67 -0
- package/rules/common/coding-style.md +45 -0
- package/rules/common/de/coding-style.md +47 -0
- package/rules/common/de/git.md +48 -0
- package/rules/common/de/performance.md +40 -0
- package/rules/common/de/security.md +45 -0
- package/rules/common/de/testing.md +45 -0
- package/rules/common/es/coding-style.md +47 -0
- package/rules/common/es/git.md +48 -0
- package/rules/common/es/performance.md +40 -0
- package/rules/common/es/security.md +45 -0
- package/rules/common/es/testing.md +45 -0
- package/rules/common/fr/coding-style.md +47 -0
- package/rules/common/fr/git.md +48 -0
- package/rules/common/fr/performance.md +40 -0
- package/rules/common/fr/security.md +45 -0
- package/rules/common/fr/testing.md +45 -0
- package/rules/common/git.md +46 -0
- package/rules/common/nl/coding-style.md +47 -0
- package/rules/common/nl/git.md +48 -0
- package/rules/common/nl/performance.md +40 -0
- package/rules/common/nl/security.md +45 -0
- package/rules/common/nl/testing.md +45 -0
- package/rules/common/performance.md +38 -0
- package/rules/common/security.md +43 -0
- package/rules/common/testing.md +43 -0
- package/rules/language-specific/de/go.md +48 -0
- package/rules/language-specific/de/python.md +38 -0
- package/rules/language-specific/de/typescript.md +51 -0
- package/rules/language-specific/es/go.md +48 -0
- package/rules/language-specific/es/python.md +38 -0
- package/rules/language-specific/es/typescript.md +51 -0
- package/rules/language-specific/fr/go.md +48 -0
- package/rules/language-specific/fr/python.md +38 -0
- package/rules/language-specific/fr/typescript.md +51 -0
- package/rules/language-specific/go.md +46 -0
- package/rules/language-specific/nl/go.md +48 -0
- package/rules/language-specific/nl/python.md +38 -0
- package/rules/language-specific/nl/typescript.md +51 -0
- package/rules/language-specific/python.md +36 -0
- package/rules/language-specific/typescript.md +49 -0
- package/scripts/cli.js +161 -0
- package/scripts/link-skills.sh +35 -0
- package/scripts/list-skills.sh +34 -0
- package/skills/ai-engineering/agent-construction.md +285 -0
- package/skills/ai-engineering/claude-api.md +248 -0
- package/skills/ai-engineering/de/agent-construction.md +287 -0
- package/skills/ai-engineering/de/claude-api.md +250 -0
- package/skills/ai-engineering/es/agent-construction.md +287 -0
- package/skills/ai-engineering/es/claude-api.md +250 -0
- package/skills/ai-engineering/fr/agent-construction.md +287 -0
- package/skills/ai-engineering/fr/claude-api.md +250 -0
- package/skills/ai-engineering/nl/agent-construction.md +287 -0
- package/skills/ai-engineering/nl/claude-api.md +250 -0
- package/skills/backend/dotnet/csharp.md +304 -0
- package/skills/backend/dotnet/de/csharp.md +306 -0
- package/skills/backend/dotnet/es/csharp.md +306 -0
- package/skills/backend/dotnet/fr/csharp.md +306 -0
- package/skills/backend/dotnet/nl/csharp.md +306 -0
- package/skills/backend/go/de/go.md +307 -0
- package/skills/backend/go/es/go.md +307 -0
- package/skills/backend/go/fr/go.md +307 -0
- package/skills/backend/go/go.md +305 -0
- package/skills/backend/go/nl/go.md +307 -0
- package/skills/backend/nodejs/de/nestjs.md +274 -0
- package/skills/backend/nodejs/de/nextjs.md +222 -0
- package/skills/backend/nodejs/es/nestjs.md +274 -0
- package/skills/backend/nodejs/es/nextjs.md +222 -0
- package/skills/backend/nodejs/fr/nestjs.md +274 -0
- package/skills/backend/nodejs/fr/nextjs.md +222 -0
- package/skills/backend/nodejs/nestjs.md +272 -0
- package/skills/backend/nodejs/nextjs.md +220 -0
- package/skills/backend/nodejs/nl/nestjs.md +274 -0
- package/skills/backend/nodejs/nl/nextjs.md +222 -0
- package/skills/backend/python/de/django.md +285 -0
- package/skills/backend/python/de/fastapi.md +244 -0
- package/skills/backend/python/django.md +283 -0
- package/skills/backend/python/es/django.md +285 -0
- package/skills/backend/python/es/fastapi.md +244 -0
- package/skills/backend/python/fastapi.md +242 -0
- package/skills/backend/python/fr/django.md +285 -0
- package/skills/backend/python/fr/fastapi.md +244 -0
- package/skills/backend/python/nl/django.md +285 -0
- package/skills/backend/python/nl/fastapi.md +244 -0
- package/skills/data-ml/dbt-data-pipelines.md +155 -0
- package/skills/data-ml/de/dbt-data-pipelines.md +157 -0
- package/skills/data-ml/de/pandas-polars.md +147 -0
- package/skills/data-ml/de/pytorch-tensorflow.md +171 -0
- package/skills/data-ml/es/dbt-data-pipelines.md +157 -0
- package/skills/data-ml/es/pandas-polars.md +147 -0
- package/skills/data-ml/es/pytorch-tensorflow.md +171 -0
- package/skills/data-ml/fr/dbt-data-pipelines.md +157 -0
- package/skills/data-ml/fr/pandas-polars.md +147 -0
- package/skills/data-ml/fr/pytorch-tensorflow.md +171 -0
- package/skills/data-ml/nl/dbt-data-pipelines.md +157 -0
- package/skills/data-ml/nl/pandas-polars.md +147 -0
- package/skills/data-ml/nl/pytorch-tensorflow.md +171 -0
- package/skills/data-ml/pandas-polars.md +145 -0
- package/skills/data-ml/pytorch-tensorflow.md +169 -0
- package/skills/database/de/graphql.md +181 -0
- package/skills/database/es/graphql.md +181 -0
- package/skills/database/fr/graphql.md +181 -0
- package/skills/database/graphql.md +179 -0
- package/skills/database/nl/graphql.md +181 -0
- package/skills/devops-infra/de/docker.md +133 -0
- package/skills/devops-infra/de/github-actions.md +179 -0
- package/skills/devops-infra/de/kubernetes.md +129 -0
- package/skills/devops-infra/de/terraform.md +130 -0
- package/skills/devops-infra/docker.md +131 -0
- package/skills/devops-infra/es/docker.md +133 -0
- package/skills/devops-infra/es/github-actions.md +179 -0
- package/skills/devops-infra/es/kubernetes.md +129 -0
- package/skills/devops-infra/es/terraform.md +130 -0
- package/skills/devops-infra/fr/docker.md +133 -0
- package/skills/devops-infra/fr/github-actions.md +179 -0
- package/skills/devops-infra/fr/kubernetes.md +129 -0
- package/skills/devops-infra/fr/terraform.md +130 -0
- package/skills/devops-infra/github-actions.md +177 -0
- package/skills/devops-infra/kubernetes.md +127 -0
- package/skills/devops-infra/nl/docker.md +133 -0
- package/skills/devops-infra/nl/github-actions.md +179 -0
- package/skills/devops-infra/nl/kubernetes.md +129 -0
- package/skills/devops-infra/nl/terraform.md +130 -0
- package/skills/devops-infra/terraform.md +128 -0
- package/skills/finance-payments/de/stripe.md +187 -0
- package/skills/finance-payments/es/stripe.md +187 -0
- package/skills/finance-payments/fr/stripe.md +187 -0
- package/skills/finance-payments/nl/stripe.md +187 -0
- package/skills/finance-payments/stripe.md +185 -0
- package/workflows/code-review.md +151 -0
- package/workflows/de/code-review.md +153 -0
- package/workflows/de/debugging-session.md +146 -0
- package/workflows/de/feature-development.md +155 -0
- package/workflows/de/new-project-bootstrap.md +175 -0
- package/workflows/de/refactor-safely.md +150 -0
- package/workflows/debugging-session.md +144 -0
- package/workflows/es/code-review.md +153 -0
- package/workflows/es/debugging-session.md +146 -0
- package/workflows/es/feature-development.md +155 -0
- package/workflows/es/new-project-bootstrap.md +175 -0
- package/workflows/es/refactor-safely.md +150 -0
- package/workflows/feature-development.md +153 -0
- package/workflows/fr/code-review.md +153 -0
- package/workflows/fr/debugging-session.md +146 -0
- package/workflows/fr/feature-development.md +155 -0
- package/workflows/fr/new-project-bootstrap.md +175 -0
- package/workflows/fr/refactor-safely.md +150 -0
- package/workflows/new-project-bootstrap.md +173 -0
- package/workflows/nl/code-review.md +153 -0
- package/workflows/nl/debugging-session.md +146 -0
- package/workflows/nl/feature-development.md +155 -0
- package/workflows/nl/new-project-bootstrap.md +175 -0
- package/workflows/nl/refactor-safely.md +150 -0
- package/workflows/refactor-safely.md +148 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Next.js Skill
|
|
2
|
+
|
|
3
|
+
## When to activate
|
|
4
|
+
- Building a Next.js application using the App Router
|
|
5
|
+
- Deciding between Server Components and Client Components
|
|
6
|
+
- Writing Server Actions for form submissions and mutations
|
|
7
|
+
- Setting up route handlers (API endpoints in App Router)
|
|
8
|
+
- Implementing authentication with NextAuth or a JWT pattern
|
|
9
|
+
- Configuring middleware for redirects and auth guards
|
|
10
|
+
- Optimizing data fetching with React `cache()` and `unstable_cache`
|
|
11
|
+
- Using parallel routes, intercepting routes, or route groups
|
|
12
|
+
|
|
13
|
+
## When NOT to use
|
|
14
|
+
- Pages Router projects — the patterns differ significantly
|
|
15
|
+
- Pure SPAs with no server rendering (use Vite + React)
|
|
16
|
+
- NestJS or Express backends — use NestJS skill
|
|
17
|
+
- Static sites with no dynamic data (use Astro)
|
|
18
|
+
|
|
19
|
+
## Instructions
|
|
20
|
+
|
|
21
|
+
### App Router directory structure
|
|
22
|
+
```
|
|
23
|
+
app/
|
|
24
|
+
├── (auth)/ # Route group — no URL segment
|
|
25
|
+
│ ├── login/
|
|
26
|
+
│ │ └── page.tsx
|
|
27
|
+
│ └── layout.tsx # Auth-specific layout
|
|
28
|
+
├── (dashboard)/
|
|
29
|
+
│ ├── dashboard/
|
|
30
|
+
│ │ ├── page.tsx # Server Component by default
|
|
31
|
+
│ │ └── loading.tsx # Suspense boundary UI
|
|
32
|
+
│ └── layout.tsx
|
|
33
|
+
├── api/
|
|
34
|
+
│ └── webhooks/
|
|
35
|
+
│ └── stripe/
|
|
36
|
+
│ └── route.ts # Route Handler
|
|
37
|
+
├── layout.tsx # Root layout (required)
|
|
38
|
+
└── page.tsx # Home page
|
|
39
|
+
components/
|
|
40
|
+
├── ui/ # Presentational (can be server or client)
|
|
41
|
+
└── forms/ # Always client components (useState/events)
|
|
42
|
+
lib/
|
|
43
|
+
├── auth.ts
|
|
44
|
+
├── db.ts
|
|
45
|
+
└── actions/ # Server Actions
|
|
46
|
+
└── user.ts
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Server vs. Client Components
|
|
50
|
+
```tsx
|
|
51
|
+
// Server Component (default) — runs on server, never sent to client
|
|
52
|
+
// Can: await fetch, read DB, access env vars
|
|
53
|
+
// Cannot: useState, useEffect, browser APIs, event handlers
|
|
54
|
+
export default async function UserProfile({ id }: { id: string }) {
|
|
55
|
+
const user = await db.user.findUnique({ where: { id } })
|
|
56
|
+
return <div>{user.name}</div>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Client Component — add 'use client' directive
|
|
60
|
+
'use client'
|
|
61
|
+
import { useState } from 'react'
|
|
62
|
+
|
|
63
|
+
export function LikeButton({ initialCount }: { initialCount: number }) {
|
|
64
|
+
const [count, setCount] = useState(initialCount)
|
|
65
|
+
return <button onClick={() => setCount(c => c + 1)}>{count} likes</button>
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Rule: default to Server Components. Add `'use client'` only when you need interactivity, browser APIs, or React hooks.
|
|
70
|
+
|
|
71
|
+
### Data fetching patterns
|
|
72
|
+
```tsx
|
|
73
|
+
// Server Component — direct async/await, no useEffect, no useState
|
|
74
|
+
export default async function PostList() {
|
|
75
|
+
const posts = await db.post.findMany({ where: { published: true } })
|
|
76
|
+
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Deduplication — React cache() wraps a function so multiple components
|
|
80
|
+
// calling it in one render share one fetch
|
|
81
|
+
import { cache } from 'react'
|
|
82
|
+
export const getUser = cache(async (id: string) => {
|
|
83
|
+
return db.user.findUnique({ where: { id } })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Static generation with revalidation
|
|
87
|
+
async function getProducts() {
|
|
88
|
+
const res = await fetch('https://api.example.com/products', {
|
|
89
|
+
next: { revalidate: 3600 }, // ISR — revalidate every hour
|
|
90
|
+
})
|
|
91
|
+
return res.json()
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Server Actions
|
|
96
|
+
```tsx
|
|
97
|
+
// lib/actions/user.ts
|
|
98
|
+
'use server'
|
|
99
|
+
import { revalidatePath } from 'next/cache'
|
|
100
|
+
import { z } from 'zod'
|
|
101
|
+
|
|
102
|
+
const UpdateSchema = z.object({
|
|
103
|
+
name: z.string().min(1),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
export async function updateUser(formData: FormData) {
|
|
107
|
+
const parsed = UpdateSchema.safeParse({ name: formData.get('name') })
|
|
108
|
+
if (!parsed.success) return { error: parsed.error.flatten() }
|
|
109
|
+
|
|
110
|
+
await db.user.update({ where: { id: getCurrentUserId() }, data: parsed.data })
|
|
111
|
+
revalidatePath('/dashboard/profile')
|
|
112
|
+
return { success: true }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// In a Server Component:
|
|
116
|
+
export default function ProfileForm({ user }: { user: User }) {
|
|
117
|
+
return (
|
|
118
|
+
<form action={updateUser}>
|
|
119
|
+
<input name="name" defaultValue={user.name} />
|
|
120
|
+
<button type="submit">Save</button>
|
|
121
|
+
</form>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Route Handlers
|
|
127
|
+
```ts
|
|
128
|
+
// app/api/webhooks/stripe/route.ts
|
|
129
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
130
|
+
import Stripe from 'stripe'
|
|
131
|
+
|
|
132
|
+
export async function POST(req: NextRequest) {
|
|
133
|
+
const body = await req.text()
|
|
134
|
+
const sig = req.headers.get('stripe-signature')!
|
|
135
|
+
|
|
136
|
+
let event: Stripe.Event
|
|
137
|
+
try {
|
|
138
|
+
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
|
|
139
|
+
} catch {
|
|
140
|
+
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (event.type === 'checkout.session.completed') {
|
|
144
|
+
await handleCheckoutCompleted(event.data.object)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return NextResponse.json({ received: true })
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Middleware
|
|
152
|
+
```ts
|
|
153
|
+
// middleware.ts (root level)
|
|
154
|
+
import { NextResponse } from 'next/server'
|
|
155
|
+
import type { NextRequest } from 'next/server'
|
|
156
|
+
|
|
157
|
+
export function middleware(request: NextRequest) {
|
|
158
|
+
const token = request.cookies.get('auth-token')
|
|
159
|
+
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
160
|
+
return NextResponse.redirect(new URL('/login', request.url))
|
|
161
|
+
}
|
|
162
|
+
return NextResponse.next()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const config = {
|
|
166
|
+
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Parallel and intercepting routes
|
|
171
|
+
```
|
|
172
|
+
app/
|
|
173
|
+
├── @modal/ # Parallel route — renders alongside main content
|
|
174
|
+
│ └── (.)photo/ # Intercepting route — intercepts /photo/[id]
|
|
175
|
+
│ └── [id]/
|
|
176
|
+
│ └── page.tsx
|
|
177
|
+
├── photo/
|
|
178
|
+
│ └── [id]/
|
|
179
|
+
│ └── page.tsx
|
|
180
|
+
└── layout.tsx # Accepts { children, modal } as props
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Error and loading boundaries
|
|
184
|
+
```tsx
|
|
185
|
+
// app/dashboard/loading.tsx — shown during Suspense
|
|
186
|
+
export default function Loading() {
|
|
187
|
+
return <DashboardSkeleton />
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// app/dashboard/error.tsx — error boundary for this segment
|
|
191
|
+
'use client'
|
|
192
|
+
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
193
|
+
return (
|
|
194
|
+
<div>
|
|
195
|
+
<p>{error.message}</p>
|
|
196
|
+
<button onClick={reset}>Retry</button>
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Environment variables
|
|
203
|
+
- `NEXT_PUBLIC_*` — exposed to the browser
|
|
204
|
+
- All others — server-only (never accessible in Client Components)
|
|
205
|
+
- Never import a server-only env var inside a Client Component — it returns `undefined` silently
|
|
206
|
+
|
|
207
|
+
## Example
|
|
208
|
+
|
|
209
|
+
**User:** Add a paginated blog posts page at `/blog` that fetches from PostgreSQL, with a "New Post" modal that opens at `/blog/new` but doesn't navigate away from the posts list.
|
|
210
|
+
|
|
211
|
+
**Expected output:**
|
|
212
|
+
- `app/blog/page.tsx` — Server Component, fetches posts with `db.post.findMany`, renders `<PostList>` + `<Link href="/blog/new">`
|
|
213
|
+
- `app/@modal/(.)blog/new/page.tsx` — Intercepting route showing a `<NewPostModal>` Client Component
|
|
214
|
+
- `app/blog/new/page.tsx` — Full-page fallback for direct navigation
|
|
215
|
+
- `app/layout.tsx` — Updated to accept `modal` parallel route slot and render it alongside `children`
|
|
216
|
+
- `lib/actions/post.ts` — `createPost` Server Action with Zod validation + `revalidatePath('/blog')`
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
> **Work with us:** Claudient is backed by [Uitbreiden](https://uitbreiden.com/) — we build AI products and B2B solutions with developer communities. [uitbreiden.com](https://uitbreiden.com/)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
> 🇳🇱 Dit is de Nederlandse vertaling. [Engelse versie](../nestjs.md).
|
|
2
|
+
|
|
3
|
+
# NestJS Skill
|
|
4
|
+
|
|
5
|
+
## Wanneer te activeren
|
|
6
|
+
- Een NestJS-applicatie bouwen (modules, controllers, services)
|
|
7
|
+
- Guards, interceptors, pipes en exception filters instellen
|
|
8
|
+
- TypeORM of Prisma integreren met NestJS
|
|
9
|
+
- CQRS implementeren met commando's, queries en events
|
|
10
|
+
- Microservices instellen (TCP, Redis, RabbitMQ transport)
|
|
11
|
+
- Unit- en e2e-tests schrijven met Jest en Supertest
|
|
12
|
+
- OpenAPI-documentatie genereren met `@nestjs/swagger`
|
|
13
|
+
|
|
14
|
+
## Wanneer NIET te gebruiken
|
|
15
|
+
- Next.js API-routes of standalone Fastify — ander framework
|
|
16
|
+
- Eenvoudige Express-scripts — NestJS-overhead is niet gerechtvaardigd
|
|
17
|
+
- Lambda-functies waarbij cold start-tijd kritiek is
|
|
18
|
+
|
|
19
|
+
## Instructies
|
|
20
|
+
|
|
21
|
+
### Module-structuur
|
|
22
|
+
```
|
|
23
|
+
src/
|
|
24
|
+
├── app.module.ts # Root module
|
|
25
|
+
├── main.ts # Bootstrap
|
|
26
|
+
├── common/
|
|
27
|
+
│ ├── decorators/
|
|
28
|
+
│ ├── filters/
|
|
29
|
+
│ ├── guards/
|
|
30
|
+
│ ├── interceptors/
|
|
31
|
+
│ └── pipes/
|
|
32
|
+
├── config/
|
|
33
|
+
│ └── configuration.ts # ConfigService-instelling
|
|
34
|
+
└── modules/
|
|
35
|
+
└── users/
|
|
36
|
+
├── users.module.ts
|
|
37
|
+
├── users.controller.ts
|
|
38
|
+
├── users.service.ts
|
|
39
|
+
├── dto/
|
|
40
|
+
│ ├── create-user.dto.ts
|
|
41
|
+
│ └── update-user.dto.ts
|
|
42
|
+
├── entities/
|
|
43
|
+
│ └── user.entity.ts
|
|
44
|
+
└── users.service.spec.ts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Bootstrap
|
|
48
|
+
```ts
|
|
49
|
+
// main.ts
|
|
50
|
+
import { NestFactory } from '@nestjs/core'
|
|
51
|
+
import { AppModule } from './app.module'
|
|
52
|
+
import { ValidationPipe } from '@nestjs/common'
|
|
53
|
+
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
|
|
54
|
+
|
|
55
|
+
async function bootstrap() {
|
|
56
|
+
const app = await NestFactory.create(AppModule)
|
|
57
|
+
|
|
58
|
+
app.useGlobalPipes(new ValidationPipe({
|
|
59
|
+
whitelist: true, // verwijder onbekende velden
|
|
60
|
+
forbidNonWhitelisted: true,
|
|
61
|
+
transform: true, // transformeer payloads automatisch naar DTO-klassen
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
const config = new DocumentBuilder()
|
|
65
|
+
.setTitle('API')
|
|
66
|
+
.setVersion('1.0')
|
|
67
|
+
.addBearerAuth()
|
|
68
|
+
.build()
|
|
69
|
+
SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, config))
|
|
70
|
+
|
|
71
|
+
await app.listen(3000)
|
|
72
|
+
}
|
|
73
|
+
bootstrap()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Module, Controller, Service
|
|
77
|
+
```ts
|
|
78
|
+
// users.module.ts
|
|
79
|
+
@Module({
|
|
80
|
+
imports: [TypeOrmModule.forFeature([User]), JwtModule.register({})],
|
|
81
|
+
controllers: [UsersController],
|
|
82
|
+
providers: [UsersService],
|
|
83
|
+
exports: [UsersService],
|
|
84
|
+
})
|
|
85
|
+
export class UsersModule {}
|
|
86
|
+
|
|
87
|
+
// users.controller.ts
|
|
88
|
+
@ApiTags('users')
|
|
89
|
+
@ApiBearerAuth()
|
|
90
|
+
@UseGuards(JwtAuthGuard)
|
|
91
|
+
@Controller('users')
|
|
92
|
+
export class UsersController {
|
|
93
|
+
constructor(private readonly usersService: UsersService) {}
|
|
94
|
+
|
|
95
|
+
@Post()
|
|
96
|
+
@HttpCode(HttpStatus.CREATED)
|
|
97
|
+
create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
|
|
98
|
+
return this.usersService.create(dto)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Get(':id')
|
|
102
|
+
findOne(@Param('id', ParseUUIDPipe) id: string): Promise<UserResponseDto> {
|
|
103
|
+
return this.usersService.findOneOrFail(id)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// users.service.ts
|
|
108
|
+
@Injectable()
|
|
109
|
+
export class UsersService {
|
|
110
|
+
constructor(
|
|
111
|
+
@InjectRepository(User)
|
|
112
|
+
private readonly userRepo: Repository<User>,
|
|
113
|
+
) {}
|
|
114
|
+
|
|
115
|
+
async create(dto: CreateUserDto): Promise<User> {
|
|
116
|
+
const exists = await this.userRepo.findOneBy({ email: dto.email })
|
|
117
|
+
if (exists) throw new ConflictException('Email already in use')
|
|
118
|
+
const user = this.userRepo.create({
|
|
119
|
+
...dto,
|
|
120
|
+
password: await bcrypt.hash(dto.password, 10),
|
|
121
|
+
})
|
|
122
|
+
return this.userRepo.save(user)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async findOneOrFail(id: string): Promise<User> {
|
|
126
|
+
const user = await this.userRepo.findOneBy({ id })
|
|
127
|
+
if (!user) throw new NotFoundException(`User ${id} not found`)
|
|
128
|
+
return user
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### DTO's met class-validator
|
|
134
|
+
```ts
|
|
135
|
+
// dto/create-user.dto.ts
|
|
136
|
+
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator'
|
|
137
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
|
138
|
+
|
|
139
|
+
export class CreateUserDto {
|
|
140
|
+
@ApiProperty({ example: 'user@example.com' })
|
|
141
|
+
@IsEmail()
|
|
142
|
+
email: string
|
|
143
|
+
|
|
144
|
+
@ApiProperty({ minLength: 8 })
|
|
145
|
+
@IsString()
|
|
146
|
+
@MinLength(8)
|
|
147
|
+
password: string
|
|
148
|
+
|
|
149
|
+
@ApiPropertyOptional()
|
|
150
|
+
@IsOptional()
|
|
151
|
+
@IsString()
|
|
152
|
+
name?: string
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Guards
|
|
157
|
+
```ts
|
|
158
|
+
// common/guards/jwt-auth.guard.ts
|
|
159
|
+
@Injectable()
|
|
160
|
+
export class JwtAuthGuard extends AuthGuard('jwt') {
|
|
161
|
+
handleRequest<T>(err: Error, user: T, info: Error): T {
|
|
162
|
+
if (err || !user) {
|
|
163
|
+
throw err || new UnauthorizedException(info?.message)
|
|
164
|
+
}
|
|
165
|
+
return user
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// common/guards/roles.guard.ts
|
|
170
|
+
@Injectable()
|
|
171
|
+
export class RolesGuard implements CanActivate {
|
|
172
|
+
constructor(private reflector: Reflector) {}
|
|
173
|
+
|
|
174
|
+
canActivate(context: ExecutionContext): boolean {
|
|
175
|
+
const roles = this.reflector.getAllAndOverride<Role[]>('roles', [
|
|
176
|
+
context.getHandler(),
|
|
177
|
+
context.getClass(),
|
|
178
|
+
])
|
|
179
|
+
if (!roles) return true
|
|
180
|
+
const { user } = context.switchToHttp().getRequest()
|
|
181
|
+
return roles.some(role => user.roles?.includes(role))
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Interceptors
|
|
187
|
+
```ts
|
|
188
|
+
// common/interceptors/transform.interceptor.ts
|
|
189
|
+
@Injectable()
|
|
190
|
+
export class TransformInterceptor<T>
|
|
191
|
+
implements NestInterceptor<T, { data: T; timestamp: string }>
|
|
192
|
+
{
|
|
193
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
194
|
+
return next.handle().pipe(
|
|
195
|
+
map(data => ({ data, timestamp: new Date().toISOString() }))
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### CQRS
|
|
202
|
+
```ts
|
|
203
|
+
// Installeer: @nestjs/cqrs
|
|
204
|
+
|
|
205
|
+
// commands/create-user.command.ts
|
|
206
|
+
export class CreateUserCommand {
|
|
207
|
+
constructor(public readonly dto: CreateUserDto) {}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// commands/create-user.handler.ts
|
|
211
|
+
@CommandHandler(CreateUserCommand)
|
|
212
|
+
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
|
|
213
|
+
constructor(private readonly userRepo: UserRepository) {}
|
|
214
|
+
|
|
215
|
+
async execute(command: CreateUserCommand): Promise<User> {
|
|
216
|
+
const user = User.create(command.dto)
|
|
217
|
+
await this.userRepo.save(user)
|
|
218
|
+
return user
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// In service/controller:
|
|
223
|
+
const user = await this.commandBus.execute(new CreateUserCommand(dto))
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Testen
|
|
227
|
+
```ts
|
|
228
|
+
// Unit test
|
|
229
|
+
describe('UsersService', () => {
|
|
230
|
+
let service: UsersService
|
|
231
|
+
let repo: jest.Mocked<Repository<User>>
|
|
232
|
+
|
|
233
|
+
beforeEach(async () => {
|
|
234
|
+
const module = await Test.createTestingModule({
|
|
235
|
+
providers: [
|
|
236
|
+
UsersService,
|
|
237
|
+
{ provide: getRepositoryToken(User), useValue: createMockRepository() },
|
|
238
|
+
],
|
|
239
|
+
}).compile()
|
|
240
|
+
service = module.get(UsersService)
|
|
241
|
+
repo = module.get(getRepositoryToken(User))
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('throws ConflictException for duplicate email', async () => {
|
|
245
|
+
repo.findOneBy.mockResolvedValue(existingUser)
|
|
246
|
+
await expect(service.create(dto)).rejects.toThrow(ConflictException)
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// E2E-test
|
|
251
|
+
describe('POST /users', () => {
|
|
252
|
+
it('creates a user', () => {
|
|
253
|
+
return request(app.getHttpServer())
|
|
254
|
+
.post('/users')
|
|
255
|
+
.send({ email: 'a@b.com', password: 'password123' })
|
|
256
|
+
.expect(201)
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Voorbeeld
|
|
262
|
+
|
|
263
|
+
**Gebruiker:** Voeg een `Products`-module toe aan een NestJS-app met TypeORM, inclusief CRUD-endpoints, admin-only delete beveiligd door een `RolesGuard`, en OpenAPI-documentatie.
|
|
264
|
+
|
|
265
|
+
**Verwachte output:**
|
|
266
|
+
- `products.entity.ts` — TypeORM-entiteit met `id` (UUID), `name`, `price` (decimal), `stock`, `createdAt`
|
|
267
|
+
- `dto/create-product.dto.ts` — class-validator DTO met `@ApiProperty`-decorators
|
|
268
|
+
- `products.service.ts` — CRUD-methoden met `Repository<Product>`, die `NotFoundException` gooit bij ontbrekende items
|
|
269
|
+
- `products.controller.ts` — alle CRUD-endpoints, `@UseGuards(JwtAuthGuard, RolesGuard)` + `@Roles(Role.Admin)` op `DELETE`
|
|
270
|
+
- `products.module.ts` — importeert `TypeOrmModule.forFeature([Product])`
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
> **Werk met ons:** Claudient wordt ondersteund door [Uitbreiden](https://uitbreiden.com/) — we bouwen AI-producten en B2B-oplossingen met ontwikkelaarsgemeenschappen. [uitbreiden.com](https://uitbreiden.com/)
|