Skip to content

Commit 4aeb664

Browse files
author
Lasim
committed
feat(api-docs): refactor schema definitions to use reusable JSON Schema constants for improved type safety and documentation consistency
1 parent 77478dd commit 4aeb664

2 files changed

Lines changed: 382 additions & 221 deletions

File tree

docs/development/backend/api-security.mdx

Lines changed: 137 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,51 @@ Understanding Fastify's hook execution order is essential for proper security im
5757

5858
```typescript
5959
import { requireGlobalAdmin } from '../../../middleware/roleMiddleware';
60-
import { z } from 'zod';
61-
import { createSchema } from 'zod-openapi';
6260

63-
// Define Zod schemas
64-
const RequestSchema = z.object({
65-
name: z.string().min(1, 'Name is required'),
66-
value: z.string()
67-
});
61+
// Reusable Schema Constants
62+
const REQUEST_SCHEMA = {
63+
type: 'object',
64+
properties: {
65+
name: { type: 'string', minLength: 1, description: 'Name is required' },
66+
value: { type: 'string', description: 'Value field' }
67+
},
68+
required: ['name', 'value'],
69+
additionalProperties: false
70+
} as const;
71+
72+
const SUCCESS_RESPONSE_SCHEMA = {
73+
type: 'object',
74+
properties: {
75+
success: { type: 'boolean' },
76+
message: { type: 'string' }
77+
},
78+
required: ['success', 'message']
79+
} as const;
80+
81+
const ERROR_RESPONSE_SCHEMA = {
82+
type: 'object',
83+
properties: {
84+
success: { type: 'boolean', default: false },
85+
error: { type: 'string' }
86+
},
87+
required: ['success', 'error']
88+
} as const;
89+
90+
// TypeScript interfaces
91+
interface RequestBody {
92+
name: string;
93+
value: string;
94+
}
6895

69-
const SuccessResponseSchema = z.object({
70-
success: z.boolean(),
71-
message: z.string()
72-
});
96+
interface SuccessResponse {
97+
success: boolean;
98+
message: string;
99+
}
73100

74-
const ErrorResponseSchema = z.object({
75-
success: z.boolean().default(false),
76-
error: z.string()
77-
});
101+
interface ErrorResponse {
102+
success: boolean;
103+
error: string;
104+
}
78105

79106
export default async function secureRoute(server: FastifyInstance) {
80107
server.post('/protected-endpoint', {
@@ -86,37 +113,48 @@ export default async function secureRoute(server: FastifyInstance) {
86113
security: [{ cookieAuth: [] }],
87114

88115
// Fastify validation schema
89-
body: {
90-
type: 'object',
91-
properties: {
92-
name: { type: 'string', minLength: 1 },
93-
value: { type: 'string' }
94-
},
95-
required: ['name', 'value'],
96-
additionalProperties: false
97-
},
116+
body: REQUEST_SCHEMA,
98117

99-
// createSchema() for OpenAPI documentation
118+
// OpenAPI documentation (same schema, reused)
100119
requestBody: {
101120
required: true,
102121
content: {
103122
'application/json': {
104-
schema: createSchema(RequestSchema)
123+
schema: REQUEST_SCHEMA
105124
}
106125
}
107126
},
108127

109128
response: {
110-
200: createSchema(SuccessResponseSchema.describe('Success')),
111-
401: createSchema(ErrorResponseSchema.describe('Unauthorized')),
112-
403: createSchema(ErrorResponseSchema.describe('Forbidden')),
113-
400: createSchema(ErrorResponseSchema.describe('Bad Request'))
129+
200: {
130+
...SUCCESS_RESPONSE_SCHEMA,
131+
description: 'Success'
132+
},
133+
401: {
134+
...ERROR_RESPONSE_SCHEMA,
135+
description: 'Unauthorized'
136+
},
137+
403: {
138+
...ERROR_RESPONSE_SCHEMA,
139+
description: 'Forbidden'
140+
},
141+
400: {
142+
...ERROR_RESPONSE_SCHEMA,
143+
description: 'Bad Request'
144+
}
114145
}
115146
}
116147
}, async (request, reply) => {
117148
// If we reach here, user is authorized AND input is validated
118-
const validatedData = request.body;
149+
const validatedData = request.body as RequestBody;
150+
119151
// Your business logic here
152+
const successResponse: SuccessResponse = {
153+
success: true,
154+
message: 'Operation completed successfully'
155+
};
156+
const jsonString = JSON.stringify(successResponse);
157+
return reply.status(200).type('application/json').send(jsonString);
120158
});
121159
}
122160
```
@@ -233,23 +271,51 @@ For endpoints that operate within team contexts (e.g., `/teams/:teamId/resource`
233271

234272
```typescript
235273
import { requireTeamPermission } from '../../../middleware/roleMiddleware';
236-
import { z } from 'zod';
237-
import { createSchema } from 'zod-openapi';
238274

239-
const CreateResourceSchema = z.object({
240-
name: z.string().min(1, 'Name is required'),
241-
description: z.string().optional()
242-
});
275+
// Reusable Schema Constants
276+
const CREATE_RESOURCE_SCHEMA = {
277+
type: 'object',
278+
properties: {
279+
name: { type: 'string', minLength: 1, description: 'Name is required' },
280+
description: { type: 'string', description: 'Optional description' }
281+
},
282+
required: ['name'],
283+
additionalProperties: false
284+
} as const;
285+
286+
const SUCCESS_RESPONSE_SCHEMA = {
287+
type: 'object',
288+
properties: {
289+
success: { type: 'boolean' },
290+
message: { type: 'string' }
291+
},
292+
required: ['success', 'message']
293+
} as const;
294+
295+
const ERROR_RESPONSE_SCHEMA = {
296+
type: 'object',
297+
properties: {
298+
success: { type: 'boolean', default: false },
299+
error: { type: 'string' }
300+
},
301+
required: ['success', 'error']
302+
} as const;
303+
304+
// TypeScript interfaces
305+
interface CreateResourceRequest {
306+
name: string;
307+
description?: string;
308+
}
243309

244-
const SuccessResponseSchema = z.object({
245-
success: z.boolean(),
246-
message: z.string()
247-
});
310+
interface SuccessResponse {
311+
success: boolean;
312+
message: string;
313+
}
248314

249-
const ErrorResponseSchema = z.object({
250-
success: z.boolean().default(false),
251-
error: z.string()
252-
});
315+
interface ErrorResponse {
316+
success: boolean;
317+
error: string;
318+
}
253319

254320
export default async function teamResourceRoute(server: FastifyInstance) {
255321
server.post('/teams/:teamId/resources', {
@@ -269,35 +335,39 @@ export default async function teamResourceRoute(server: FastifyInstance) {
269335
additionalProperties: false
270336
},
271337

272-
body: {
273-
type: 'object',
274-
properties: {
275-
name: { type: 'string', minLength: 1 },
276-
description: { type: 'string' }
277-
},
278-
required: ['name'],
279-
additionalProperties: false
280-
},
338+
body: CREATE_RESOURCE_SCHEMA,
281339

282340
requestBody: {
283341
required: true,
284342
content: {
285343
'application/json': {
286-
schema: createSchema(CreateResourceSchema)
344+
schema: CREATE_RESOURCE_SCHEMA
287345
}
288346
}
289347
},
290348

291349
response: {
292-
201: createSchema(SuccessResponseSchema),
293-
401: createSchema(ErrorResponseSchema.describe('Unauthorized')),
294-
403: createSchema(ErrorResponseSchema.describe('Forbidden - Not team member or insufficient permissions')),
295-
400: createSchema(ErrorResponseSchema.describe('Bad Request'))
350+
201: {
351+
...SUCCESS_RESPONSE_SCHEMA,
352+
description: 'Resource created successfully'
353+
},
354+
401: {
355+
...ERROR_RESPONSE_SCHEMA,
356+
description: 'Unauthorized'
357+
},
358+
403: {
359+
...ERROR_RESPONSE_SCHEMA,
360+
description: 'Forbidden - Not team member or insufficient permissions'
361+
},
362+
400: {
363+
...ERROR_RESPONSE_SCHEMA,
364+
description: 'Bad Request'
365+
}
296366
}
297367
}
298368
}, async (request, reply) => {
299-
const { teamId } = request.params;
300-
const resourceData = request.body;
369+
const { teamId } = request.params as { teamId: string };
370+
const resourceData = request.body as CreateResourceRequest;
301371

302372
// User is guaranteed to be:
303373
// 1. Authenticated
@@ -306,6 +376,12 @@ export default async function teamResourceRoute(server: FastifyInstance) {
306376
// 4. Input is validated
307377

308378
// Your business logic here
379+
const successResponse: SuccessResponse = {
380+
success: true,
381+
message: `Resource "${resourceData.name}" created successfully`
382+
};
383+
const jsonString = JSON.stringify(successResponse);
384+
return reply.status(201).type('application/json').send(jsonString);
309385
});
310386
}
311387
```

0 commit comments

Comments
 (0)