@@ -58,41 +58,123 @@ Understanding Fastify's hook execution order is essential for proper security im
5858``` typescript
5959import { requireGlobalAdmin } from ' ../../../middleware/roleMiddleware' ;
6060
61- export default async function secureRoute(fastify : FastifyInstance ) {
62- fastify .post <{ Body: RequestInput }>(' /protected-endpoint' , {
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+ }
95+
96+ interface SuccessResponse {
97+ success: boolean ;
98+ message: string ;
99+ }
100+
101+ interface ErrorResponse {
102+ success: boolean ;
103+ error: string ;
104+ }
105+
106+ export default async function secureRoute(server : FastifyInstance ) {
107+ server .post (' /protected-endpoint' , {
108+ preValidation: requireGlobalAdmin (), // ✅ CORRECT: Runs before validation
63109 schema: {
64110 tags: [' Protected' ],
65111 summary: ' Protected endpoint' ,
66112 description: ' Requires admin permissions' ,
67113 security: [{ cookieAuth: [] }],
68- body: createSchema (RequestSchema ),
114+
115+ // Fastify validation schema
116+ body: REQUEST_SCHEMA ,
117+
118+ // OpenAPI documentation (same schema, reused)
119+ requestBody: {
120+ required: true ,
121+ content: {
122+ ' application/json' : {
123+ schema: REQUEST_SCHEMA
124+ }
125+ }
126+ },
127+
69128 response: {
70- 200 : createSchema (SuccessResponseSchema .describe (' Success' )),
71- 401 : createSchema (ErrorResponseSchema .describe (' Unauthorized' )),
72- 403 : createSchema (ErrorResponseSchema .describe (' Forbidden' )),
73- 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+ }
74145 }
75- },
76- preValidation: requireGlobalAdmin (), // ✅ CORRECT: Runs before validation,
146+ }
77147 }, async (request , reply ) => {
78148 // If we reach here, user is authorized AND input is validated
79- const validatedData = request .body ;
149+ const validatedData = request .body as RequestBody ;
150+
80151 // 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 );
81158 });
82159}
83160```
84161
85162### ❌ Insecure Pattern: preHandler for Authorization
86163
87164``` typescript
88- export default async function insecureRoute(fastify : FastifyInstance ) {
89- fastify .post <{ Body : RequestInput }> (' /protected-endpoint' , {
165+ export default async function insecureRoute(server : FastifyInstance ) {
166+ server .post (' /protected-endpoint' , {
90167 schema: {
91168 // Schema definition...
92- body: zodToJsonSchema (RequestSchema , {
93- $refStrategy: ' none' ,
94- target: ' openApi3'
95- })
169+ body: {
170+ type: ' object' ,
171+ properties: {
172+ name: { type: ' string' , minLength: 1 },
173+ value: { type: ' string' }
174+ },
175+ required: [' name' , ' value' ],
176+ additionalProperties: false
177+ }
96178 },
97179 preHandler: requireGlobalAdmin (), // ❌ WRONG: Runs after validation
98180 }, async (request , reply ) => {
@@ -161,22 +243,24 @@ For endpoints that support both web users (cookies) and CLI users (OAuth2 Bearer
161243``` typescript
162244import { requireAuthenticationAny , requireOAuthScope } from ' ../../middleware/oauthMiddleware' ;
163245
164- fastify .get (' /dual-auth-endpoint' , {
165- schema: {
166- security: [
167- { cookieAuth: [] }, // Cookie authentication
168- { bearerAuth: [] } // OAuth2 Bearer token
169- ]
170- },
171- preValidation: [
172- requireAuthenticationAny (), // Accept either auth method
173- requireOAuthScope (' your:scope' ) // Enforce OAuth2 scope
174- ]
175- }, async (request , reply ) => {
176- // Endpoint accessible via both authentication methods
177- const authType = request .tokenPayload ? ' oauth2' : ' cookie' ;
178- const userId = request .user ! .id ;
179- });
246+ export default async function dualAuthRoute(server : FastifyInstance ) {
247+ server .get (' /dual-auth-endpoint' , {
248+ preValidation: [
249+ requireAuthenticationAny (), // Accept either auth method
250+ requireOAuthScope (' your:scope' ) // Enforce OAuth2 scope
251+ ],
252+ schema: {
253+ security: [
254+ { cookieAuth: [] }, // Cookie authentication
255+ { bearerAuth: [] } // OAuth2 Bearer token
256+ ]
257+ }
258+ }, async (request , reply ) => {
259+ // Endpoint accessible via both authentication methods
260+ const authType = request .tokenPayload ? ' oauth2' : ' cookie' ;
261+ const userId = request .user ! .id ;
262+ });
263+ }
180264```
181265
182266For detailed OAuth2 implementation, see the [ Backend OAuth Implementation Guide] ( /development/backend/oauth-providers ) and [ Backend Security Policy] ( /development/backend/security#oauth2-server-security ) .
@@ -188,31 +272,102 @@ For endpoints that operate within team contexts (e.g., `/teams/:teamId/resource`
188272``` typescript
189273import { requireTeamPermission } from ' ../../../middleware/roleMiddleware' ;
190274
191- export default async function teamResourceRoute(fastify : FastifyInstance ) {
192- fastify .post <{
193- Params: { teamId: string };
194- Body: CreateResourceRequest ;
195- }>(' /teams/:teamId/resources' , {
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+ }
309+
310+ interface SuccessResponse {
311+ success: boolean ;
312+ message: string ;
313+ }
314+
315+ interface ErrorResponse {
316+ success: boolean ;
317+ error: string ;
318+ }
319+
320+ export default async function teamResourceRoute(server : FastifyInstance ) {
321+ server .post (' /teams/:teamId/resources' , {
322+ preValidation: requireTeamPermission (' resources.create' ), // ✅ Team-aware authorization
196323 schema: {
197324 tags: [' Team Resources' ],
198325 summary: ' Create team resource' ,
199326 description: ' Creates a resource within the specified team context' ,
200327 security: [{ cookieAuth: [] }],
201- params: zodToJsonSchema (z .object ({
202- teamId: z .string ().min (1 , ' Team ID is required' )
203- })),
204- body: zodToJsonSchema (CreateResourceSchema ),
328+
329+ params: {
330+ type: ' object' ,
331+ properties: {
332+ teamId: { type: ' string' , minLength: 1 }
333+ },
334+ required: [' teamId' ],
335+ additionalProperties: false
336+ },
337+
338+ body: CREATE_RESOURCE_SCHEMA ,
339+
340+ requestBody: {
341+ required: true ,
342+ content: {
343+ ' application/json' : {
344+ schema: CREATE_RESOURCE_SCHEMA
345+ }
346+ }
347+ },
348+
205349 response: {
206- 201 : zodToJsonSchema (SuccessResponseSchema ),
207- 401 : zodToJsonSchema (ErrorResponseSchema .describe (' Unauthorized' )),
208- 403 : zodToJsonSchema (ErrorResponseSchema .describe (' Forbidden - Not team member or insufficient permissions' )),
209- 400 : zodToJsonSchema (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+ }
210366 }
211- },
212- preValidation: requireTeamPermission (' resources.create' ), // ✅ Team-aware authorization
367+ }
213368 }, async (request , reply ) => {
214- const { teamId } = request .params ;
215- const resourceData = request .body ;
369+ const { teamId } = request .params as { teamId : string } ;
370+ const resourceData = request .body as CreateResourceRequest ;
216371
217372 // User is guaranteed to be:
218373 // 1. Authenticated
@@ -221,6 +376,12 @@ export default async function teamResourceRoute(fastify: FastifyInstance) {
221376 // 4. Input is validated
222377
223378 // 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 );
224385 });
225386}
226387```
@@ -358,21 +519,21 @@ team_user: [
358519
359520``` typescript
360521// Global admin only
361- fastify .delete (' /admin/users/:id' , {
362- schema: { /* ... */ },
522+ server .delete (' /admin/users/:id' , {
363523 preValidation: requireGlobalAdmin (),
524+ schema: { /* ... */ }
364525}, handler );
365526
366527// Specific permission required
367- fastify .post (' /settings/bulk' , {
368- schema: { /* ... */ },
528+ server .post (' /settings/bulk' , {
369529 preValidation: requirePermission (' settings.edit' ),
530+ schema: { /* ... */ }
370531}, handler );
371532
372533// User can access own data OR admin can access any
373- fastify .get (' /users/:id/profile' , {
374- schema: { /* ... */ },
534+ server .get (' /users/:id/profile' , {
375535 preValidation: requireOwnershipOrAdmin (getUserIdFromParams ),
536+ schema: { /* ... */ }
376537}, handler );
377538```
378539
@@ -441,13 +602,13 @@ For complex authorization requirements:
441602
442603``` typescript
443604// Multiple checks in sequence
444- fastify .post (' /complex-endpoint' , {
445- schema: { /* ... */ },
605+ server .post (' /complex-endpoint' , {
446606 preValidation: [
447607 requireAuthentication (), // Must be logged in
448608 requireRole (' team_member' ), // Must have team role
449609 requirePermission (' data.write' ) // Must have write permission
450610 ],
611+ schema: { /* ... */ }
451612}, handler );
452613```
453614
@@ -465,9 +626,9 @@ async function conditionalAuth(request: FastifyRequest, reply: FastifyReply) {
465626 }
466627}
467628
468- fastify .post (' /conditional-endpoint' , {
469- schema: { /* ... */ },
629+ server .post (' /conditional-endpoint' , {
470630 preValidation: conditionalAuth ,
631+ schema: { /* ... */ }
471632}, handler );
472633```
473634
0 commit comments