Skip to content

Commit 7f96b3b

Browse files
committed
feat: integrate zod and zod-to-json-schema for improved request/response validation in authentication routes
1 parent 86fba41 commit 7f96b3b

File tree

9 files changed

+231
-254
lines changed

9 files changed

+231
-254
lines changed

package-lock.json

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/backend/API_DOCUMENTATION.md

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This document explains how to generate and use the OpenAPI specification for the
44

55
## Overview
66

7-
The DeployStack Backend uses Fastify with Swagger plugins to automatically generate OpenAPI 3.0 specifications from route definitions. This provides:
7+
The DeployStack Backend uses Fastify with Swagger plugins to automatically generate OpenAPI 3.0 specifications. Route schemas are defined using [Zod](https://zod.dev/) for type safety and expressiveness, and then converted to JSON Schema using the [zod-to-json-schema](https://www.npmjs.com/package/zod-to-json-schema) library. This provides:
88

99
- **Interactive Documentation**: Swagger UI interface for testing APIs
1010
- **Postman Integration**: JSON/YAML specs that can be imported into Postman
@@ -86,31 +86,73 @@ When the server is running (`npm run dev`), you can access:
8686

8787
## Adding Documentation to Routes
8888

89-
To add OpenAPI documentation to your routes, include a schema object:
89+
To add OpenAPI documentation to your routes, define your request body and response schemas using Zod. Then, use the `zodToJsonSchema` utility to convert these Zod schemas into the JSON Schema format expected by Fastify.
90+
91+
Make sure you have `zod` and `zod-to-json-schema` installed in your backend service.
9092

9193
```typescript
94+
import { z } from 'zod';
95+
import { zodToJsonSchema } from 'zod-to-json-schema';
96+
97+
// 1. Define your Zod schemas for request body, responses, etc.
98+
const myRequestBodySchema = z.object({
99+
name: z.string().min(3).describe("The name of the item (min 3 chars)"),
100+
count: z.number().positive().describe("How many items (must be positive)")
101+
});
102+
103+
const mySuccessResponseSchema = z.object({
104+
success: z.boolean().describe("Indicates if the operation was successful"),
105+
itemId: z.string().uuid().describe("The UUID of the created/affected item"),
106+
message: z.string().optional().describe("Optional success message")
107+
});
108+
109+
const myErrorResponseSchema = z.object({
110+
success: z.boolean().default(false).describe("Indicates failure"),
111+
error: z.string().describe("Error message detailing what went wrong")
112+
});
113+
114+
// 2. Construct the Fastify route schema using zodToJsonSchema
92115
const routeSchema = {
93-
tags: ['Category'],
94-
summary: 'Brief description',
95-
description: 'Detailed description of what this endpoint does',
96-
security: [{ cookieAuth: [] }], // If authentication required
116+
tags: ['Category'], // Your API category
117+
summary: 'Brief description of your endpoint',
118+
description: 'Detailed description of what this endpoint does, its parameters, and expected outcomes.',
119+
security: [{ cookieAuth: [] }], // Include if authentication is required
120+
body: zodToJsonSchema(myRequestBodySchema, {
121+
$refStrategy: 'none', // Keeps definitions inline, often simpler for Fastify
122+
target: 'openApi3' // Ensures compatibility with OpenAPI 3.0
123+
}),
97124
response: {
98-
200: {
99-
type: 'object',
100-
properties: {
101-
success: { type: 'boolean' },
102-
message: { type: 'string' }
103-
},
104-
required: ['success', 'message']
105-
}
125+
200: zodToJsonSchema(mySuccessResponseSchema.describe("Successful operation"), {
126+
$refStrategy: 'none',
127+
target: 'openApi3'
128+
}),
129+
400: zodToJsonSchema(myErrorResponseSchema.describe("Bad Request - Invalid input"), {
130+
$refStrategy: 'none',
131+
target: 'openApi3'
132+
}),
133+
// Define other responses (e.g., 401, 403, 404, 500) similarly
106134
}
107135
};
108136

137+
// 3. Use the schema in your Fastify route definition
109138
fastify.post('/your-route', { schema: routeSchema }, async (request, reply) => {
110-
// Your route handler
139+
// Your route handler logic here
140+
// Fastify will automatically validate request.body against myRequestBodySchema
141+
142+
// Example of returning a success response:
143+
// return reply.status(200).send({
144+
// success: true,
145+
// itemId: 'some-uuid-v4-here',
146+
// message: 'Item processed successfully.'
147+
// });
148+
149+
// Example of returning an error response:
150+
// return reply.status(400).send({ success: false, error: "Invalid name provided." });
111151
});
112152
```
113153

154+
**Note**: Older examples in this document (like the "Logout Route Documentation" below) might still show manually crafted JSON schemas. The recommended approach is now to use Zod with `zod-to-json-schema` as shown above for better type safety and maintainability.
155+
114156
## Example: Logout Route Documentation
115157

116158
The logout route (`/api/auth/logout`) demonstrates proper documentation:

services/backend/api-spec.json

Lines changed: 42 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,8 @@
433433
"required": [
434434
"login",
435435
"password"
436-
]
436+
],
437+
"additionalProperties": false
437438
}
438439
}
439440
},
@@ -450,7 +451,6 @@
450451
"content": {
451452
"application/json": {
452453
"schema": {
453-
"description": "Login successful. Session cookie is set.",
454454
"type": "object",
455455
"properties": {
456456
"success": {
@@ -463,7 +463,6 @@
463463
},
464464
"user": {
465465
"type": "object",
466-
"description": "Basic information about the logged-in user.",
467466
"properties": {
468467
"id": {
469468
"type": "string",
@@ -476,44 +475,40 @@
476475
},
477476
"username": {
478477
"type": "string",
479-
"nullable": true
478+
"nullable": true,
479+
"description": "User's username."
480480
},
481481
"first_name": {
482482
"type": "string",
483-
"nullable": true
483+
"nullable": true,
484+
"description": "User's first name."
484485
},
485486
"last_name": {
486487
"type": "string",
487-
"nullable": true
488+
"nullable": true,
489+
"description": "User's last name."
488490
},
489491
"role_id": {
490492
"type": "string",
491-
"nullable": true
493+
"nullable": true,
494+
"description": "User's role ID."
492495
}
493496
},
494497
"required": [
495498
"id",
496499
"email"
497-
]
500+
],
501+
"additionalProperties": false,
502+
"description": "Basic information about the logged-in user."
498503
}
499504
},
500505
"required": [
501506
"success",
502507
"message",
503508
"user"
504-
]
505-
},
506-
"example": {
507-
"success": true,
508-
"message": "Logged in successfully.",
509-
"user": {
510-
"id": "clxyz1234000008l3abcde123",
511-
"email": "user@example.com",
512-
"username": "testuser",
513-
"first_name": "Test",
514-
"last_name": "User",
515-
"role_id": "user_role_id"
516-
}
509+
],
510+
"additionalProperties": false,
511+
"description": "Login successful. Session cookie is set."
517512
}
518513
}
519514
}
@@ -523,35 +518,23 @@
523518
"content": {
524519
"application/json": {
525520
"schema": {
526-
"description": "Bad Request - Invalid input or invalid credentials.",
527521
"type": "object",
528522
"properties": {
529523
"success": {
530-
"type": "boolean"
524+
"type": "boolean",
525+
"description": "Indicates if the operation was successful (typically false for errors).",
526+
"default": false
531527
},
532528
"error": {
533529
"type": "string",
534530
"description": "Error message."
535531
}
536532
},
537533
"required": [
538-
"success",
539534
"error"
540-
]
541-
},
542-
"examples": {
543-
"example1": {
544-
"value": {
545-
"success": false,
546-
"error": "Email/username and password are required."
547-
}
548-
},
549-
"example2": {
550-
"value": {
551-
"success": false,
552-
"error": "Invalid email/username or password."
553-
}
554-
}
535+
],
536+
"additionalProperties": false,
537+
"description": "Bad Request - Invalid input or invalid credentials."
555538
}
556539
}
557540
}
@@ -561,24 +544,23 @@
561544
"content": {
562545
"application/json": {
563546
"schema": {
564-
"description": "Forbidden - Login is disabled by administrator.",
565547
"type": "object",
566548
"properties": {
567549
"success": {
568-
"type": "boolean"
550+
"type": "boolean",
551+
"description": "Indicates if the operation was successful (typically false for errors).",
552+
"default": false
569553
},
570554
"error": {
571-
"type": "string"
555+
"type": "string",
556+
"description": "Error message."
572557
}
573558
},
574559
"required": [
575-
"success",
576560
"error"
577-
]
578-
},
579-
"example": {
580-
"success": false,
581-
"error": "Login is currently disabled by administrator."
561+
],
562+
"additionalProperties": false,
563+
"description": "Forbidden - Login is disabled by administrator."
582564
}
583565
}
584566
}
@@ -588,40 +570,23 @@
588570
"content": {
589571
"application/json": {
590572
"schema": {
591-
"description": "Internal Server Error - An unexpected error occurred on the server.",
592573
"type": "object",
593574
"properties": {
594575
"success": {
595-
"type": "boolean"
576+
"type": "boolean",
577+
"description": "Indicates if the operation was successful (typically false for errors).",
578+
"default": false
596579
},
597580
"error": {
598-
"type": "string"
581+
"type": "string",
582+
"description": "Error message."
599583
}
600584
},
601585
"required": [
602-
"success",
603586
"error"
604-
]
605-
},
606-
"examples": {
607-
"example1": {
608-
"value": {
609-
"success": false,
610-
"error": "An unexpected error occurred during login."
611-
}
612-
},
613-
"example2": {
614-
"value": {
615-
"success": false,
616-
"error": "Internal server error: User table configuration missing."
617-
}
618-
},
619-
"example3": {
620-
"value": {
621-
"success": false,
622-
"error": "User ID not found."
623-
}
624-
}
587+
],
588+
"additionalProperties": false,
589+
"description": "Internal Server Error - An unexpected error occurred on the server."
625590
}
626591
}
627592
}
@@ -661,7 +626,7 @@
661626
],
662627
"responses": {
663628
"200": {
664-
"description": "Default Response",
629+
"description": "{\"examples\":[{\"success\":true,\"message\":\"Logged out successfully.\"},{\"success\":true,\"message\":\"No active session to logout or already logged out.\"}]}",
665630
"content": {
666631
"application/json": {
667632
"schema": {
@@ -679,7 +644,9 @@
679644
"required": [
680645
"success",
681646
"message"
682-
]
647+
],
648+
"additionalProperties": false,
649+
"description": "{\"examples\":[{\"success\":true,\"message\":\"Logged out successfully.\"},{\"success\":true,\"message\":\"No active session to logout or already logged out.\"}]}"
683650
},
684651
"examples": {
685652
"example1": {

0 commit comments

Comments
 (0)