@@ -57,24 +57,51 @@ Understanding Fastify's hook execution order is essential for proper security im
5757
5858``` typescript
5959import { 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
79106export 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
235273import { 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
254320export 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