Skip to content

Commit 479c100

Browse files
authored
Merge pull request #171 from deploystackio/feat/ui
Feat/UI
2 parents 5598074 + 0957fa9 commit 479c100

6 files changed

Lines changed: 743 additions & 80 deletions

File tree

docs/deploystack/development/backend/api-pagination.mdx

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ const paginatedResponseSchema = z.object({
121121

122122
```typescript
123123
import { z } from 'zod';
124-
import { zodToJsonSchema } from 'zod-to-json-schema';
124+
import { createSchema } from 'zod-openapi';
125125

126126
// Query parameters (including pagination)
127127
const querySchema = z.object({
@@ -168,15 +168,9 @@ export default async function listItems(server: FastifyInstance) {
168168
tags: ['Items'],
169169
summary: 'List items with pagination',
170170
description: 'Retrieve items with pagination support. Supports filtering and sorting.',
171-
querystring: zodToJsonSchema(querySchema, {
172-
$refStrategy: 'none',
173-
target: 'openApi3'
174-
}),
171+
querystring: createSchema(querySchema),
175172
response: {
176-
200: zodToJsonSchema(responseSchema, {
177-
$refStrategy: 'none',
178-
target: 'openApi3'
179-
})
173+
200: createSchema(responseSchema)
180174
}
181175
}
182176
}, async (request, reply) => {
@@ -559,15 +553,9 @@ export default async function listServers(server: FastifyInstance) {
559553
tags: ['MCP Servers'],
560554
summary: 'List MCP servers',
561555
description: 'Retrieve MCP servers with pagination support...',
562-
querystring: zodToJsonSchema(querySchema, {
563-
$refStrategy: 'none',
564-
target: 'openApi3'
565-
}),
556+
querystring: createSchema(querySchema),
566557
response: {
567-
200: zodToJsonSchema(listServersResponseSchema, {
568-
$refStrategy: 'none',
569-
target: 'openApi3'
570-
})
558+
200: createSchema(listServersResponseSchema)
571559
}
572560
}
573561
}, async (request, reply) => {

docs/deploystack/development/backend/api-security.mdx

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,30 +65,15 @@ export default async function secureRoute(fastify: FastifyInstance) {
6565
summary: 'Protected endpoint',
6666
description: 'Requires admin permissions',
6767
security: [{ cookieAuth: [] }],
68-
body: zodToJsonSchema(RequestSchema, {
69-
$refStrategy: 'none',
70-
target: 'openApi3'
71-
}),
68+
body: createSchema(RequestSchema),
7269
response: {
73-
200: zodToJsonSchema(SuccessResponseSchema, {
74-
$refStrategy: 'none',
75-
target: 'openApi3'
76-
}),
77-
401: zodToJsonSchema(ErrorResponseSchema.describe('Unauthorized'), {
78-
$refStrategy: 'none',
79-
target: 'openApi3'
80-
}),
81-
403: zodToJsonSchema(ErrorResponseSchema.describe('Forbidden'), {
82-
$refStrategy: 'none',
83-
target: 'openApi3'
84-
}),
85-
400: zodToJsonSchema(ErrorResponseSchema.describe('Bad Request'), {
86-
$refStrategy: 'none',
87-
target: 'openApi3'
88-
})
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'))
8974
}
9075
},
91-
preValidation: requireGlobalAdmin(), // ✅ CORRECT: Runs before validation
76+
preValidation: requireGlobalAdmin(), // ✅ CORRECT: Runs before validation,
9277
}, async (request, reply) => {
9378
// If we reach here, user is authorized AND input is validated
9479
const validatedData = request.body;

docs/deploystack/development/backend/api.mdx

Lines changed: 134 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This document explains how to generate and use the OpenAPI specification for the
99

1010
## Overview
1111

12-
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:
12+
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-openapi](https://www.npmjs.com/package/zod-openapi) library. This provides:
1313

1414
- **Interactive Documentation**: Swagger UI interface for testing APIs
1515
- **Postman Integration**: JSON/YAML specs that can be imported into Postman
@@ -171,7 +171,7 @@ Each route file should follow this pattern:
171171
```typescript
172172
import { type FastifyInstance } from 'fastify'
173173
import { z } from 'zod'
174-
import { zodToJsonSchema } from 'zod-to-json-schema'
174+
import { createSchema } from 'zod-openapi'
175175

176176
// Define your schemas
177177
const responseSchema = z.object({
@@ -185,10 +185,7 @@ export default async function yourRoute(server: FastifyInstance) {
185185
summary: 'Brief description',
186186
description: 'Detailed description',
187187
response: {
188-
200: zodToJsonSchema(responseSchema, {
189-
$refStrategy: 'none',
190-
target: 'openApi3'
191-
})
188+
200: createSchema(responseSchema)
192189
}
193190
}
194191
}, async () => {
@@ -311,7 +308,7 @@ const routeSchema = {
311308
required: true,
312309
content: {
313310
'application/json': {
314-
schema: zodToJsonSchema(requestSchema, { $refStrategy: 'none', target: 'openApi3' })
311+
schema: createSchema(requestSchema)
315312
}
316313
}
317314
},
@@ -321,17 +318,17 @@ const routeSchema = {
321318

322319
## Adding Documentation to Routes
323320

324-
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.
321+
To add OpenAPI documentation to your routes, define your request body and response schemas using Zod. Then, use the `createSchema` utility to convert these Zod schemas into the JSON Schema format expected by Fastify.
325322

326-
Make sure you have `zod` and `zod-to-json-schema` installed in your backend service.
323+
Make sure you have `zod` and `zod-openapi` installed in your backend service.
327324

328-
### Recommended Approach: Automatic Validation with Zod
325+
### Recommended Approach: Dual-Schema Pattern for Validation + Documentation
329326

330-
The power of Zod lies in providing **automatic validation** through Fastify's schema system. This approach eliminates manual validation and leverages Zod's full validation capabilities.
327+
**IMPORTANT**: After the Zod v4 migration, we use a **dual-schema approach** to ensure both proper Fastify validation and accurate OpenAPI documentation.
331328

332329
```typescript
333330
import { z } from 'zod';
334-
import { zodToJsonSchema } from 'zod-to-json-schema';
331+
import { createSchema } from 'zod-openapi';
335332

336333
// 1. Define your Zod schemas for request body, responses, etc.
337334
const myRequestBodySchema = z.object({
@@ -351,36 +348,37 @@ const myErrorResponseSchema = z.object({
351348
error: z.string().describe("Error message detailing what went wrong")
352349
});
353350

354-
// 2. Construct the Fastify route schema using zodToJsonSchema
351+
// 2. Construct the Fastify route schema using DUAL-SCHEMA PATTERN
355352
const routeSchema = {
356353
tags: ['Category'], // Your API category
357354
summary: 'Brief description of your endpoint',
358355
description: 'Detailed description of what this endpoint does, its parameters, and expected outcomes. Requires Content-Type: application/json header when sending request body.',
359356
security: [{ cookieAuth: [] }], // Include if authentication is required
360-
body: zodToJsonSchema(myRequestBodySchema, {
361-
$refStrategy: 'none', // Keeps definitions inline, often simpler for Fastify
362-
target: 'openApi3' // Ensures compatibility with OpenAPI 3.0
363-
}),
357+
358+
// ✅ CRITICAL: Use plain JSON Schema for Fastify validation
359+
body: {
360+
type: 'object',
361+
properties: {
362+
name: { type: 'string', minLength: 3 },
363+
count: { type: 'number', minimum: 1 },
364+
type: { type: 'string', enum: ['mysql', 'sqlite'] }
365+
},
366+
required: ['name', 'count', 'type'],
367+
additionalProperties: false
368+
},
369+
370+
// ✅ Use createSchema() for OpenAPI documentation
364371
requestBody: {
365372
required: true,
366373
content: {
367374
'application/json': {
368-
schema: zodToJsonSchema(myRequestBodySchema, {
369-
$refStrategy: 'none',
370-
target: 'openApi3'
371-
})
375+
schema: createSchema(myRequestBodySchema)
372376
}
373377
}
374378
},
375379
response: {
376-
200: zodToJsonSchema(mySuccessResponseSchema.describe("Successful operation"), {
377-
$refStrategy: 'none',
378-
target: 'openApi3'
379-
}),
380-
400: zodToJsonSchema(myErrorResponseSchema.describe("Bad Request - Invalid input"), {
381-
$refStrategy: 'none',
382-
target: 'openApi3'
383-
}),
380+
200: createSchema(mySuccessResponseSchema.describe("Successful operation")),
381+
400: createSchema(myErrorResponseSchema.describe("Bad Request - Invalid input")),
384382
// Define other responses (e.g., 401, 403, 404, 500) similarly
385383
}
386384
};
@@ -396,18 +394,20 @@ fastify.post<{ Body: RequestBody }>(
396394
'/your-route',
397395
{ schema: routeSchema },
398396
async (request, reply) => {
399-
// ✅ Fastify has already validated request.body using our Zod schema
397+
// ✅ Fastify has already validated request.body using the JSON schema
400398
// ✅ If we reach here, request.body is guaranteed to be valid
401399
// ✅ No manual validation needed!
402400

403401
const { name, count, type } = request.body; // Fully typed and validated
404402

405403
// Your route handler logic here
406-
return reply.status(200).send({
404+
const successResponse = {
407405
success: true,
408406
itemId: 'some-uuid-v4-here',
409407
message: `Item ${name} processed successfully with ${count} items using ${type}.`
410-
});
408+
};
409+
const jsonString = JSON.stringify(successResponse);
410+
return reply.status(200).type('application/json').send(jsonString);
411411
}
412412
);
413413
```
@@ -421,6 +421,78 @@ fastify.post<{ Body: RequestBody }>(
421421
5. **Type Safety**: Handlers receive properly typed, validated data
422422
6. **Cleaner Code**: No redundant validation logic in handlers
423423

424+
## JSON Response Serialization Pattern
425+
426+
**CRITICAL**: After the Zod v4 migration, all API responses must use manual JSON serialization to prevent `"[object Object]"` serialization issues.
427+
428+
### Required Response Pattern
429+
430+
```typescript
431+
// ✅ CORRECT: Manual JSON serialization
432+
const successResponse = {
433+
success: true,
434+
message: 'Operation completed successfully',
435+
data: { /* your data */ }
436+
};
437+
const jsonString = JSON.stringify(successResponse);
438+
return reply.status(200).type('application/json').send(jsonString);
439+
```
440+
441+
### What NOT to Do
442+
443+
```typescript
444+
// ❌ WRONG: Direct object response (causes serialization issues)
445+
return reply.status(200).send({
446+
success: true,
447+
message: 'This will become "[object Object]"'
448+
});
449+
450+
// ❌ WRONG: Using reply.send() without JSON.stringify()
451+
const response = { success: true, message: 'Test' };
452+
return reply.status(200).send(response);
453+
```
454+
455+
### Error Response Pattern
456+
457+
All error responses must also use manual JSON serialization:
458+
459+
```typescript
460+
// ✅ CORRECT: Error response with manual serialization
461+
const errorResponse = {
462+
success: false,
463+
error: 'Detailed error message'
464+
};
465+
const jsonString = JSON.stringify(errorResponse);
466+
return reply.status(400).type('application/json').send(jsonString);
467+
```
468+
469+
### Authentication Middleware Pattern
470+
471+
Authentication middleware and hooks must also use this pattern:
472+
473+
```typescript
474+
// ✅ CORRECT: Authentication error with manual serialization
475+
const errorResponse = {
476+
success: false,
477+
error: 'Unauthorized: Authentication required.'
478+
};
479+
const jsonString = JSON.stringify(errorResponse);
480+
return reply.status(401).type('application/json').send(jsonString);
481+
```
482+
483+
### Why This Pattern is Required
484+
485+
After the Zod v4 migration, Fastify's automatic JSON serialization can fail with complex objects, resulting in:
486+
- Response bodies showing `"[object Object]"` instead of actual data
487+
- Client applications receiving unparseable responses
488+
- Test failures due to missing `success` and `error` properties
489+
490+
The manual JSON serialization pattern ensures:
491+
- ✅ Consistent, parseable JSON responses
492+
- ✅ Proper `success`/`error` properties in all responses
493+
- ✅ Reliable client-server communication
494+
- ✅ Passing e2e tests
495+
424496
### Why Both `body` and `requestBody` Properties?
425497

426498
**Important**: You need BOTH properties for complete functionality:
@@ -466,7 +538,7 @@ The validation chain works as follows:
466538
#### Zod Schema → JSON Schema → Fastify Validation → Handler
467539

468540
1. **Zod Schema**: Define validation rules using Zod
469-
2. **JSON Schema**: Convert to OpenAPI format using `zodToJsonSchema()`
541+
2. **JSON Schema**: Convert to OpenAPI format using `createSchema()`
470542
3. **Fastify Validation**: Fastify automatically validates incoming requests
471543
4. **Handler**: Receives validated, typed data
472544

@@ -520,7 +592,35 @@ const logoutSchema = {
520592

521593
## Configuration
522594

523-
The Swagger configuration is in `src/server.ts`:
595+
### Fastify Server Configuration
596+
597+
The Fastify server is configured with custom AJV options to ensure compatibility with `zod-openapi` schema generation. This configuration is in `src/server.ts`:
598+
599+
```typescript
600+
const server = fastify({
601+
logger: loggerConfig,
602+
disableRequestLogging: true,
603+
ajv: {
604+
customOptions: {
605+
strict: false, // Allows unknown keywords in schemas
606+
strictTypes: false, // Disables strict type checking
607+
strictTuples: false // Disables strict tuple checking
608+
}
609+
}
610+
})
611+
```
612+
613+
**Why these AJV options are required:**
614+
615+
- **`strict: false`**: AJV v8+ runs in strict mode by default, which rejects schemas containing unknown keywords. The `zod-openapi` library generates schemas that may include keywords AJV doesn't recognize in strict mode.
616+
- **`strictTypes: false`**: Prevents strict type validation errors that can occur with complex Zod schemas.
617+
- **`strictTuples: false`**: Allows more flexible tuple handling for array schemas.
618+
619+
**Important**: These settings don't affect validation behavior - they only allow the schema compilation to succeed. All validation rules defined in your Zod schemas still work exactly as expected.
620+
621+
### Swagger Configuration
622+
623+
The Swagger documentation configuration is also in `src/server.ts`:
524624

525625
```typescript
526626
await server.register(fastifySwagger, {

docs/deploystack/development/backend/database-sqlite.mdx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,5 +444,3 @@ Consider hybrid approaches for scaling:
444444
For general database concepts and cross-database functionality, see the [Database Development Guide](/deploystack/development/backend/database).
445445

446446
For initial setup and configuration, see the [Database Setup Guide](/deploystack/self-hosted/database-setup).
447-
448-
For comparison with other databases, see the [Cloudflare D1 Development Guide](/deploystack/development/backend/database-d1).

docs/deploystack/development/backend/oauth.mdx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ Create the OAuth routes file:
149149
// services/backend/src/routes/auth/google.ts
150150
import type { FastifyInstance, FastifyReply } from 'fastify';
151151
import { z } from 'zod';
152-
import { zodToJsonSchema } from 'zod-to-json-schema';
152+
import { createSchema } from 'zod-openapi';
153153
import { getLucia } from '../../lib/lucia';
154154
import { getDb, getSchema } from '../../db';
155155
import { eq } from 'drizzle-orm';
@@ -379,7 +379,7 @@ Create a status endpoint for the provider:
379379
// services/backend/src/routes/auth/googleStatus.ts
380380
import type { FastifyInstance } from 'fastify';
381381
import { z } from 'zod';
382-
import { zodToJsonSchema } from 'zod-to-json-schema';
382+
import { createSchema } from 'zod-openapi';
383383
import { GlobalSettingsInitService } from '../../global-settings';
384384

385385
const GoogleStatusResponseSchema = z.object({
@@ -395,10 +395,7 @@ export default async function googleStatusRoutes(fastify: FastifyInstance) {
395395
summary: 'Get Google OAuth status',
396396
description: 'Returns the current status and configuration of Google OAuth',
397397
response: {
398-
200: zodToJsonSchema(GoogleStatusResponseSchema, {
399-
$refStrategy: 'none',
400-
target: 'openApi3'
401-
})
398+
200: createSchema(GoogleStatusResponseSchema)
402399
}
403400
}
404401
}, async (_request, reply) => {

0 commit comments

Comments
 (0)