@bradtaylorsf/alpha-loop 1.1.2 → 1.1.3

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.
@@ -1,346 +0,0 @@
1
- ---
2
- name: api-patterns
3
- description: REST API best practices including request validation, error handling, authentication, rate limiting, and OpenAPI documentation. Use when building backend APIs.
4
- auto_load: backend-developer
5
- priority: high
6
- ---
7
-
8
- # API Patterns Skill
9
-
10
- ## Quick Reference
11
-
12
- **Use when**: Building REST APIs, implementing authentication, handling errors, validating inputs
13
-
14
- **Key Patterns**:
15
- - Request validation with Zod
16
- - Standardized error responses
17
- - JWT authentication
18
- - Rate limiting
19
- - API documentation
20
-
21
- ---
22
-
23
- ## 1. Request Validation (Zod)
24
-
25
- ```typescript
26
- import { z } from 'zod';
27
-
28
- // ✅ Define schemas
29
- const CreateUserSchema = z.object({
30
- email: z.string().email('Invalid email format'),
31
- password: z.string()
32
- .min(12, 'Password must be at least 12 characters')
33
- .regex(/[A-Z]/, 'Must contain uppercase')
34
- .regex(/[0-9]/, 'Must contain number'),
35
- name: z.string().min(2).max(100),
36
- age: z.number().int().min(18).optional()
37
- });
38
-
39
- type CreateUserDto = z.infer<typeof CreateUserSchema>;
40
-
41
- // ✅ Validate in route handler
42
- router.post('/api/users', async (req, res, next) => {
43
- try {
44
- const userData = CreateUserSchema.parse(req.body);
45
- const user = await createUser(userData);
46
- res.status(201).json(user);
47
- } catch (err) {
48
- if (err instanceof z.ZodError) {
49
- return res.status(400).json({
50
- error: 'Validation failed',
51
- details: err.errors
52
- });
53
- }
54
- next(err);
55
- }
56
- });
57
- ```
58
-
59
- **Reusable Validation Middleware**:
60
- ```typescript
61
- export function validateRequest(schema: z.ZodSchema) {
62
- return (req: Request, res: Response, next: NextFunction) => {
63
- try {
64
- req.body = schema.parse(req.body);
65
- next();
66
- } catch (err) {
67
- if (err instanceof z.ZodError) {
68
- return res.status(400).json({
69
- error: 'Validation failed',
70
- details: err.errors
71
- });
72
- }
73
- next(err);
74
- }
75
- };
76
- }
77
-
78
- // Usage
79
- router.post('/api/users', validateRequest(CreateUserSchema), createUserHandler);
80
- ```
81
-
82
- ---
83
-
84
- ## 2. Standardized Error Responses
85
-
86
- ```typescript
87
- // ✅ Custom error class
88
- export class AppError extends Error {
89
- constructor(
90
- message: string,
91
- public statusCode: number = 500,
92
- public code?: string,
93
- public details?: any
94
- ) {
95
- super(message);
96
- this.name = 'AppError';
97
- }
98
- }
99
-
100
- // ✅ Error handling middleware
101
- export function errorHandler(
102
- err: Error,
103
- req: Request,
104
- res: Response,
105
- next: NextFunction
106
- ) {
107
- if (err instanceof AppError) {
108
- return res.status(err.statusCode).json({
109
- error: err.message,
110
- code: err.code,
111
- details: err.details
112
- });
113
- }
114
-
115
- // Log unexpected errors
116
- console.error('Unexpected error:', err);
117
-
118
- res.status(500).json({
119
- error: 'Internal server error',
120
- ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
121
- });
122
- }
123
-
124
- // Usage
125
- throw new AppError('User not found', 404, 'USER_NOT_FOUND');
126
- ```
127
-
128
- ---
129
-
130
- ## 3. JWT Authentication
131
-
132
- ```typescript
133
- import jwt from 'jsonwebtoken';
134
-
135
- const JWT_SECRET = process.env.JWT_SECRET!;
136
-
137
- // ✅ Generate token
138
- export function generateToken(userId: string): string {
139
- return jwt.sign(
140
- { userId },
141
- JWT_SECRET,
142
- { expiresIn: '7d' }
143
- );
144
- }
145
-
146
- // ✅ Auth middleware
147
- export function requireAuth(req: Request, res: Response, next: NextFunction) {
148
- const token = req.headers.authorization?.replace('Bearer ', '');
149
-
150
- if (!token) {
151
- throw new AppError('Authentication required', 401, 'NO_TOKEN');
152
- }
153
-
154
- try {
155
- const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
156
- req.userId = decoded.userId;
157
- next();
158
- } catch (err) {
159
- throw new AppError('Invalid token', 401, 'INVALID_TOKEN');
160
- }
161
- }
162
-
163
- // ✅ Optional auth
164
- export function optionalAuth(req: Request, res: Response, next: NextFunction) {
165
- const token = req.headers.authorization?.replace('Bearer ', '');
166
-
167
- if (token) {
168
- try {
169
- const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
170
- req.userId = decoded.userId;
171
- } catch {
172
- // Ignore invalid tokens in optional auth
173
- }
174
- }
175
-
176
- next();
177
- }
178
- ```
179
-
180
- ---
181
-
182
- ## 4. Rate Limiting
183
-
184
- ```typescript
185
- import rateLimit from 'express-rate-limit';
186
-
187
- // ✅ Global rate limit
188
- const globalLimiter = rateLimit({
189
- windowMs: 15 * 60 * 1000, // 15 minutes
190
- max: 100, // 100 requests per window
191
- message: 'Too many requests from this IP'
192
- });
193
-
194
- app.use('/api/', globalLimiter);
195
-
196
- // ✅ Strict limit for auth endpoints
197
- const authLimiter = rateLimit({
198
- windowMs: 15 * 60 * 1000,
199
- max: 5, // Only 5 login attempts
200
- message: 'Too many login attempts. Try again later.',
201
- skipSuccessfulRequests: true
202
- });
203
-
204
- router.post('/api/auth/login', authLimiter, loginHandler);
205
- ```
206
-
207
- ---
208
-
209
- ## 5. Pagination
210
-
211
- ```typescript
212
- // ✅ Standard pagination pattern
213
- const PaginationSchema = z.object({
214
- page: z.coerce.number().int().min(1).default(1),
215
- limit: z.coerce.number().int().min(1).max(100).default(20)
216
- });
217
-
218
- router.get('/api/users', async (req, res) => {
219
- const { page, limit } = PaginationSchema.parse(req.query);
220
- const offset = (page - 1) * limit;
221
-
222
- const [users, total] = await Promise.all([
223
- db.query('SELECT * FROM users LIMIT $1 OFFSET $2', [limit, offset]),
224
- db.query('SELECT COUNT(*) FROM users')
225
- ]);
226
-
227
- res.json({
228
- data: users.rows,
229
- pagination: {
230
- page,
231
- limit,
232
- total: parseInt(total.rows[0].count),
233
- totalPages: Math.ceil(parseInt(total.rows[0].count) / limit)
234
- }
235
- });
236
- });
237
- ```
238
-
239
- ---
240
-
241
- ## 6. API Response Format
242
-
243
- ```typescript
244
- // ✅ Consistent response structure
245
- interface ApiResponse<T> {
246
- data?: T;
247
- error?: string;
248
- meta?: {
249
- pagination?: PaginationMeta;
250
- timestamp: string;
251
- };
252
- }
253
-
254
- // Success response
255
- res.json({
256
- data: users,
257
- meta: {
258
- pagination: { page, limit, total },
259
- timestamp: new Date().toISOString()
260
- }
261
- });
262
-
263
- // Error response
264
- res.status(400).json({
265
- error: 'Validation failed',
266
- meta: {
267
- timestamp: new Date().toISOString()
268
- }
269
- });
270
- ```
271
-
272
- ---
273
-
274
- ## 7. OpenAPI Documentation
275
-
276
- ```typescript
277
- /**
278
- * @openapi
279
- * /api/users:
280
- * get:
281
- * summary: List all users
282
- * tags: [Users]
283
- * parameters:
284
- * - in: query
285
- * name: page
286
- * schema:
287
- * type: integer
288
- * minimum: 1
289
- * description: Page number
290
- * - in: query
291
- * name: limit
292
- * schema:
293
- * type: integer
294
- * minimum: 1
295
- * maximum: 100
296
- * description: Items per page
297
- * responses:
298
- * 200:
299
- * description: List of users
300
- * content:
301
- * application/json:
302
- * schema:
303
- * type: object
304
- * properties:
305
- * data:
306
- * type: array
307
- * items:
308
- * $ref: '#/components/schemas/User'
309
- */
310
- router.get('/api/users', listUsers);
311
- ```
312
-
313
- ---
314
-
315
- ## Checklist for New Endpoints
316
-
317
- - ✅ Input validation with Zod
318
- - ✅ Authentication/authorization check
319
- - ✅ Rate limiting configured
320
- - ✅ Error handling with AppError
321
- - ✅ Consistent response format
322
- - ✅ OpenAPI documentation
323
- - ✅ Tests with mocked APIs (MSW)
324
- - ✅ Logging for debugging
325
-
326
- ---
327
-
328
- ## Common Patterns
329
-
330
- **CRUD Operations**:
331
- - `GET /api/resources` - List (with pagination)
332
- - `GET /api/resources/:id` - Get one
333
- - `POST /api/resources` - Create
334
- - `PUT /api/resources/:id` - Update
335
- - `DELETE /api/resources/:id` - Delete
336
-
337
- **Status Codes**:
338
- - `200` - Success (GET, PUT)
339
- - `201` - Created (POST)
340
- - `204` - No Content (DELETE)
341
- - `400` - Bad Request (validation)
342
- - `401` - Unauthorized (auth required)
343
- - `403` - Forbidden (insufficient permissions)
344
- - `404` - Not Found
345
- - `429` - Too Many Requests (rate limit)
346
- - `500` - Internal Server Error
@@ -1,293 +0,0 @@
1
- /**
2
- * Complete REST API Example with Best Practices
3
- * Demonstrates: validation, auth, rate limiting, error handling
4
- */
5
-
6
- import express, { Request, Response, NextFunction } from 'express';
7
- import { z } from 'zod';
8
- import rateLimit from 'express-rate-limit';
9
- import helmet from 'helmet';
10
- import jwt from 'jsonwebtoken';
11
- import bcrypt from 'bcrypt';
12
-
13
- const app = express();
14
- const PORT = process.env.PORT || 3000;
15
- const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
16
-
17
- // Middleware
18
- app.use(express.json());
19
- app.use(helmet());
20
-
21
- // Custom error class
22
- export class AppError extends Error {
23
- constructor(
24
- message: string,
25
- public statusCode: number = 500,
26
- public code?: string,
27
- public details?: any
28
- ) {
29
- super(message);
30
- this.name = 'AppError';
31
- }
32
- }
33
-
34
- // Validation schemas
35
- const CreateUserSchema = z.object({
36
- email: z.string().email('Invalid email format'),
37
- password: z.string()
38
- .min(12, 'Password must be at least 12 characters')
39
- .regex(/[A-Z]/, 'Must contain uppercase')
40
- .regex(/[0-9]/, 'Must contain number'),
41
- name: z.string().min(2).max(100)
42
- });
43
-
44
- const LoginSchema = z.object({
45
- email: z.string().email(),
46
- password: z.string()
47
- });
48
-
49
- const PaginationSchema = z.object({
50
- page: z.coerce.number().int().min(1).default(1),
51
- limit: z.coerce.number().int().min(1).max(100).default(20)
52
- });
53
-
54
- // Type inference
55
- type CreateUserDto = z.infer<typeof CreateUserSchema>;
56
- type LoginDto = z.infer<typeof LoginSchema>;
57
- type PaginationDto = z.infer<typeof PaginationSchema>;
58
-
59
- // Validation middleware
60
- function validateRequest(schema: z.ZodSchema) {
61
- return (req: Request, res: Response, next: NextFunction) => {
62
- try {
63
- const target = req.method === 'GET' ? req.query : req.body;
64
- req.body = schema.parse(target);
65
- next();
66
- } catch (err) {
67
- if (err instanceof z.ZodError) {
68
- return res.status(400).json({
69
- error: 'Validation failed',
70
- details: err.errors
71
- });
72
- }
73
- next(err);
74
- }
75
- };
76
- }
77
-
78
- // Auth middleware
79
- function requireAuth(req: Request, res: Response, next: NextFunction) {
80
- const token = req.headers.authorization?.replace('Bearer ', '');
81
-
82
- if (!token) {
83
- throw new AppError('Authentication required', 401, 'NO_TOKEN');
84
- }
85
-
86
- try {
87
- const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
88
- (req as any).userId = decoded.userId;
89
- next();
90
- } catch (err) {
91
- throw new AppError('Invalid token', 401, 'INVALID_TOKEN');
92
- }
93
- }
94
-
95
- // Rate limiting
96
- const globalLimiter = rateLimit({
97
- windowMs: 15 * 60 * 1000, // 15 minutes
98
- max: 100, // 100 requests per window
99
- message: 'Too many requests from this IP'
100
- });
101
-
102
- const authLimiter = rateLimit({
103
- windowMs: 15 * 60 * 1000,
104
- max: 5, // Only 5 login attempts
105
- message: 'Too many login attempts. Try again later.',
106
- skipSuccessfulRequests: true
107
- });
108
-
109
- app.use('/api/', globalLimiter);
110
-
111
- // Mock database
112
- const users: any[] = [];
113
-
114
- // Routes
115
-
116
- // POST /api/auth/register
117
- app.post(
118
- '/api/auth/register',
119
- validateRequest(CreateUserSchema),
120
- async (req: Request, res: Response, next: NextFunction) => {
121
- try {
122
- const { email, password, name } = req.body as CreateUserDto;
123
-
124
- // Check if user exists
125
- if (users.find(u => u.email === email)) {
126
- throw new AppError('Email already registered', 409, 'EMAIL_EXISTS');
127
- }
128
-
129
- // Hash password
130
- const passwordHash = await bcrypt.hash(password, 12);
131
-
132
- // Create user
133
- const user = {
134
- id: String(users.length + 1),
135
- email,
136
- name,
137
- passwordHash,
138
- createdAt: new Date().toISOString()
139
- };
140
-
141
- users.push(user);
142
-
143
- // Generate token
144
- const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
145
-
146
- res.status(201).json({
147
- data: {
148
- user: { id: user.id, email: user.email, name: user.name },
149
- token
150
- }
151
- });
152
- } catch (err) {
153
- next(err);
154
- }
155
- }
156
- );
157
-
158
- // POST /api/auth/login
159
- app.post(
160
- '/api/auth/login',
161
- authLimiter,
162
- validateRequest(LoginSchema),
163
- async (req: Request, res: Response, next: NextFunction) => {
164
- try {
165
- const { email, password } = req.body as LoginDto;
166
-
167
- // Find user
168
- const user = users.find(u => u.email === email);
169
- if (!user) {
170
- throw new AppError('Invalid credentials', 401, 'INVALID_CREDENTIALS');
171
- }
172
-
173
- // Verify password
174
- const isValid = await bcrypt.compare(password, user.passwordHash);
175
- if (!isValid) {
176
- throw new AppError('Invalid credentials', 401, 'INVALID_CREDENTIALS');
177
- }
178
-
179
- // Generate token
180
- const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
181
-
182
- res.json({
183
- data: {
184
- user: { id: user.id, email: user.email, name: user.name },
185
- token
186
- }
187
- });
188
- } catch (err) {
189
- next(err);
190
- }
191
- }
192
- );
193
-
194
- // GET /api/users (paginated)
195
- app.get(
196
- '/api/users',
197
- requireAuth,
198
- validateRequest(PaginationSchema),
199
- async (req: Request, res: Response) => {
200
- const { page, limit } = req.body as PaginationDto;
201
- const offset = (page - 1) * limit;
202
-
203
- const paginatedUsers = users
204
- .slice(offset, offset + limit)
205
- .map(({ passwordHash, ...user }) => user);
206
-
207
- res.json({
208
- data: paginatedUsers,
209
- pagination: {
210
- page,
211
- limit,
212
- total: users.length,
213
- totalPages: Math.ceil(users.length / limit)
214
- },
215
- meta: {
216
- timestamp: new Date().toISOString()
217
- }
218
- });
219
- }
220
- );
221
-
222
- // GET /api/users/:id
223
- app.get(
224
- '/api/users/:id',
225
- requireAuth,
226
- async (req: Request, res: Response, next: NextFunction) => {
227
- try {
228
- const user = users.find(u => u.id === req.params.id);
229
-
230
- if (!user) {
231
- throw new AppError('User not found', 404, 'USER_NOT_FOUND');
232
- }
233
-
234
- const { passwordHash, ...userData } = user;
235
- res.json({ data: userData });
236
- } catch (err) {
237
- next(err);
238
- }
239
- }
240
- );
241
-
242
- // DELETE /api/users/:id
243
- app.delete(
244
- '/api/users/:id',
245
- requireAuth,
246
- async (req: Request, res: Response, next: NextFunction) => {
247
- try {
248
- const userId = (req as any).userId;
249
- const targetId = req.params.id;
250
-
251
- // Check authorization (can only delete own account unless admin)
252
- if (userId !== targetId) {
253
- throw new AppError('Forbidden', 403, 'FORBIDDEN');
254
- }
255
-
256
- const index = users.findIndex(u => u.id === targetId);
257
- if (index === -1) {
258
- throw new AppError('User not found', 404, 'USER_NOT_FOUND');
259
- }
260
-
261
- users.splice(index, 1);
262
- res.status(204).send();
263
- } catch (err) {
264
- next(err);
265
- }
266
- }
267
- );
268
-
269
- // Error handling middleware
270
- app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
271
- if (err instanceof AppError) {
272
- return res.status(err.statusCode).json({
273
- error: err.message,
274
- code: err.code,
275
- details: err.details
276
- });
277
- }
278
-
279
- // Log unexpected errors
280
- console.error('Unexpected error:', err);
281
-
282
- res.status(500).json({
283
- error: 'Internal server error',
284
- ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
285
- });
286
- });
287
-
288
- // Start server
289
- app.listen(PORT, () => {
290
- console.log(`Server running on port ${PORT}`);
291
- });
292
-
293
- export default app;