@martinffx/atelier 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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/agents/architect.md +38 -0
- package/agents/oracle.md +40 -0
- package/agents/recon.md +39 -0
- package/dist/atelier.js +33082 -0
- package/package.json +44 -0
- package/skills/code-conventional-commit/SKILL.md +134 -0
- package/skills/code-conventional-commit/evals/evals.json +23 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/commit-message.txt +4 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/validation-result.txt +16 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/commit-message.txt +8 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/report.html +1325 -0
- package/skills/code-debug/SKILL.md +271 -0
- package/skills/code-debug/references/language-tools.md +139 -0
- package/skills/code-debug/references/techniques.md +117 -0
- package/skills/code-docs/SKILL.md +171 -0
- package/skills/code-docs/references/api-docs.md +116 -0
- package/skills/code-docs/references/changelog.md +64 -0
- package/skills/code-docs/references/readme-structure.md +94 -0
- package/skills/code-git-worktrees/SKILL.md +218 -0
- package/skills/code-perf/SKILL.md +163 -0
- package/skills/code-perf/references/common-bottlenecks.md +124 -0
- package/skills/code-perf/references/optimization-patterns.md +167 -0
- package/skills/code-perf/references/profiling-tools.md +126 -0
- package/skills/code-review/SKILL.md +78 -0
- package/skills/code-review/references/output.md +200 -0
- package/skills/code-review/references/reviewers.md +719 -0
- package/skills/code-review/references/rq.md +357 -0
- package/skills/code-review/references/rs.md +30 -0
- package/skills/code-security/SKILL.md +183 -0
- package/skills/code-security/references/owasp-top-10.md +129 -0
- package/skills/code-security/references/security-tools.md +102 -0
- package/skills/code-security/references/vulnerability-patterns.md +129 -0
- package/skills/code-stacked-commit/SKILL.md +155 -0
- package/skills/code-subagents/SKILL.md +184 -0
- package/skills/code-subagents/references/code-quality-reviewer-prompt.md +48 -0
- package/skills/code-subagents/references/implementor-prompt.md +79 -0
- package/skills/code-subagents/references/spec-reviewer-prompt.md +62 -0
- package/skills/oracle-architect/SKILL.md +246 -0
- package/skills/oracle-architect/references/api-design.md +345 -0
- package/skills/oracle-architect/references/data-modeling.md +318 -0
- package/skills/oracle-architect/references/ddd-patterns.md +247 -0
- package/skills/oracle-challenge/SKILL.md +126 -0
- package/skills/oracle-testing/SKILL.md +237 -0
- package/skills/oracle-testing/references/anti-patterns.md +618 -0
- package/skills/oracle-testing/references/boundaries.md +579 -0
- package/skills/oracle-testing/references/core-testing.md +691 -0
- package/skills/oracle-testing/references/edge-testing.md +794 -0
- package/skills/oracle-testing/references/stub-driven-tdd.md +322 -0
- package/skills/oracle-thinkdeep/SKILL.md +164 -0
- package/skills/python-architecture/SKILL.md +148 -0
- package/skills/python-architecture/references/data-modeling.md +558 -0
- package/skills/python-architecture/references/ddd.md +569 -0
- package/skills/python-architecture/references/functional-core.md +337 -0
- package/skills/python-build-tools/SKILL.md +153 -0
- package/skills/python-build-tools/references/basedpyright.md +321 -0
- package/skills/python-build-tools/references/ruff.md +225 -0
- package/skills/python-build-tools/references/uv.md +257 -0
- package/skills/python-fastapi/SKILL.md +184 -0
- package/skills/python-fastapi/references/api-design.md +563 -0
- package/skills/python-fastapi/references/dependencies.md +479 -0
- package/skills/python-fastapi/references/middleware.md +475 -0
- package/skills/python-fastapi/references/validation.md +497 -0
- package/skills/python-modern-python/SKILL.md +201 -0
- package/skills/python-modern-python/references/async.md +76 -0
- package/skills/python-modern-python/references/pattern-matching.md +89 -0
- package/skills/python-modern-python/references/typing.md +96 -0
- package/skills/python-monorepo/SKILL.md +146 -0
- package/skills/python-monorepo/references/docker.md +515 -0
- package/skills/python-monorepo/references/namespace-packages.md +426 -0
- package/skills/python-monorepo/references/workspace-config.md +341 -0
- package/skills/python-sqlalchemy/SKILL.md +119 -0
- package/skills/python-sqlalchemy/references/async.md +51 -0
- package/skills/python-sqlalchemy/references/models.md +67 -0
- package/skills/python-sqlalchemy/references/queries.md +57 -0
- package/skills/python-temporal/SKILL.md +128 -0
- package/skills/python-temporal/references/patterns.md +87 -0
- package/skills/python-temporal/references/testing.md +54 -0
- package/skills/python-testing/SKILL.md +161 -0
- package/skills/python-testing/references/boundaries.md +449 -0
- package/skills/python-testing/references/mocking.md +449 -0
- package/skills/python-testing/references/pytest.md +524 -0
- package/skills/spec-brainstorm/SKILL.md +325 -0
- package/skills/spec-finish/SKILL.md +159 -0
- package/skills/spec-implement/SKILL.md +296 -0
- package/skills/spec-orchestrator/SKILL.md +156 -0
- package/skills/spec-plan/SKILL.md +248 -0
- package/skills/typescript-api-design/SKILL.md +396 -0
- package/skills/typescript-api-design/references/error-responses.md +469 -0
- package/skills/typescript-build-tools/SKILL.md +447 -0
- package/skills/typescript-build-tools/references/biome.md +436 -0
- package/skills/typescript-build-tools/references/bun.md +432 -0
- package/skills/typescript-build-tools/references/turborepo.md +426 -0
- package/skills/typescript-build-tools/references/vitest.md +518 -0
- package/skills/typescript-drizzle-orm/SKILL.md +398 -0
- package/skills/typescript-drizzle-orm/references/cloudflare.md +392 -0
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +412 -0
- package/skills/typescript-drizzle-orm/references/postgresql.md +191 -0
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +479 -0
- package/skills/typescript-drizzle-orm/references/sqlite.md +291 -0
- package/skills/typescript-dynamodb-toolbox/SKILL.md +370 -0
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +574 -0
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +495 -0
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +332 -0
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +631 -0
- package/skills/typescript-dynamodb-toolbox/references/testing.md +718 -0
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +423 -0
- package/skills/typescript-effect-ts/SKILL.md +341 -0
- package/skills/typescript-effect-ts/references/core.md +412 -0
- package/skills/typescript-effect-ts/references/error-handling.md +468 -0
- package/skills/typescript-effect-ts/references/resources.md +476 -0
- package/skills/typescript-effect-ts/references/schema.md +536 -0
- package/skills/typescript-effect-ts/references/services.md +461 -0
- package/skills/typescript-fastify/SKILL.md +405 -0
- package/skills/typescript-fastify/references/plugins.md +329 -0
- package/skills/typescript-fastify/references/typeid.md +315 -0
- package/skills/typescript-functional-patterns/SKILL.md +402 -0
- package/skills/typescript-functional-patterns/references/adts.md +497 -0
- package/skills/typescript-functional-patterns/references/branded-types.md +542 -0
- package/skills/typescript-functional-patterns/references/migration-guide.md +695 -0
- package/skills/typescript-functional-patterns/references/option-result.md +585 -0
- package/skills/typescript-testing/SKILL.md +353 -0
- package/skills/typescript-testing/references/mocking.md +675 -0
- package/skills/typescript-testing/references/msw.md +852 -0
- package/skills/typescript-testing/references/snapshot-testing.md +774 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# Error Response Patterns
|
|
2
|
+
|
|
3
|
+
Comprehensive error handling patterns using RFC 7807 Problem Details standard.
|
|
4
|
+
|
|
5
|
+
## Error Class Hierarchy
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
type ErrorContext = {
|
|
9
|
+
[key: string]: unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Base error class
|
|
13
|
+
abstract class AppError extends Error {
|
|
14
|
+
abstract readonly status: number
|
|
15
|
+
abstract readonly type: string
|
|
16
|
+
|
|
17
|
+
constructor(message: string, public readonly context?: ErrorContext) {
|
|
18
|
+
super(message)
|
|
19
|
+
this.name = this.constructor.name
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toResponse(instance: string, traceId: string): ProblemDetail {
|
|
23
|
+
return {
|
|
24
|
+
type: this.type,
|
|
25
|
+
status: this.status,
|
|
26
|
+
title: this.name.replace(/Error$/, '').replace(/([A-Z])/g, ' $1').trim(),
|
|
27
|
+
detail: this.message,
|
|
28
|
+
instance,
|
|
29
|
+
traceId,
|
|
30
|
+
...this.context,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 4xx Client Errors
|
|
36
|
+
class BadRequestError extends AppError {
|
|
37
|
+
readonly status = 400
|
|
38
|
+
readonly type = 'BAD_REQUEST'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class UnauthorizedError extends AppError {
|
|
42
|
+
readonly status = 401
|
|
43
|
+
readonly type = 'UNAUTHORIZED'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class ForbiddenError extends AppError {
|
|
47
|
+
readonly status = 403
|
|
48
|
+
readonly type = 'FORBIDDEN'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class NotFoundError extends AppError {
|
|
52
|
+
readonly status = 404
|
|
53
|
+
readonly type = 'NOT_FOUND'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class ConflictError extends AppError {
|
|
57
|
+
readonly status = 409
|
|
58
|
+
readonly type = 'CONFLICT'
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
message: string,
|
|
62
|
+
public readonly retryable: boolean = false,
|
|
63
|
+
context?: ErrorContext
|
|
64
|
+
) {
|
|
65
|
+
super(message, context)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class ValidationError extends AppError {
|
|
70
|
+
readonly status = 422
|
|
71
|
+
readonly type = 'VALIDATION_ERROR'
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
message: string,
|
|
75
|
+
public readonly errors: Array<{ field: string; message: string }>,
|
|
76
|
+
context?: ErrorContext
|
|
77
|
+
) {
|
|
78
|
+
super(message, { ...context, errors })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class TooManyRequestsError extends AppError {
|
|
83
|
+
readonly status = 429
|
|
84
|
+
readonly type = 'TOO_MANY_REQUESTS'
|
|
85
|
+
|
|
86
|
+
constructor(
|
|
87
|
+
message: string,
|
|
88
|
+
public readonly retryAfter: number, // seconds
|
|
89
|
+
context?: ErrorContext
|
|
90
|
+
) {
|
|
91
|
+
super(message, { ...context, retryAfter })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 5xx Server Errors
|
|
96
|
+
class InternalServerError extends AppError {
|
|
97
|
+
readonly status = 500
|
|
98
|
+
readonly type = 'INTERNAL_SERVER_ERROR'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class ServiceUnavailableError extends AppError {
|
|
102
|
+
readonly status = 503
|
|
103
|
+
readonly type = 'SERVICE_UNAVAILABLE'
|
|
104
|
+
|
|
105
|
+
constructor(
|
|
106
|
+
message: string,
|
|
107
|
+
public readonly retryable: boolean = true,
|
|
108
|
+
context?: ErrorContext
|
|
109
|
+
) {
|
|
110
|
+
super(message, { ...context, retryable })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## TypeBox Schemas
|
|
116
|
+
|
|
117
|
+
Define error response schemas for OpenAPI:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { Type } from '@sinclair/typebox'
|
|
121
|
+
|
|
122
|
+
// Base ProblemDetail
|
|
123
|
+
const ProblemDetail = Type.Object({
|
|
124
|
+
type: Type.String({ description: 'Error type identifier' }),
|
|
125
|
+
status: Type.Number({ description: 'HTTP status code' }),
|
|
126
|
+
title: Type.String({ description: 'Short, human-readable summary' }),
|
|
127
|
+
detail: Type.String({ description: 'Specific explanation' }),
|
|
128
|
+
instance: Type.String({ description: 'URI to this error occurrence' }),
|
|
129
|
+
traceId: Type.String({ description: 'Request trace ID' }),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Specific error responses
|
|
133
|
+
export const BadRequestErrorResponse = Type.Composite([
|
|
134
|
+
ProblemDetail,
|
|
135
|
+
Type.Object({
|
|
136
|
+
type: Type.Literal('BAD_REQUEST'),
|
|
137
|
+
status: Type.Literal(400),
|
|
138
|
+
}),
|
|
139
|
+
], { $id: 'BadRequestErrorResponse' })
|
|
140
|
+
|
|
141
|
+
export const UnauthorizedErrorResponse = Type.Composite([
|
|
142
|
+
ProblemDetail,
|
|
143
|
+
Type.Object({
|
|
144
|
+
type: Type.Literal('UNAUTHORIZED'),
|
|
145
|
+
status: Type.Literal(401),
|
|
146
|
+
}),
|
|
147
|
+
], { $id: 'UnauthorizedErrorResponse' })
|
|
148
|
+
|
|
149
|
+
export const ForbiddenErrorResponse = Type.Composite([
|
|
150
|
+
ProblemDetail,
|
|
151
|
+
Type.Object({
|
|
152
|
+
type: Type.Literal('FORBIDDEN'),
|
|
153
|
+
status: Type.Literal(403),
|
|
154
|
+
}),
|
|
155
|
+
], { $id: 'ForbiddenErrorResponse' })
|
|
156
|
+
|
|
157
|
+
export const NotFoundErrorResponse = Type.Composite([
|
|
158
|
+
ProblemDetail,
|
|
159
|
+
Type.Object({
|
|
160
|
+
type: Type.Literal('NOT_FOUND'),
|
|
161
|
+
status: Type.Literal(404),
|
|
162
|
+
}),
|
|
163
|
+
], { $id: 'NotFoundErrorResponse' })
|
|
164
|
+
|
|
165
|
+
export const ConflictErrorResponse = Type.Composite([
|
|
166
|
+
ProblemDetail,
|
|
167
|
+
Type.Object({
|
|
168
|
+
type: Type.Literal('CONFLICT'),
|
|
169
|
+
status: Type.Literal(409),
|
|
170
|
+
retryable: Type.Optional(Type.Boolean()),
|
|
171
|
+
}),
|
|
172
|
+
], { $id: 'ConflictErrorResponse' })
|
|
173
|
+
|
|
174
|
+
export const ValidationErrorResponse = Type.Composite([
|
|
175
|
+
ProblemDetail,
|
|
176
|
+
Type.Object({
|
|
177
|
+
type: Type.Literal('VALIDATION_ERROR'),
|
|
178
|
+
status: Type.Literal(422),
|
|
179
|
+
errors: Type.Array(Type.Object({
|
|
180
|
+
field: Type.String(),
|
|
181
|
+
message: Type.String(),
|
|
182
|
+
})),
|
|
183
|
+
}),
|
|
184
|
+
], { $id: 'ValidationErrorResponse' })
|
|
185
|
+
|
|
186
|
+
export const TooManyRequestsErrorResponse = Type.Composite([
|
|
187
|
+
ProblemDetail,
|
|
188
|
+
Type.Object({
|
|
189
|
+
type: Type.Literal('TOO_MANY_REQUESTS'),
|
|
190
|
+
status: Type.Literal(429),
|
|
191
|
+
retryAfter: Type.Number({ description: 'Seconds until retry allowed' }),
|
|
192
|
+
}),
|
|
193
|
+
], { $id: 'TooManyRequestsErrorResponse' })
|
|
194
|
+
|
|
195
|
+
export const InternalServerErrorResponse = Type.Composite([
|
|
196
|
+
ProblemDetail,
|
|
197
|
+
Type.Object({
|
|
198
|
+
type: Type.Literal('INTERNAL_SERVER_ERROR'),
|
|
199
|
+
status: Type.Literal(500),
|
|
200
|
+
}),
|
|
201
|
+
], { $id: 'InternalServerErrorResponse' })
|
|
202
|
+
|
|
203
|
+
export const ServiceUnavailableErrorResponse = Type.Composite([
|
|
204
|
+
ProblemDetail,
|
|
205
|
+
Type.Object({
|
|
206
|
+
type: Type.Literal('SERVICE_UNAVAILABLE'),
|
|
207
|
+
status: Type.Literal(503),
|
|
208
|
+
retryable: Type.Optional(Type.Boolean()),
|
|
209
|
+
}),
|
|
210
|
+
], { $id: 'ServiceUnavailableErrorResponse' })
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Global Error Handler (Fastify)
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import type { FastifyError, FastifyRequest, FastifyReply } from 'fastify'
|
|
217
|
+
|
|
218
|
+
const globalErrorHandler = (
|
|
219
|
+
error: FastifyError,
|
|
220
|
+
request: FastifyRequest,
|
|
221
|
+
reply: FastifyReply
|
|
222
|
+
) => {
|
|
223
|
+
// Handle Fastify validation errors
|
|
224
|
+
if (error.code === 'FST_ERR_VALIDATION') {
|
|
225
|
+
return reply.status(400).send({
|
|
226
|
+
type: 'BAD_REQUEST',
|
|
227
|
+
status: 400,
|
|
228
|
+
title: 'Bad Request',
|
|
229
|
+
detail: error.message,
|
|
230
|
+
instance: request.url,
|
|
231
|
+
traceId: request.id,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Handle domain errors
|
|
236
|
+
if (error instanceof AppError) {
|
|
237
|
+
const response = error.toResponse(request.url, request.id)
|
|
238
|
+
|
|
239
|
+
// Add Retry-After header for 429 and 503
|
|
240
|
+
if (error instanceof TooManyRequestsError) {
|
|
241
|
+
reply.header('Retry-After', error.retryAfter.toString())
|
|
242
|
+
}
|
|
243
|
+
if (error instanceof ServiceUnavailableError && error.retryable) {
|
|
244
|
+
reply.header('Retry-After', '60') // Default 60 seconds
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return reply.status(error.status).send(response)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Unknown error - log and return 500
|
|
251
|
+
request.log.error({ err: error }, 'Unhandled error')
|
|
252
|
+
return reply.status(500).send({
|
|
253
|
+
type: 'INTERNAL_SERVER_ERROR',
|
|
254
|
+
status: 500,
|
|
255
|
+
title: 'Internal Server Error',
|
|
256
|
+
detail: 'An unexpected error occurred',
|
|
257
|
+
instance: request.url,
|
|
258
|
+
traceId: request.id,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
app.setErrorHandler(globalErrorHandler)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Usage Examples
|
|
266
|
+
|
|
267
|
+
### Not Found
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
async function getUser(userId: string): Promise<UserEntity> {
|
|
271
|
+
const record = await db.query.users.findFirst({
|
|
272
|
+
where: eq(users.id, userId),
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
if (!record) {
|
|
276
|
+
throw new NotFoundError(
|
|
277
|
+
`User with ID ${userId} not found`,
|
|
278
|
+
{ userId }
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return UserEntity.fromRecord(record)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Response
|
|
286
|
+
{
|
|
287
|
+
"type": "NOT_FOUND",
|
|
288
|
+
"status": 404,
|
|
289
|
+
"title": "Not Found",
|
|
290
|
+
"detail": "User with ID usr_abc123 not found",
|
|
291
|
+
"instance": "/api/v1/users/usr_abc123",
|
|
292
|
+
"traceId": "req_xyz789",
|
|
293
|
+
"userId": "usr_abc123"
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Conflict with Retryable Flag
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
async function updateAccount(entity: AccountEntity): Promise<AccountEntity> {
|
|
301
|
+
const result = await db.update(accounts)
|
|
302
|
+
.set(entity.toRecord())
|
|
303
|
+
.where(and(
|
|
304
|
+
eq(accounts.id, entity.id),
|
|
305
|
+
eq(accounts.lockVersion, entity.lockVersion)
|
|
306
|
+
))
|
|
307
|
+
.returning()
|
|
308
|
+
|
|
309
|
+
if (result.length === 0) {
|
|
310
|
+
throw new ConflictError(
|
|
311
|
+
'Account was modified by another transaction',
|
|
312
|
+
true, // retryable
|
|
313
|
+
{ accountId: entity.id, lockVersion: entity.lockVersion }
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return AccountEntity.fromRecord(result[0])
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Response
|
|
321
|
+
{
|
|
322
|
+
"type": "CONFLICT",
|
|
323
|
+
"status": 409,
|
|
324
|
+
"title": "Conflict",
|
|
325
|
+
"detail": "Account was modified by another transaction",
|
|
326
|
+
"instance": "/api/v1/accounts/acc_abc123",
|
|
327
|
+
"traceId": "req_xyz789",
|
|
328
|
+
"retryable": true,
|
|
329
|
+
"accountId": "acc_abc123",
|
|
330
|
+
"lockVersion": 5
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Validation Error
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
async function createUser(rq: CreateUserRequest): Promise<UserEntity> {
|
|
338
|
+
const errors: Array<{ field: string; message: string }> = []
|
|
339
|
+
|
|
340
|
+
if (!rq.name || rq.name.length < 2) {
|
|
341
|
+
errors.push({ field: 'name', message: 'Name must be at least 2 characters' })
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!rq.email || !rq.email.includes('@')) {
|
|
345
|
+
errors.push({ field: 'email', message: 'Invalid email format' })
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (errors.length > 0) {
|
|
349
|
+
throw new ValidationError('Invalid user data', errors)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Continue with creation...
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Response
|
|
356
|
+
{
|
|
357
|
+
"type": "VALIDATION_ERROR",
|
|
358
|
+
"status": 422,
|
|
359
|
+
"title": "Validation Error",
|
|
360
|
+
"detail": "Invalid user data",
|
|
361
|
+
"instance": "/api/v1/users",
|
|
362
|
+
"traceId": "req_xyz789",
|
|
363
|
+
"errors": [
|
|
364
|
+
{ "field": "name", "message": "Name must be at least 2 characters" },
|
|
365
|
+
{ "field": "email", "message": "Invalid email format" }
|
|
366
|
+
]
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Forbidden (Missing Permission)
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
async function deleteUser(userId: string, token: Token): Promise<void> {
|
|
374
|
+
if (!token.permissions.includes('user:delete')) {
|
|
375
|
+
throw new ForbiddenError(
|
|
376
|
+
'Missing permission: user:delete',
|
|
377
|
+
{ userId, requiredPermission: 'user:delete' }
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
await userRepo.delete(userId)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Response
|
|
385
|
+
{
|
|
386
|
+
"type": "FORBIDDEN",
|
|
387
|
+
"status": 403,
|
|
388
|
+
"title": "Forbidden",
|
|
389
|
+
"detail": "Missing permission: user:delete",
|
|
390
|
+
"instance": "/api/v1/users/usr_abc123",
|
|
391
|
+
"traceId": "req_xyz789",
|
|
392
|
+
"userId": "usr_abc123",
|
|
393
|
+
"requiredPermission": "user:delete"
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Rate Limit
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
async function checkRateLimit(userId: string): Promise<void> {
|
|
401
|
+
const limit = await rateLimiter.check(userId)
|
|
402
|
+
|
|
403
|
+
if (limit.exceeded) {
|
|
404
|
+
throw new TooManyRequestsError(
|
|
405
|
+
'Rate limit exceeded',
|
|
406
|
+
limit.resetIn, // seconds
|
|
407
|
+
{ userId, limit: limit.max, window: '1 minute' }
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Response with Retry-After header
|
|
413
|
+
{
|
|
414
|
+
"type": "TOO_MANY_REQUESTS",
|
|
415
|
+
"status": 429,
|
|
416
|
+
"title": "Too Many Requests",
|
|
417
|
+
"detail": "Rate limit exceeded",
|
|
418
|
+
"instance": "/api/v1/posts",
|
|
419
|
+
"traceId": "req_xyz789",
|
|
420
|
+
"retryAfter": 45,
|
|
421
|
+
"userId": "usr_abc123",
|
|
422
|
+
"limit": 100,
|
|
423
|
+
"window": "1 minute"
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Service Unavailable (Database Error)
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
function handleDBError(error: unknown, context: ErrorContext): never {
|
|
431
|
+
const code = (error as { code?: string }).code
|
|
432
|
+
|
|
433
|
+
switch (code) {
|
|
434
|
+
case '40001': // serialization_failure
|
|
435
|
+
case 'OC000': // occ_conflict (AWS DSQL)
|
|
436
|
+
throw new ServiceUnavailableError(
|
|
437
|
+
'Transaction conflict - please retry',
|
|
438
|
+
true, // retryable
|
|
439
|
+
context
|
|
440
|
+
)
|
|
441
|
+
default:
|
|
442
|
+
throw new InternalServerError('Database error', context)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Response with Retry-After header
|
|
447
|
+
{
|
|
448
|
+
"type": "SERVICE_UNAVAILABLE",
|
|
449
|
+
"status": 503,
|
|
450
|
+
"title": "Service Unavailable",
|
|
451
|
+
"detail": "Transaction conflict - please retry",
|
|
452
|
+
"instance": "/api/v1/transactions",
|
|
453
|
+
"traceId": "req_xyz789",
|
|
454
|
+
"retryable": true
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Guidelines
|
|
459
|
+
|
|
460
|
+
1. **Use RFC 7807** - Standard ProblemDetail format for all errors
|
|
461
|
+
2. **Include context** - Add relevant IDs to error context for debugging
|
|
462
|
+
3. **Retryable flag** - Mark 409/503 errors as retryable when appropriate
|
|
463
|
+
4. **Validation arrays** - List all validation errors, not just the first one
|
|
464
|
+
5. **Retry-After header** - Include for 429 and 503 responses
|
|
465
|
+
6. **Log server errors** - Always log 500 errors with full stack trace
|
|
466
|
+
7. **Don't expose internals** - Keep error messages user-friendly, not technical
|
|
467
|
+
8. **Consistent types** - Use same error type strings across all services
|
|
468
|
+
9. **TraceId always** - Include request ID for correlating logs
|
|
469
|
+
10. **TypeBox schemas** - Define error response schemas for OpenAPI documentation
|