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,307 @@
|
|
|
1
|
+
> 🇳🇱 Dit is de Nederlandse vertaling. [Engelse versie](../go.md).
|
|
2
|
+
|
|
3
|
+
# Go Skill
|
|
4
|
+
|
|
5
|
+
## Wanneer te activeren
|
|
6
|
+
- Een Go HTTP-service of CLI-tool bouwen
|
|
7
|
+
- Een Go-project structureren met `cmd/`, `internal/`, `pkg/`
|
|
8
|
+
- HTTP-handlers schrijven met `net/http`, chi of gin
|
|
9
|
+
- Interfaces, embedding en compositiepatronen implementeren
|
|
10
|
+
- Gelijktijdige code schrijven met goroutines, kanalen en `sync`-primitieven
|
|
11
|
+
- Foutafhandeling, inpakking en sentinaelfouten
|
|
12
|
+
- Tabelgestuurde tests schrijven
|
|
13
|
+
|
|
14
|
+
## Wanneer NIET te gebruiken
|
|
15
|
+
- Node.js-, Python- of andere taalservices — andere ecosystemen
|
|
16
|
+
- Protobuf/gRPC-services zonder voorafgaande gRPC-context
|
|
17
|
+
- Pure scripttaken — shell of Python passen beter
|
|
18
|
+
|
|
19
|
+
## Instructies
|
|
20
|
+
|
|
21
|
+
### Projectindeling
|
|
22
|
+
```
|
|
23
|
+
myapp/
|
|
24
|
+
├── cmd/
|
|
25
|
+
│ └── server/
|
|
26
|
+
│ └── main.go # Ingangspunt — slank, bedraadt en start alleen
|
|
27
|
+
├── internal/ # Private packages — niet van buitenaf importeerbaar
|
|
28
|
+
│ ├── config/
|
|
29
|
+
│ ├── handler/
|
|
30
|
+
│ ├── service/
|
|
31
|
+
│ └── store/
|
|
32
|
+
├── pkg/ # Publieke packages — importeerbaar door andere modules
|
|
33
|
+
│ └── apierr/
|
|
34
|
+
├── go.mod
|
|
35
|
+
└── go.sum
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### main.go — alleen bedrading
|
|
39
|
+
```go
|
|
40
|
+
// cmd/server/main.go
|
|
41
|
+
package main
|
|
42
|
+
|
|
43
|
+
import (
|
|
44
|
+
"context"
|
|
45
|
+
"log/slog"
|
|
46
|
+
"net/http"
|
|
47
|
+
"os"
|
|
48
|
+
"os/signal"
|
|
49
|
+
"syscall"
|
|
50
|
+
"time"
|
|
51
|
+
|
|
52
|
+
"myapp/internal/config"
|
|
53
|
+
"myapp/internal/handler"
|
|
54
|
+
"myapp/internal/store"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
func main() {
|
|
58
|
+
cfg := config.MustLoad()
|
|
59
|
+
db := store.MustConnect(cfg.DatabaseURL)
|
|
60
|
+
defer db.Close()
|
|
61
|
+
|
|
62
|
+
mux := handler.NewMux(db, cfg)
|
|
63
|
+
|
|
64
|
+
srv := &http.Server{
|
|
65
|
+
Addr: ":" + cfg.Port,
|
|
66
|
+
Handler: mux,
|
|
67
|
+
ReadTimeout: 10 * time.Second,
|
|
68
|
+
WriteTimeout: 30 * time.Second,
|
|
69
|
+
IdleTimeout: 60 * time.Second,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
go func() {
|
|
73
|
+
slog.Info("server starting", "addr", srv.Addr)
|
|
74
|
+
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
|
75
|
+
slog.Error("server error", "err", err)
|
|
76
|
+
os.Exit(1)
|
|
77
|
+
}
|
|
78
|
+
}()
|
|
79
|
+
|
|
80
|
+
quit := make(chan os.Signal, 1)
|
|
81
|
+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
82
|
+
<-quit
|
|
83
|
+
|
|
84
|
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
85
|
+
defer cancel()
|
|
86
|
+
srv.Shutdown(ctx)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Foutafhandeling
|
|
91
|
+
```go
|
|
92
|
+
// Definieer sentinaelfouten voor verwachte omstandigheden
|
|
93
|
+
var (
|
|
94
|
+
ErrNotFound = errors.New("not found")
|
|
95
|
+
ErrDuplicate = errors.New("duplicate entry")
|
|
96
|
+
ErrForbidden = errors.New("forbidden")
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
// Wikkel fouten in met context — gebruik %w om uitpakken te behouden
|
|
100
|
+
func (s *UserStore) GetByID(ctx context.Context, id string) (*User, error) {
|
|
101
|
+
var u User
|
|
102
|
+
err := s.db.QueryRowContext(ctx, `SELECT id, email FROM users WHERE id = $1`, id).
|
|
103
|
+
Scan(&u.ID, &u.Email)
|
|
104
|
+
if errors.Is(err, sql.ErrNoRows) {
|
|
105
|
+
return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
|
|
106
|
+
}
|
|
107
|
+
if err != nil {
|
|
108
|
+
return nil, fmt.Errorf("user store get: %w", err)
|
|
109
|
+
}
|
|
110
|
+
return &u, nil
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Aanroeper controleert gedrag, niet berichtstrings
|
|
114
|
+
user, err := store.GetByID(ctx, id)
|
|
115
|
+
if errors.Is(err, ErrNotFound) {
|
|
116
|
+
http.Error(w, "not found", http.StatusNotFound)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### HTTP-handlers met chi
|
|
122
|
+
```go
|
|
123
|
+
// internal/handler/users.go
|
|
124
|
+
type UserHandler struct {
|
|
125
|
+
svc UserService
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func (h *UserHandler) Register(r chi.Router) {
|
|
129
|
+
r.Get("/users/{id}", h.GetUser)
|
|
130
|
+
r.Post("/users", h.CreateUser)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
|
134
|
+
id := chi.URLParam(r, "id")
|
|
135
|
+
user, err := h.svc.GetUser(r.Context(), id)
|
|
136
|
+
if err != nil {
|
|
137
|
+
renderError(w, err)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
renderJSON(w, http.StatusOK, user)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func renderJSON(w http.ResponseWriter, code int, v any) {
|
|
144
|
+
w.Header().Set("Content-Type", "application/json")
|
|
145
|
+
w.WriteHeader(code)
|
|
146
|
+
json.NewEncoder(w).Encode(v)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func renderError(w http.ResponseWriter, err error) {
|
|
150
|
+
switch {
|
|
151
|
+
case errors.Is(err, ErrNotFound):
|
|
152
|
+
http.Error(w, "not found", http.StatusNotFound)
|
|
153
|
+
case errors.Is(err, ErrForbidden):
|
|
154
|
+
http.Error(w, "forbidden", http.StatusForbidden)
|
|
155
|
+
default:
|
|
156
|
+
slog.Error("internal error", "err", err)
|
|
157
|
+
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Interfaces
|
|
163
|
+
```go
|
|
164
|
+
// Definieer interfaces waar ze worden gebruikt (consumerkant), niet waar geïmplementeerd
|
|
165
|
+
// internal/handler/users.go
|
|
166
|
+
type UserService interface {
|
|
167
|
+
GetUser(ctx context.Context, id string) (*User, error)
|
|
168
|
+
CreateUser(ctx context.Context, req CreateUserRequest) (*User, error)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Houd interfaces klein — 1-3 methoden voorkeur
|
|
172
|
+
// io.Reader, io.Writer zijn de canonieke voorbeelden
|
|
173
|
+
|
|
174
|
+
// Embedding voor compositie
|
|
175
|
+
type ReadWriter interface {
|
|
176
|
+
io.Reader
|
|
177
|
+
io.Writer
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Gelijktijdigheidspatronen
|
|
182
|
+
```go
|
|
183
|
+
// Fan-out met errgroup
|
|
184
|
+
import "golang.org/x/sync/errgroup"
|
|
185
|
+
|
|
186
|
+
func fetchAll(ctx context.Context, ids []string) ([]*User, error) {
|
|
187
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
188
|
+
results := make([]*User, len(ids))
|
|
189
|
+
|
|
190
|
+
for i, id := range ids {
|
|
191
|
+
i, id := i, id // vang loopvariabelen
|
|
192
|
+
g.Go(func() error {
|
|
193
|
+
u, err := store.GetByID(ctx, id)
|
|
194
|
+
if err != nil {
|
|
195
|
+
return err
|
|
196
|
+
}
|
|
197
|
+
results[i] = u
|
|
198
|
+
return nil
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if err := g.Wait(); err != nil {
|
|
203
|
+
return nil, err
|
|
204
|
+
}
|
|
205
|
+
return results, nil
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Worker-pool
|
|
209
|
+
func processJobs(ctx context.Context, jobs <-chan Job) {
|
|
210
|
+
const workers = 10
|
|
211
|
+
var wg sync.WaitGroup
|
|
212
|
+
for range workers {
|
|
213
|
+
wg.Add(1)
|
|
214
|
+
go func() {
|
|
215
|
+
defer wg.Done()
|
|
216
|
+
for job := range jobs {
|
|
217
|
+
if err := process(ctx, job); err != nil {
|
|
218
|
+
slog.Error("job failed", "id", job.ID, "err", err)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}()
|
|
222
|
+
}
|
|
223
|
+
wg.Wait()
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Contextpropagatie
|
|
228
|
+
```go
|
|
229
|
+
// Geef context altijd door als eerste argument
|
|
230
|
+
func (s *Service) DoWork(ctx context.Context, id string) error {
|
|
231
|
+
// Geef ctx door aan alle downstream-aanroepen
|
|
232
|
+
user, err := s.store.GetByID(ctx, id)
|
|
233
|
+
if err != nil {
|
|
234
|
+
return err
|
|
235
|
+
}
|
|
236
|
+
return s.notifier.Send(ctx, user.Email)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sla aanvraag-scoped waarden op in context alleen voor cross-cutting concerns
|
|
240
|
+
// (trace-ID's, auth-info) — nooit voor optionele functieparameters
|
|
241
|
+
type contextKey string
|
|
242
|
+
const requestIDKey contextKey = "request_id"
|
|
243
|
+
|
|
244
|
+
func WithRequestID(ctx context.Context, id string) context.Context {
|
|
245
|
+
return context.WithValue(ctx, requestIDKey, id)
|
|
246
|
+
}
|
|
247
|
+
func GetRequestID(ctx context.Context) string {
|
|
248
|
+
id, _ := ctx.Value(requestIDKey).(string)
|
|
249
|
+
return id
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Tabelgestuurde tests
|
|
254
|
+
```go
|
|
255
|
+
func TestGetUser(t *testing.T) {
|
|
256
|
+
tests := []struct {
|
|
257
|
+
name string
|
|
258
|
+
id string
|
|
259
|
+
want *User
|
|
260
|
+
wantErr error
|
|
261
|
+
}{
|
|
262
|
+
{"found", "123", &User{ID: "123", Email: "a@b.com"}, nil},
|
|
263
|
+
{"not found", "999", nil, ErrNotFound},
|
|
264
|
+
{"empty id", "", nil, ErrNotFound},
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for _, tt := range tests {
|
|
268
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
269
|
+
svc := newTestService(t)
|
|
270
|
+
got, err := svc.GetUser(context.Background(), tt.id)
|
|
271
|
+
if !errors.Is(err, tt.wantErr) {
|
|
272
|
+
t.Fatalf("err = %v, want %v", err, tt.wantErr)
|
|
273
|
+
}
|
|
274
|
+
if diff := cmp.Diff(tt.want, got); diff != "" {
|
|
275
|
+
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Loggen met slog
|
|
283
|
+
```go
|
|
284
|
+
// Gestructureerde logging — nooit fmt.Println in productiecode
|
|
285
|
+
slog.Info("request completed", "method", r.Method, "path", r.URL.Path, "status", 200, "duration_ms", elapsed.Milliseconds())
|
|
286
|
+
slog.Error("db query failed", "query", "getUserByID", "err", err)
|
|
287
|
+
|
|
288
|
+
// JSON-logger instellen in main
|
|
289
|
+
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
290
|
+
Level: slog.LevelInfo,
|
|
291
|
+
}))
|
|
292
|
+
slog.SetDefault(logger)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Voorbeeld
|
|
296
|
+
|
|
297
|
+
**Gebruiker:** Bouw een Go HTTP API voor een takenlijst: aanmaken, ophalen en voltooien van taken. PostgreSQL-backend, chi-router, gestructureerde logging, gracieuze afsluiting.
|
|
298
|
+
|
|
299
|
+
**Verwachte output:**
|
|
300
|
+
- `cmd/server/main.go` — bedraadt store, handler, start server met gracieuze afsluiting
|
|
301
|
+
- `internal/store/task.go` — `TaskStore` met `Create`-, `List`-, `Complete`-methoden via `database/sql`
|
|
302
|
+
- `internal/handler/tasks.go` — `TaskHandler` met `Register(r chi.Router)`, JSON-antwoorden
|
|
303
|
+
- `internal/handler/tasks_test.go` — tabelgestuurde tests met een mock-store
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
> **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/)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
> 🇩🇪 Dies ist die deutsche Übersetzung. [Englische Version](../nestjs.md).
|
|
2
|
+
|
|
3
|
+
# NestJS Skill
|
|
4
|
+
|
|
5
|
+
## Wann aktivieren
|
|
6
|
+
- Eine NestJS-Anwendung bauen (Module, Controller, Services)
|
|
7
|
+
- Guards, Interceptors, Pipes und Exception Filter einrichten
|
|
8
|
+
- TypeORM oder Prisma mit NestJS integrieren
|
|
9
|
+
- CQRS mit Commands, Queries und Events implementieren
|
|
10
|
+
- Microservices einrichten (TCP-, Redis-, RabbitMQ-Transport)
|
|
11
|
+
- Unit- und E2E-Tests mit Jest und Supertest schreiben
|
|
12
|
+
- OpenAPI-Docs mit `@nestjs/swagger` generieren
|
|
13
|
+
|
|
14
|
+
## Wann NICHT verwenden
|
|
15
|
+
- Next.js API-Routes oder eigenständiges Fastify — anderes Framework
|
|
16
|
+
- Einfache Express-Skripte — NestJS-Overhead nicht gerechtfertigt
|
|
17
|
+
- Lambda-Funktionen, bei denen die Kaltstart-Zeit kritisch ist
|
|
18
|
+
|
|
19
|
+
## Anweisungen
|
|
20
|
+
|
|
21
|
+
### Modulstruktur
|
|
22
|
+
```
|
|
23
|
+
src/
|
|
24
|
+
├── app.module.ts # Root-Modul
|
|
25
|
+
├── main.ts # Bootstrap
|
|
26
|
+
├── common/
|
|
27
|
+
│ ├── decorators/
|
|
28
|
+
│ ├── filters/
|
|
29
|
+
│ ├── guards/
|
|
30
|
+
│ ├── interceptors/
|
|
31
|
+
│ └── pipes/
|
|
32
|
+
├── config/
|
|
33
|
+
│ └── configuration.ts # ConfigService-Setup
|
|
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, // unbekannte Felder entfernen
|
|
60
|
+
forbidNonWhitelisted: true,
|
|
61
|
+
transform: true, // Payloads automatisch in DTO-Klassen umwandeln
|
|
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
|
+
### Modul, 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
|
+
### DTOs mit 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
|
+
// Installation: @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
|
+
// Im Service/Controller:
|
|
223
|
+
const user = await this.commandBus.execute(new CreateUserCommand(dto))
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Tests
|
|
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
|
+
## Beispiel
|
|
262
|
+
|
|
263
|
+
**Benutzer:** Ein `Products`-Modul zu einer NestJS-App mit TypeORM hinzufügen, einschließlich CRUD-Endpunkte, nur-Admin-Löschen durch einen `RolesGuard` geschützt, und OpenAPI-Docs.
|
|
264
|
+
|
|
265
|
+
**Erwartete Ausgabe:**
|
|
266
|
+
- `products.entity.ts` — TypeORM-Entity mit `id` (UUID), `name`, `price` (decimal), `stock`, `createdAt`
|
|
267
|
+
- `dto/create-product.dto.ts` — class-validator DTO mit `@ApiProperty`-Dekoratoren
|
|
268
|
+
- `products.service.ts` — CRUD-Methoden mit `Repository<Product>`, `NotFoundException` bei fehlendem Element
|
|
269
|
+
- `products.controller.ts` — alle CRUD-Endpunkte, `@UseGuards(JwtAuthGuard, RolesGuard)` + `@Roles(Role.Admin)` bei `DELETE`
|
|
270
|
+
- `products.module.ts` — importiert `TypeOrmModule.forFeature([Product])`
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
> **Mit uns arbeiten:** Claudient wird von [Uitbreiden](https://uitbreiden.com/) unterstützt — wir bauen KI-Produkte und B2B-Lösungen mit Entwickler-Communities. [uitbreiden.com](https://uitbreiden.com/)
|