autoworkflow 3.1.5 → 3.6.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/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Angular Skill
|
|
2
|
+
|
|
3
|
+
## Component Structure
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'app-user-card',
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [CommonModule],
|
|
9
|
+
template: \`
|
|
10
|
+
<div class="card">
|
|
11
|
+
<h2>{{ user()?.name }}</h2>
|
|
12
|
+
<button (click)="increment()">Count: {{ count() }}</button>
|
|
13
|
+
<p>Double: {{ doubleCount() }}</p>
|
|
14
|
+
</div>
|
|
15
|
+
\`
|
|
16
|
+
})
|
|
17
|
+
export class UserCardComponent {
|
|
18
|
+
private userService = inject(UserService);
|
|
19
|
+
|
|
20
|
+
// Signals (Angular 16+)
|
|
21
|
+
count = signal(0);
|
|
22
|
+
user = signal<User | null>(null);
|
|
23
|
+
doubleCount = computed(() => this.count() * 2);
|
|
24
|
+
|
|
25
|
+
// Input signals (Angular 17+)
|
|
26
|
+
userId = input.required<string>();
|
|
27
|
+
initialCount = input(0);
|
|
28
|
+
|
|
29
|
+
// Output
|
|
30
|
+
countChange = output<number>();
|
|
31
|
+
|
|
32
|
+
increment() {
|
|
33
|
+
this.count.update(c => c + 1);
|
|
34
|
+
this.countChange.emit(this.count());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor() {
|
|
38
|
+
// Effect for side effects
|
|
39
|
+
effect(() => {
|
|
40
|
+
console.log('Count changed:', this.count());
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
## Services & Dependency Injection
|
|
47
|
+
\`\`\`typescript
|
|
48
|
+
// user.service.ts
|
|
49
|
+
@Injectable({ providedIn: 'root' })
|
|
50
|
+
export class UserService {
|
|
51
|
+
private http = inject(HttpClient);
|
|
52
|
+
private apiUrl = '/api/users';
|
|
53
|
+
|
|
54
|
+
getUsers(): Observable<User[]> {
|
|
55
|
+
return this.http.get<User[]>(this.apiUrl);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getUser(id: string): Observable<User> {
|
|
59
|
+
return this.http.get<User>(\`\${this.apiUrl}/\${id}\`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
createUser(user: CreateUserDto): Observable<User> {
|
|
63
|
+
return this.http.post<User>(this.apiUrl, user);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Usage in component
|
|
68
|
+
export class UsersComponent {
|
|
69
|
+
private userService = inject(UserService);
|
|
70
|
+
|
|
71
|
+
users = signal<User[]>([]);
|
|
72
|
+
loading = signal(true);
|
|
73
|
+
|
|
74
|
+
constructor() {
|
|
75
|
+
this.userService.getUsers().subscribe({
|
|
76
|
+
next: (users) => this.users.set(users),
|
|
77
|
+
complete: () => this.loading.set(false)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
\`\`\`
|
|
82
|
+
|
|
83
|
+
## Routing
|
|
84
|
+
\`\`\`typescript
|
|
85
|
+
// app.routes.ts
|
|
86
|
+
export const routes: Routes = [
|
|
87
|
+
{ path: '', component: HomeComponent },
|
|
88
|
+
{ path: 'users', component: UsersComponent },
|
|
89
|
+
{ path: 'users/:id', component: UserDetailComponent },
|
|
90
|
+
{
|
|
91
|
+
path: 'admin',
|
|
92
|
+
loadComponent: () => import('./admin/admin.component'),
|
|
93
|
+
canActivate: [authGuard],
|
|
94
|
+
children: [
|
|
95
|
+
{ path: 'dashboard', component: DashboardComponent }
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{ path: '**', component: NotFoundComponent }
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// Route guard
|
|
102
|
+
export const authGuard: CanActivateFn = (route, state) => {
|
|
103
|
+
const authService = inject(AuthService);
|
|
104
|
+
const router = inject(Router);
|
|
105
|
+
|
|
106
|
+
if (authService.isLoggedIn()) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return router.createUrlTree(['/login'], {
|
|
111
|
+
queryParams: { returnUrl: state.url }
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Accessing route params
|
|
116
|
+
export class UserDetailComponent {
|
|
117
|
+
private route = inject(ActivatedRoute);
|
|
118
|
+
|
|
119
|
+
userId = input.required<string>(); // With input binding
|
|
120
|
+
// Or from route snapshot:
|
|
121
|
+
// userId = this.route.snapshot.paramMap.get('id');
|
|
122
|
+
}
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
## Reactive Forms
|
|
126
|
+
\`\`\`typescript
|
|
127
|
+
@Component({
|
|
128
|
+
standalone: true,
|
|
129
|
+
imports: [ReactiveFormsModule],
|
|
130
|
+
template: \`
|
|
131
|
+
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
|
132
|
+
<input formControlName="email" />
|
|
133
|
+
@if (form.get('email')?.errors?.['required']) {
|
|
134
|
+
<span class="error">Email is required</span>
|
|
135
|
+
}
|
|
136
|
+
@if (form.get('email')?.errors?.['email']) {
|
|
137
|
+
<span class="error">Invalid email</span>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
<input type="password" formControlName="password" />
|
|
141
|
+
|
|
142
|
+
<button type="submit" [disabled]="form.invalid">Submit</button>
|
|
143
|
+
</form>
|
|
144
|
+
\`
|
|
145
|
+
})
|
|
146
|
+
export class LoginComponent {
|
|
147
|
+
private fb = inject(FormBuilder);
|
|
148
|
+
|
|
149
|
+
form = this.fb.group({
|
|
150
|
+
email: ['', [Validators.required, Validators.email]],
|
|
151
|
+
password: ['', [Validators.required, Validators.minLength(8)]]
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
onSubmit() {
|
|
155
|
+
if (this.form.valid) {
|
|
156
|
+
console.log(this.form.value);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
\`\`\`
|
|
161
|
+
|
|
162
|
+
## HTTP Interceptors
|
|
163
|
+
\`\`\`typescript
|
|
164
|
+
// auth.interceptor.ts
|
|
165
|
+
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|
166
|
+
const authService = inject(AuthService);
|
|
167
|
+
const token = authService.getToken();
|
|
168
|
+
|
|
169
|
+
if (token) {
|
|
170
|
+
req = req.clone({
|
|
171
|
+
setHeaders: { Authorization: \`Bearer \${token}\` }
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return next(req).pipe(
|
|
176
|
+
catchError((error: HttpErrorResponse) => {
|
|
177
|
+
if (error.status === 401) {
|
|
178
|
+
authService.logout();
|
|
179
|
+
}
|
|
180
|
+
return throwError(() => error);
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Register in app.config.ts
|
|
186
|
+
export const appConfig: ApplicationConfig = {
|
|
187
|
+
providers: [
|
|
188
|
+
provideHttpClient(withInterceptors([authInterceptor])),
|
|
189
|
+
provideRouter(routes)
|
|
190
|
+
]
|
|
191
|
+
};
|
|
192
|
+
\`\`\`
|
|
193
|
+
|
|
194
|
+
## RxJS Patterns
|
|
195
|
+
\`\`\`typescript
|
|
196
|
+
// Search with debounce
|
|
197
|
+
searchTerm = new Subject<string>();
|
|
198
|
+
|
|
199
|
+
results$ = this.searchTerm.pipe(
|
|
200
|
+
debounceTime(300),
|
|
201
|
+
distinctUntilChanged(),
|
|
202
|
+
switchMap(term => this.searchService.search(term))
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Combine observables
|
|
206
|
+
user$ = combineLatest([
|
|
207
|
+
this.userService.getUser(this.userId),
|
|
208
|
+
this.userService.getUserPosts(this.userId)
|
|
209
|
+
]).pipe(
|
|
210
|
+
map(([user, posts]) => ({ ...user, posts }))
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Auto-unsubscribe with takeUntilDestroyed
|
|
214
|
+
export class MyComponent {
|
|
215
|
+
private destroyRef = inject(DestroyRef);
|
|
216
|
+
|
|
217
|
+
ngOnInit() {
|
|
218
|
+
interval(1000).pipe(
|
|
219
|
+
takeUntilDestroyed(this.destroyRef)
|
|
220
|
+
).subscribe(console.log);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
\`\`\`
|
|
224
|
+
|
|
225
|
+
## ❌ DON'T
|
|
226
|
+
- Use constructor for complex logic (use inject())
|
|
227
|
+
- Forget to unsubscribe from observables
|
|
228
|
+
- Use NgModules for new projects (use standalone)
|
|
229
|
+
- Mutate signal values directly
|
|
230
|
+
|
|
231
|
+
## ✅ DO
|
|
232
|
+
- Use standalone components
|
|
233
|
+
- Use signals for reactive state
|
|
234
|
+
- Use inject() for DI
|
|
235
|
+
- Use functional guards and interceptors
|
|
236
|
+
- Use takeUntilDestroyed for cleanup
|
|
237
|
+
- Use control flow (@if, @for) instead of *ngIf, *ngFor
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# API Design Skill
|
|
2
|
+
|
|
3
|
+
## REST Best Practices
|
|
4
|
+
\`\`\`
|
|
5
|
+
GET /users → List users
|
|
6
|
+
GET /users/:id → Get single user
|
|
7
|
+
POST /users → Create user
|
|
8
|
+
PUT /users/:id → Update user (full)
|
|
9
|
+
PATCH /users/:id → Update user (partial)
|
|
10
|
+
DELETE /users/:id → Delete user
|
|
11
|
+
|
|
12
|
+
Nested Resources:
|
|
13
|
+
GET /users/:id/posts → List user's posts
|
|
14
|
+
POST /users/:id/posts → Create post for user
|
|
15
|
+
\`\`\`
|
|
16
|
+
|
|
17
|
+
## Status Codes
|
|
18
|
+
\`\`\`
|
|
19
|
+
2xx Success:
|
|
20
|
+
200 OK - Success with body
|
|
21
|
+
201 Created - Resource created (include Location header)
|
|
22
|
+
204 No Content - Success, no body (DELETE, PUT)
|
|
23
|
+
|
|
24
|
+
4xx Client Errors:
|
|
25
|
+
400 Bad Request - Invalid input/validation failed
|
|
26
|
+
401 Unauthorized - Not authenticated
|
|
27
|
+
403 Forbidden - Not authorized for this resource
|
|
28
|
+
404 Not Found - Resource doesn't exist
|
|
29
|
+
409 Conflict - Duplicate resource (e.g., email exists)
|
|
30
|
+
422 Unprocessable - Validation error (alternative to 400)
|
|
31
|
+
429 Too Many Reqs - Rate limit exceeded
|
|
32
|
+
|
|
33
|
+
5xx Server Errors:
|
|
34
|
+
500 Internal Error - Server error (hide details from client)
|
|
35
|
+
503 Unavailable - Maintenance/overload
|
|
36
|
+
\`\`\`
|
|
37
|
+
|
|
38
|
+
## Pagination Patterns
|
|
39
|
+
|
|
40
|
+
### Offset-based (simple, but slow for large datasets)
|
|
41
|
+
\`\`\`typescript
|
|
42
|
+
// Request: GET /users?page=2&limit=20
|
|
43
|
+
// Response:
|
|
44
|
+
{
|
|
45
|
+
"data": [...],
|
|
46
|
+
"pagination": {
|
|
47
|
+
"page": 2,
|
|
48
|
+
"limit": 20,
|
|
49
|
+
"total": 150,
|
|
50
|
+
"totalPages": 8
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
### Cursor-based (better for large/real-time data)
|
|
56
|
+
\`\`\`typescript
|
|
57
|
+
// Request: GET /users?cursor=abc123&limit=20
|
|
58
|
+
// Response:
|
|
59
|
+
{
|
|
60
|
+
"data": [...],
|
|
61
|
+
"pagination": {
|
|
62
|
+
"nextCursor": "def456",
|
|
63
|
+
"prevCursor": "xyz789",
|
|
64
|
+
"hasMore": true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Implementation
|
|
69
|
+
const users = await prisma.user.findMany({
|
|
70
|
+
take: limit + 1, // Fetch one extra to check hasMore
|
|
71
|
+
cursor: cursor ? { id: cursor } : undefined,
|
|
72
|
+
orderBy: { createdAt: 'desc' }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const hasMore = users.length > limit;
|
|
76
|
+
const data = hasMore ? users.slice(0, -1) : users;
|
|
77
|
+
const nextCursor = hasMore ? data[data.length - 1].id : null;
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
## Filtering & Sorting
|
|
81
|
+
\`\`\`typescript
|
|
82
|
+
// Request: GET /users?status=active&role=admin&sort=-createdAt,name
|
|
83
|
+
|
|
84
|
+
// Parse and validate query params
|
|
85
|
+
const filters = {
|
|
86
|
+
status: z.enum(['active', 'inactive']).optional(),
|
|
87
|
+
role: z.enum(['user', 'admin']).optional(),
|
|
88
|
+
search: z.string().max(100).optional(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const sortFields = ['createdAt', 'name', 'email'];
|
|
92
|
+
// - prefix means descending: -createdAt
|
|
93
|
+
|
|
94
|
+
// Response includes applied filters
|
|
95
|
+
{
|
|
96
|
+
"data": [...],
|
|
97
|
+
"filters": { "status": "active", "role": "admin" },
|
|
98
|
+
"sort": ["-createdAt", "name"]
|
|
99
|
+
}
|
|
100
|
+
\`\`\`
|
|
101
|
+
|
|
102
|
+
## Error Response Format
|
|
103
|
+
\`\`\`json
|
|
104
|
+
{
|
|
105
|
+
"error": {
|
|
106
|
+
"code": "VALIDATION_ERROR",
|
|
107
|
+
"message": "Invalid input data",
|
|
108
|
+
"details": [
|
|
109
|
+
{ "field": "email", "message": "Invalid email format" },
|
|
110
|
+
{ "field": "age", "message": "Must be a positive number" }
|
|
111
|
+
],
|
|
112
|
+
"requestId": "req_abc123"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
## Rate Limiting Headers
|
|
118
|
+
\`\`\`
|
|
119
|
+
X-RateLimit-Limit: 100 # Max requests per window
|
|
120
|
+
X-RateLimit-Remaining: 45 # Remaining requests
|
|
121
|
+
X-RateLimit-Reset: 1640000000 # Unix timestamp when window resets
|
|
122
|
+
Retry-After: 60 # Seconds until retry (on 429)
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
## API Versioning Strategies
|
|
126
|
+
\`\`\`
|
|
127
|
+
1. URL Path (recommended):
|
|
128
|
+
/api/v1/users
|
|
129
|
+
/api/v2/users
|
|
130
|
+
|
|
131
|
+
2. Header:
|
|
132
|
+
Accept: application/vnd.myapi.v1+json
|
|
133
|
+
|
|
134
|
+
3. Query Parameter:
|
|
135
|
+
/api/users?version=1
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
## Request/Response Examples
|
|
139
|
+
\`\`\`typescript
|
|
140
|
+
// POST /api/v1/users
|
|
141
|
+
// Request:
|
|
142
|
+
{
|
|
143
|
+
"email": "user@example.com",
|
|
144
|
+
"name": "John Doe",
|
|
145
|
+
"role": "user"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Response: 201 Created
|
|
149
|
+
// Headers: Location: /api/v1/users/123
|
|
150
|
+
{
|
|
151
|
+
"id": "123",
|
|
152
|
+
"email": "user@example.com",
|
|
153
|
+
"name": "John Doe",
|
|
154
|
+
"role": "user",
|
|
155
|
+
"createdAt": "2024-01-15T10:30:00Z"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// GET /api/v1/users/123
|
|
159
|
+
// Response: 200 OK
|
|
160
|
+
{
|
|
161
|
+
"id": "123",
|
|
162
|
+
"email": "user@example.com",
|
|
163
|
+
"name": "John Doe",
|
|
164
|
+
"role": "user",
|
|
165
|
+
"createdAt": "2024-01-15T10:30:00Z",
|
|
166
|
+
"_links": {
|
|
167
|
+
"self": "/api/v1/users/123",
|
|
168
|
+
"posts": "/api/v1/users/123/posts"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
\`\`\`
|
|
172
|
+
|
|
173
|
+
## ❌ DON'T
|
|
174
|
+
- Use verbs in URLs (POST /createUser)
|
|
175
|
+
- Return 200 for errors
|
|
176
|
+
- Ignore pagination for list endpoints
|
|
177
|
+
- Expose internal IDs or stack traces
|
|
178
|
+
- Change API without versioning
|
|
179
|
+
|
|
180
|
+
## ✅ DO
|
|
181
|
+
- Version your APIs (/api/v1/)
|
|
182
|
+
- Document with OpenAPI
|
|
183
|
+
- Use consistent naming (plural nouns)
|
|
184
|
+
- Implement pagination for all lists
|
|
185
|
+
- Add rate limiting with headers
|
|
186
|
+
- Include request IDs for debugging
|
|
187
|
+
- Use ISO 8601 for dates
|