@neyugn/agent-kits 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 +514 -0
- package/README.vi.md +410 -0
- package/README.zh.md +410 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +422 -0
- package/kits/coder/ARCHITECTURE.md +289 -0
- package/kits/coder/agents/ai-engineer.md +344 -0
- package/kits/coder/agents/backend-specialist.md +270 -0
- package/kits/coder/agents/cloud-architect.md +363 -0
- package/kits/coder/agents/code-reviewer.md +284 -0
- package/kits/coder/agents/data-engineer.md +401 -0
- package/kits/coder/agents/database-specialist.md +251 -0
- package/kits/coder/agents/debugger.md +209 -0
- package/kits/coder/agents/devops-engineer.md +281 -0
- package/kits/coder/agents/documentation-writer.md +296 -0
- package/kits/coder/agents/frontend-specialist.md +298 -0
- package/kits/coder/agents/i18n-specialist.md +348 -0
- package/kits/coder/agents/integration-specialist.md +314 -0
- package/kits/coder/agents/mobile-developer.md +271 -0
- package/kits/coder/agents/multi-tenant-architect.md +281 -0
- package/kits/coder/agents/orchestrator.md +263 -0
- package/kits/coder/agents/performance-analyst.md +327 -0
- package/kits/coder/agents/project-planner.md +277 -0
- package/kits/coder/agents/queue-specialist.md +282 -0
- package/kits/coder/agents/realtime-specialist.md +267 -0
- package/kits/coder/agents/security-auditor.md +253 -0
- package/kits/coder/agents/test-engineer.md +315 -0
- package/kits/coder/agents/ux-researcher.md +388 -0
- package/kits/coder/rules/.cursorrules +287 -0
- package/kits/coder/rules/CLAUDE.md +287 -0
- package/kits/coder/rules/CODEX.md +287 -0
- package/kits/coder/rules/GEMINI.md +287 -0
- package/kits/coder/scripts/checklist.py +318 -0
- package/kits/coder/scripts/kit_status.py +292 -0
- package/kits/coder/scripts/skills_manager.py +243 -0
- package/kits/coder/scripts/verify_all.py +391 -0
- package/kits/coder/skills/accessibility-patterns/SKILL.md +372 -0
- package/kits/coder/skills/accessibility-patterns/scripts/a11y_checker.py +211 -0
- package/kits/coder/skills/ai-rag-patterns/SKILL.md +444 -0
- package/kits/coder/skills/api-patterns/SKILL.md +316 -0
- package/kits/coder/skills/api-patterns/assets/.gitkeep +1 -0
- package/kits/coder/skills/api-patterns/references/deep-dive.md +21 -0
- package/kits/coder/skills/api-patterns/scripts/api_validator.py +253 -0
- package/kits/coder/skills/api-patterns/scripts/validate.py +56 -0
- package/kits/coder/skills/auth-patterns/SKILL.md +267 -0
- package/kits/coder/skills/aws-patterns/SKILL.md +576 -0
- package/kits/coder/skills/brainstorming/SKILL.md +370 -0
- package/kits/coder/skills/brainstorming/assets/.gitkeep +1 -0
- package/kits/coder/skills/brainstorming/references/deep-dive.md +21 -0
- package/kits/coder/skills/brainstorming/scripts/validate.py +56 -0
- package/kits/coder/skills/clean-code/SKILL.md +240 -0
- package/kits/coder/skills/clean-code/assets/.gitkeep +1 -0
- package/kits/coder/skills/clean-code/references/deep-dive.md +21 -0
- package/kits/coder/skills/clean-code/scripts/lint_runner.py +186 -0
- package/kits/coder/skills/clean-code/scripts/validate.py +56 -0
- package/kits/coder/skills/database-design/SKILL.md +255 -0
- package/kits/coder/skills/database-design/assets/.gitkeep +1 -0
- package/kits/coder/skills/database-design/references/deep-dive.md +21 -0
- package/kits/coder/skills/database-design/scripts/schema_validator.py +272 -0
- package/kits/coder/skills/database-design/scripts/validate.py +56 -0
- package/kits/coder/skills/docker-patterns/SKILL.md +240 -0
- package/kits/coder/skills/documentation-templates/SKILL.md +441 -0
- package/kits/coder/skills/e2e-testing/SKILL.md +457 -0
- package/kits/coder/skills/flutter-patterns/SKILL.md +330 -0
- package/kits/coder/skills/frontend-design/SKILL.md +127 -0
- package/kits/coder/skills/github-actions/SKILL.md +349 -0
- package/kits/coder/skills/gitlab-ci-patterns/SKILL.md +466 -0
- package/kits/coder/skills/graphql-patterns/SKILL.md +558 -0
- package/kits/coder/skills/i18n-localization/SKILL.md +345 -0
- package/kits/coder/skills/i18n-localization/scripts/i18n_checker.py +267 -0
- package/kits/coder/skills/kubernetes-patterns/SKILL.md +357 -0
- package/kits/coder/skills/mermaid-diagrams/SKILL.md +351 -0
- package/kits/coder/skills/mobile-design/SKILL.md +305 -0
- package/kits/coder/skills/monitoring-observability/SKILL.md +458 -0
- package/kits/coder/skills/multi-tenancy/SKILL.md +317 -0
- package/kits/coder/skills/multi-tenancy/assets/.gitkeep +1 -0
- package/kits/coder/skills/multi-tenancy/references/deep-dive.md +21 -0
- package/kits/coder/skills/multi-tenancy/scripts/validate.py +56 -0
- package/kits/coder/skills/nodejs-best-practices/SKILL.md +220 -0
- package/kits/coder/skills/performance-profiling/SKILL.md +333 -0
- package/kits/coder/skills/performance-profiling/assets/.gitkeep +1 -0
- package/kits/coder/skills/performance-profiling/references/deep-dive.md +21 -0
- package/kits/coder/skills/performance-profiling/scripts/validate.py +56 -0
- package/kits/coder/skills/plan-writing/SKILL.md +360 -0
- package/kits/coder/skills/plan-writing/assets/.gitkeep +1 -0
- package/kits/coder/skills/plan-writing/references/deep-dive.md +21 -0
- package/kits/coder/skills/plan-writing/scripts/validate.py +56 -0
- package/kits/coder/skills/postgres-patterns/SKILL.md +361 -0
- package/kits/coder/skills/prompt-engineering/SKILL.md +277 -0
- package/kits/coder/skills/queue-patterns/SKILL.md +359 -0
- package/kits/coder/skills/queue-patterns/assets/.gitkeep +1 -0
- package/kits/coder/skills/queue-patterns/references/deep-dive.md +21 -0
- package/kits/coder/skills/queue-patterns/scripts/validate.py +56 -0
- package/kits/coder/skills/react-native-patterns/SKILL.md +393 -0
- package/kits/coder/skills/react-patterns/SKILL.md +319 -0
- package/kits/coder/skills/realtime-patterns/SKILL.md +506 -0
- package/kits/coder/skills/realtime-patterns/assets/.gitkeep +1 -0
- package/kits/coder/skills/realtime-patterns/references/deep-dive.md +21 -0
- package/kits/coder/skills/realtime-patterns/scripts/validate.py +56 -0
- package/kits/coder/skills/redis-patterns/SKILL.md +484 -0
- package/kits/coder/skills/security-fundamentals/SKILL.md +363 -0
- package/kits/coder/skills/security-fundamentals/assets/.gitkeep +1 -0
- package/kits/coder/skills/security-fundamentals/references/deep-dive.md +21 -0
- package/kits/coder/skills/security-fundamentals/scripts/security_scan.py +326 -0
- package/kits/coder/skills/security-fundamentals/scripts/validate.py +56 -0
- package/kits/coder/skills/seo-patterns/SKILL.md +262 -0
- package/kits/coder/skills/seo-patterns/scripts/seo_checker.py +211 -0
- package/kits/coder/skills/systematic-debugging/SKILL.md +478 -0
- package/kits/coder/skills/systematic-debugging/assets/.gitkeep +1 -0
- package/kits/coder/skills/systematic-debugging/references/deep-dive.md +21 -0
- package/kits/coder/skills/systematic-debugging/scripts/validate.py +56 -0
- package/kits/coder/skills/tailwind-patterns/SKILL.md +395 -0
- package/kits/coder/skills/terraform-patterns/SKILL.md +470 -0
- package/kits/coder/skills/testing-patterns/SKILL.md +285 -0
- package/kits/coder/skills/testing-patterns/assets/.gitkeep +1 -0
- package/kits/coder/skills/testing-patterns/references/deep-dive.md +21 -0
- package/kits/coder/skills/testing-patterns/scripts/test_runner.py +219 -0
- package/kits/coder/skills/testing-patterns/scripts/validate.py +56 -0
- package/kits/coder/skills/typescript-patterns/SKILL.md +417 -0
- package/kits/coder/skills/ui-ux-pro-max/SKILL.md +364 -0
- package/kits/coder/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/kits/coder/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/kits/coder/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/kits/coder/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/kits/coder/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/kits/coder/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/kits/coder/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/kits/coder/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/kits/coder/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/kits/coder/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/kits/coder/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/kits/coder/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/kits/coder/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc +0 -0
- package/kits/coder/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc +0 -0
- package/kits/coder/skills/ui-ux-pro-max/scripts/core.py +257 -0
- package/kits/coder/skills/ui-ux-pro-max/scripts/design_system.py +488 -0
- package/kits/coder/skills/ui-ux-pro-max/scripts/search.py +76 -0
- package/kits/coder/workflows/.gitkeep +20 -0
- package/kits/coder/workflows/create.md +152 -0
- package/kits/coder/workflows/debug.md +223 -0
- package/kits/coder/workflows/deploy.md +283 -0
- package/kits/coder/workflows/orchestrate.md +243 -0
- package/kits/coder/workflows/plan.md +134 -0
- package/kits/coder/workflows/test.md +237 -0
- package/kits/coder/workflows/ui-ux-pro-max.md +109 -0
- package/package.json +49 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphql-patterns
|
|
3
|
+
description: GraphQL API design, schema patterns, and performance optimization. Use when designing GraphQL schemas, implementing resolvers, preventing N+1 queries, or building federated architectures.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Glob, Grep
|
|
5
|
+
version: 2.0
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GraphQL Patterns - API Design & Performance
|
|
9
|
+
|
|
10
|
+
> **Philosophy:** GraphQL is a contract, not just an API. The schema IS documentation. Design it carefully.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## When to Use This Skill
|
|
15
|
+
|
|
16
|
+
| ✅ Use | ❌ Don't Use |
|
|
17
|
+
| --------------------------- | -------------------------------- |
|
|
18
|
+
| Schema design | REST API design |
|
|
19
|
+
| Resolver implementation | Database queries (use ORM) |
|
|
20
|
+
| N+1 prevention (DataLoader) | Client-side caching (use Apollo) |
|
|
21
|
+
| Federation architecture | Simple CRUD APIs |
|
|
22
|
+
| Real-time subscriptions | File uploads as primary use |
|
|
23
|
+
| Query optimization | Rate limiting (use middleware) |
|
|
24
|
+
|
|
25
|
+
➡️ For REST patterns, see `api-patterns` skill.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Core Rules (Non-Negotiable)
|
|
30
|
+
|
|
31
|
+
1. **DataLoader mandatory** - Every resolver with DB calls needs DataLoader
|
|
32
|
+
2. **Depth limiting required** - Prevent deep query attacks
|
|
33
|
+
3. **Explicit nullability** - Design nullability intentionally
|
|
34
|
+
4. **Auth in resolvers** - Never rely on schema directives alone
|
|
35
|
+
5. **Disable introspection** - Production must disable introspection
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Schema Design Principles
|
|
40
|
+
|
|
41
|
+
### Type Design
|
|
42
|
+
|
|
43
|
+
```graphql
|
|
44
|
+
# ✅ Good: Explicit nullability, clear types
|
|
45
|
+
type User {
|
|
46
|
+
id: ID! # Always non-null
|
|
47
|
+
email: String! # Required field
|
|
48
|
+
name: String!
|
|
49
|
+
bio: String # Optional (nullable)
|
|
50
|
+
avatar: String
|
|
51
|
+
posts(first: Int): [Post!]! # List of non-null posts, list itself non-null
|
|
52
|
+
createdAt: DateTime!
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ✅ Good: Input types for mutations
|
|
56
|
+
input CreateUserInput {
|
|
57
|
+
email: String!
|
|
58
|
+
name: String!
|
|
59
|
+
bio: String
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
input UpdateUserInput {
|
|
63
|
+
name: String
|
|
64
|
+
bio: String
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Nullability Strategy
|
|
69
|
+
|
|
70
|
+
| Pattern | Meaning |
|
|
71
|
+
| ------------ | ----------------------------------- |
|
|
72
|
+
| `String` | May be null (optional field) |
|
|
73
|
+
| `String!` | Never null (required) |
|
|
74
|
+
| `[String]` | List may be null, items may be null |
|
|
75
|
+
| `[String]!` | List never null, items may be null |
|
|
76
|
+
| `[String!]` | List may be null, items never null |
|
|
77
|
+
| `[String!]!` | List never null, items never null |
|
|
78
|
+
|
|
79
|
+
**Recommendation:** Use `[Type!]!` for lists (empty list over null).
|
|
80
|
+
|
|
81
|
+
### Relay Connection Pattern
|
|
82
|
+
|
|
83
|
+
```graphql
|
|
84
|
+
type Query {
|
|
85
|
+
users(
|
|
86
|
+
first: Int
|
|
87
|
+
after: String
|
|
88
|
+
last: Int
|
|
89
|
+
before: String
|
|
90
|
+
filter: UserFilter
|
|
91
|
+
): UserConnection!
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type UserConnection {
|
|
95
|
+
edges: [UserEdge!]!
|
|
96
|
+
pageInfo: PageInfo!
|
|
97
|
+
totalCount: Int!
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type UserEdge {
|
|
101
|
+
cursor: String!
|
|
102
|
+
node: User!
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type PageInfo {
|
|
106
|
+
hasNextPage: Boolean!
|
|
107
|
+
hasPreviousPage: Boolean!
|
|
108
|
+
startCursor: String
|
|
109
|
+
endCursor: String
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## DataLoader Pattern (N+1 Prevention)
|
|
116
|
+
|
|
117
|
+
### The Problem
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// ❌ N+1: Each user triggers separate DB query
|
|
121
|
+
const resolvers = {
|
|
122
|
+
Post: {
|
|
123
|
+
author: (post) => db.users.findById(post.authorId), // N queries!
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### The Solution
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import DataLoader from "dataloader";
|
|
132
|
+
|
|
133
|
+
// Create loader (per-request)
|
|
134
|
+
function createLoaders() {
|
|
135
|
+
return {
|
|
136
|
+
userLoader: new DataLoader(async (userIds: string[]) => {
|
|
137
|
+
const users = await db.users.findByIds(userIds);
|
|
138
|
+
// MUST return in same order as input
|
|
139
|
+
return userIds.map((id) => users.find((u) => u.id === id) || null);
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
postsByUserLoader: new DataLoader(async (userIds: string[]) => {
|
|
143
|
+
const posts = await db.posts.findByUserIds(userIds);
|
|
144
|
+
// Group by userId
|
|
145
|
+
return userIds.map((id) => posts.filter((p) => p.authorId === id));
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Use in resolver
|
|
151
|
+
const resolvers = {
|
|
152
|
+
Post: {
|
|
153
|
+
author: (post, _, { loaders }) => loaders.userLoader.load(post.authorId),
|
|
154
|
+
},
|
|
155
|
+
User: {
|
|
156
|
+
posts: (user, _, { loaders }) => loaders.postsByUserLoader.load(user.id),
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Context creation
|
|
161
|
+
const context = ({ req }) => ({
|
|
162
|
+
loaders: createLoaders(),
|
|
163
|
+
user: getUserFromToken(req),
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### DataLoader Rules
|
|
168
|
+
|
|
169
|
+
1. **Create per-request** - New instance for each GraphQL request
|
|
170
|
+
2. **Batch must match order** - Return results in exact input order
|
|
171
|
+
3. **Handle missing items** - Return null for not-found
|
|
172
|
+
4. **Key uniqueness** - Convert to string if complex keys
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Query Optimization
|
|
177
|
+
|
|
178
|
+
### Depth Limiting
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import depthLimit from "graphql-depth-limit";
|
|
182
|
+
|
|
183
|
+
const server = new ApolloServer({
|
|
184
|
+
typeDefs,
|
|
185
|
+
resolvers,
|
|
186
|
+
validationRules: [depthLimit(10)], // Max 10 levels deep
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Query Complexity
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { createComplexityLimitRule } from "graphql-validation-complexity";
|
|
194
|
+
|
|
195
|
+
const complexityLimitRule = createComplexityLimitRule(1000, {
|
|
196
|
+
onCost: (cost) => console.log("Query cost:", cost),
|
|
197
|
+
scalarCost: 1,
|
|
198
|
+
objectCost: 2,
|
|
199
|
+
listFactor: 10,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const server = new ApolloServer({
|
|
203
|
+
validationRules: [complexityLimitRule],
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Field Cost Analysis
|
|
208
|
+
|
|
209
|
+
```graphql
|
|
210
|
+
type Query {
|
|
211
|
+
users(first: Int): [User!]! @cost(complexity: 10, multipliers: ["first"])
|
|
212
|
+
user(id: ID!): User @cost(complexity: 1)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
type User {
|
|
216
|
+
id: ID!
|
|
217
|
+
name: String!
|
|
218
|
+
posts: [Post!]! @cost(complexity: 5) # More expensive field
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Authorization Patterns
|
|
225
|
+
|
|
226
|
+
### Resolver-Level Auth (Recommended)
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
const resolvers = {
|
|
230
|
+
Query: {
|
|
231
|
+
user: async (_, { id }, { user }) => {
|
|
232
|
+
if (!user) throw new AuthenticationError("Not authenticated");
|
|
233
|
+
|
|
234
|
+
const targetUser = await db.users.findById(id);
|
|
235
|
+
|
|
236
|
+
// Authorization logic
|
|
237
|
+
if (user.role !== "admin" && user.id !== id) {
|
|
238
|
+
throw new ForbiddenError("Not authorized");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return targetUser;
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
User: {
|
|
246
|
+
email: (user, _, { currentUser }) => {
|
|
247
|
+
// Field-level authorization
|
|
248
|
+
if (currentUser?.id === user.id || currentUser?.role === "admin") {
|
|
249
|
+
return user.email;
|
|
250
|
+
}
|
|
251
|
+
return null; // Hide from unauthorized users
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Directive-Based Auth (Supplement, not replace)
|
|
258
|
+
|
|
259
|
+
```graphql
|
|
260
|
+
directive @auth(requires: Role = USER) on FIELD_DEFINITION
|
|
261
|
+
directive @owner on FIELD_DEFINITION
|
|
262
|
+
|
|
263
|
+
type Query {
|
|
264
|
+
users: [User!]! @auth(requires: ADMIN)
|
|
265
|
+
me: User @auth
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
type User {
|
|
269
|
+
id: ID!
|
|
270
|
+
name: String!
|
|
271
|
+
email: String! @owner # Only owner can see
|
|
272
|
+
secretField: String @auth(requires: ADMIN)
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Error Handling
|
|
279
|
+
|
|
280
|
+
### Structured Errors
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
class UserInputError extends Error {
|
|
284
|
+
constructor(
|
|
285
|
+
message: string,
|
|
286
|
+
public extensions: Record<string, any>,
|
|
287
|
+
) {
|
|
288
|
+
super(message);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const resolvers = {
|
|
293
|
+
Mutation: {
|
|
294
|
+
createUser: async (_, { input }) => {
|
|
295
|
+
const errors: Record<string, string> = {};
|
|
296
|
+
|
|
297
|
+
if (!isValidEmail(input.email)) {
|
|
298
|
+
errors.email = "Invalid email format";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (Object.keys(errors).length > 0) {
|
|
302
|
+
throw new UserInputError("Validation failed", {
|
|
303
|
+
validationErrors: errors,
|
|
304
|
+
code: "VALIDATION_ERROR",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return db.users.create(input);
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Union Types for Errors
|
|
315
|
+
|
|
316
|
+
```graphql
|
|
317
|
+
type Mutation {
|
|
318
|
+
createUser(input: CreateUserInput!): CreateUserResult!
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
union CreateUserResult = User | ValidationError | EmailTakenError
|
|
322
|
+
|
|
323
|
+
type ValidationError {
|
|
324
|
+
message: String!
|
|
325
|
+
field: String!
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
type EmailTakenError {
|
|
329
|
+
message: String!
|
|
330
|
+
suggestedEmail: String
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Subscriptions
|
|
337
|
+
|
|
338
|
+
### Basic Subscription
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { PubSub } from "graphql-subscriptions";
|
|
342
|
+
|
|
343
|
+
const pubsub = new PubSub();
|
|
344
|
+
|
|
345
|
+
const resolvers = {
|
|
346
|
+
Mutation: {
|
|
347
|
+
sendMessage: async (_, { input }, { user }) => {
|
|
348
|
+
const message = await db.messages.create({
|
|
349
|
+
...input,
|
|
350
|
+
authorId: user.id,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Publish event
|
|
354
|
+
pubsub.publish(`CHAT_${input.chatId}`, {
|
|
355
|
+
messageSent: message,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return message;
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
Subscription: {
|
|
363
|
+
messageSent: {
|
|
364
|
+
subscribe: (_, { chatId }, { user }) => {
|
|
365
|
+
// Auth check
|
|
366
|
+
if (!user) throw new AuthenticationError("Not authenticated");
|
|
367
|
+
|
|
368
|
+
return pubsub.asyncIterator(`CHAT_${chatId}`);
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Filtered Subscriptions
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
import { withFilter } from "graphql-subscriptions";
|
|
379
|
+
|
|
380
|
+
const resolvers = {
|
|
381
|
+
Subscription: {
|
|
382
|
+
messageSent: {
|
|
383
|
+
subscribe: withFilter(
|
|
384
|
+
() => pubsub.asyncIterator("MESSAGE_CREATED"),
|
|
385
|
+
(payload, variables, context) => {
|
|
386
|
+
// Only deliver to users in the chat
|
|
387
|
+
return (
|
|
388
|
+
payload.messageSent.chatId === variables.chatId &&
|
|
389
|
+
context.user.chats.includes(variables.chatId)
|
|
390
|
+
);
|
|
391
|
+
},
|
|
392
|
+
),
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Federation Pattern
|
|
401
|
+
|
|
402
|
+
### Subgraph Schema
|
|
403
|
+
|
|
404
|
+
```graphql
|
|
405
|
+
# Users Service
|
|
406
|
+
type User @key(fields: "id") {
|
|
407
|
+
id: ID!
|
|
408
|
+
name: String!
|
|
409
|
+
email: String!
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
extend type Query {
|
|
413
|
+
user(id: ID!): User
|
|
414
|
+
me: User
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
# Posts Service
|
|
418
|
+
type Post @key(fields: "id") {
|
|
419
|
+
id: ID!
|
|
420
|
+
title: String!
|
|
421
|
+
content: String!
|
|
422
|
+
author: User!
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# Extend User from Users Service
|
|
426
|
+
extend type User @key(fields: "id") {
|
|
427
|
+
id: ID! @external
|
|
428
|
+
posts: [Post!]!
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Reference Resolver
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// Posts service resolves User reference
|
|
436
|
+
const resolvers = {
|
|
437
|
+
User: {
|
|
438
|
+
__resolveReference: (user) => {
|
|
439
|
+
// user contains { id } from key
|
|
440
|
+
return { id: user.id }; // Minimal stub, Users service fills rest
|
|
441
|
+
},
|
|
442
|
+
posts: (user) => db.posts.findByAuthorId(user.id),
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Caching Strategies
|
|
450
|
+
|
|
451
|
+
### CDN/Response Caching
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
const resolvers = {
|
|
455
|
+
Query: {
|
|
456
|
+
// Public, cacheable
|
|
457
|
+
products: () => db.products.findAll(),
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
Product: {
|
|
461
|
+
__cacheControl: { maxAge: 3600, scope: "PUBLIC" },
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
User: {
|
|
465
|
+
__cacheControl: { maxAge: 0, scope: "PRIVATE" },
|
|
466
|
+
email: {
|
|
467
|
+
__cacheControl: { maxAge: 0 }, // Never cache sensitive data
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Automatic Persisted Queries (APQ)
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
import { ApolloServer } from "@apollo/server";
|
|
477
|
+
|
|
478
|
+
const server = new ApolloServer({
|
|
479
|
+
typeDefs,
|
|
480
|
+
resolvers,
|
|
481
|
+
persistedQueries: {
|
|
482
|
+
ttl: 900, // 15 minutes
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Production Security Checklist
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const server = new ApolloServer({
|
|
493
|
+
typeDefs,
|
|
494
|
+
resolvers,
|
|
495
|
+
|
|
496
|
+
// ✅ Disable introspection in production
|
|
497
|
+
introspection: process.env.NODE_ENV !== "production",
|
|
498
|
+
|
|
499
|
+
// ✅ Disable playground in production
|
|
500
|
+
playground: process.env.NODE_ENV !== "production",
|
|
501
|
+
|
|
502
|
+
// ✅ Validation rules
|
|
503
|
+
validationRules: [depthLimit(10), createComplexityLimitRule(1000)],
|
|
504
|
+
|
|
505
|
+
// ✅ Error formatting (hide internal details)
|
|
506
|
+
formatError: (error) => {
|
|
507
|
+
if (process.env.NODE_ENV === "production") {
|
|
508
|
+
return { message: error.message };
|
|
509
|
+
}
|
|
510
|
+
return error;
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Anti-Patterns
|
|
518
|
+
|
|
519
|
+
| ❌ Don't | ✅ Do |
|
|
520
|
+
| ------------------------------------- | ------------------------------------------- |
|
|
521
|
+
| Resolver makes DB call without loader | Use DataLoader for all DB access |
|
|
522
|
+
| All fields nullable | Design nullability intentionally |
|
|
523
|
+
| Auth only in directives | Auth in resolvers, directives as supplement |
|
|
524
|
+
| Introspection in production | Disable introspection |
|
|
525
|
+
| Deep unlimited queries | Depth limiting + complexity analysis |
|
|
526
|
+
| Return all errors as same type | Use union types for error variants |
|
|
527
|
+
| Create DataLoader once globally | Create per-request |
|
|
528
|
+
| Subscription without auth check | Check auth in subscribe function |
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Performance Checklist
|
|
533
|
+
|
|
534
|
+
Before production:
|
|
535
|
+
|
|
536
|
+
- [ ] DataLoader for all nested resolvers?
|
|
537
|
+
- [ ] Depth limit configured?
|
|
538
|
+
- [ ] Query complexity analysis?
|
|
539
|
+
- [ ] Introspection disabled?
|
|
540
|
+
- [ ] Error details hidden in production?
|
|
541
|
+
- [ ] Subscription auth implemented?
|
|
542
|
+
- [ ] Persisted queries enabled?
|
|
543
|
+
- [ ] Cache headers set appropriately?
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Related Skills
|
|
548
|
+
|
|
549
|
+
| Need | Skill |
|
|
550
|
+
| ---------------------- | --------------------- |
|
|
551
|
+
| REST API design | `api-patterns` |
|
|
552
|
+
| Database queries | `database-design` |
|
|
553
|
+
| Real-time patterns | `realtime-patterns` |
|
|
554
|
+
| TypeScript integration | `typescript-patterns` |
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
> **Remember:** GraphQL's flexibility is its power and danger. Protect your API with DataLoader, depth limits, and auth at every level.
|