Skip to content

Backend Integration (Express / Fastify / Hono) ​

QuickModel works as a DTO layer for Node.js backend frameworks. It provides automatic type coercion, unknown-field stripping, validation, and @QComputed response enrichment — replacing class-validator + class-transformer in any runtime.

Key Patterns ​

ConcernQuickModel solution
Request body coercion@Quick({ ... }) + new Dto(req.body)
Unknown field strippingunknownPropertyPolicy: 'strip'
Validation (sync/async)dto.$qCheckRules() / qCheckRulesAsync(dto)
Response enrichment@QComputed() + dto.$qSerialize()
Bulk data loadingDto.createMany(array)

Express ​

Validation Middleware Factory ​

typescript
// middleware/validate-body.ts
import type { Request, Response, NextFunction } from 'express';
import { QModel } from 'quickmodel';

export function validateBody<TDto extends QModel<object>>(
	DtoClass: new (data: object) => TDto
) {
	return (
		req: Request & { dto?: TDto },
		res: Response,
		next: NextFunction
	) => {
		try {
			const dto = new DtoClass(req.body);
			const validation = dto.$qCheckRules();
			if (!validation.valid) {
				res.status(422).json({ errors: validation.errors });
				return;
			}
			req.dto = dto; // TDto — inferred from DtoClass, no cast needed
			next();
		} catch {
			res.status(400).json({ error: 'Invalid request body' });
		}
	};
}

DTO Definition ​

typescript
// dto/create-user.dto.ts
import { QModel, Quick, QRule, QField, QComputed } from 'quickmodel';

@Quick(
	{ username: 'string', email: 'string', age: 'number', role: 'string' },
	{ unknownPropertyPolicy: 'strip', coercionStrategy: 'loose' }
)
export class CreateUserDto extends QModel<ICreateUser> {
	@QField({ label: 'Username', required: true })
	@QRule((v: string) => v.length >= 3, 'Username too short')
	declare username: string;

	@QField({ label: 'Email', widget: 'email' })
	@QRule((v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), 'Invalid email')
	declare email: string;

	@QField({ label: 'Age' })
	@QRule((v: number) => v >= 18, 'Must be at least 18')
	declare age: number;

	@QField({ label: 'Role' })
	@QRule(
		(v: string) => ['admin', 'editor', 'viewer'].includes(v),
		'Invalid role'
	)
	declare role: string;

	@QComputed()
	get displayName(): string {
		return `${this.username} (${this.role})`;
	}
}

Route Handler ​

typescript
// routes/users.ts
import { Router } from 'express';
import { validateBody } from '../middleware/validate-body';
import { CreateUserDto } from '../dto/create-user.dto';

const router = Router();

router.post(
	'/users',
	validateBody(CreateUserDto),
	(req: Request & { dto?: CreateUserDto }, res) => {
		const dto = req.dto!; // CreateUserDto — fully typed, inferred from validateBody()
		// dto is coerced, validated, and stripped
		res.status(201).json(dto.$qSerialize());
		// response includes @QComputed displayName
	}
);

Fastify ​

Plugin / preHandler Hook ​

typescript
// plugins/dto-validation.ts
import type { FastifyRequest, FastifyReply } from 'fastify';
import { QModel } from 'quickmodel';

export function dtoValidator<TDto extends QModel<object>>(
	DtoClass: new (data: object) => TDto
) {
	return async (
		request: FastifyRequest & { dto?: TDto },
		reply: FastifyReply
	) => {
		try {
			const dto = new DtoClass(request.body as object);
			const validation = dto.$qCheckRules();
			if (!validation.valid) {
				reply.code(422).send({ errors: validation.errors });
				return;
			}
			request.dto = dto; // TDto — inferred from DtoClass, no cast needed
		} catch {
			reply.code(400).send({ error: 'Malformed request' });
		}
	};
}

Route Registration ​

typescript
// routes/invoices.ts
fastify.post('/invoices', {
	preHandler: dtoValidator(CreateInvoiceDto),
	handler: async (
		request: FastifyRequest & { dto?: CreateInvoiceDto },
		reply
	) => {
		const dto = request.dto!; // CreateInvoiceDto — fully typed, inferred from dtoValidator()
		const saved = await invoiceService.save(dto.$qSerialize());
		reply.code(201).send(saved);
	},
});

Response Serialization with @QComputed ​

typescript
@Quick(
	{
		id: 'string',
		amount: 'number',
		currency: 'string',
		dueDate: Date,
		paid: 'boolean',
	},
	{ unknownPropertyPolicy: 'strip' }
)
class InvoiceDto extends QModel<IInvoice> {
	declare id: string;
	declare amount: number;
	declare currency: string;
	declare dueDate: Date;
	declare paid: boolean;

	@QComputed()
	get formattedAmount(): string {
		return `${this.amount.toFixed(2)} ${this.currency.toUpperCase()}`;
	}

	@QComputed()
	get isOverdue(): boolean {
		return !this.paid && this.dueDate < new Date();
	}
}

Hono ​

Validator Middleware ​

typescript
// middleware/q-validator.ts
import type { Context, Next } from 'hono';
import { QModel } from 'quickmodel';

export function qValidator<TDto extends QModel<object>>(
	DtoClass: new (data: object) => TDto,
	onSuccess: (dto: TDto, c: Context) => Promise<Response>
) {
	return async (c: Context, next: Next) => {
		const body = await c.req.json<object>();
		try {
			const dto = new DtoClass(body);
			const validation = dto.$qCheckRules();
			if (!validation.valid) {
				return c.json({ errors: validation.errors }, 422);
			}
			return await onSuccess(dto, c);
		} catch {
			return c.json({ error: 'Bad request' }, 400);
		}
	};
}

Route Definition ​

typescript
// routes/users.ts
import { Hono } from 'hono';
import { qValidator } from '../middleware/q-validator';

const app = new Hono();

app.post(
	'/users',
	qValidator(CreateUserDto, async (dto, c) => {
		return c.json(dto.$qSerialize(), 201);
	})
);

Async Validation — Duplicate Checks ​

typescript
import { qCheckRulesAsync } from 'quickmodel/forms';

class RegistrationDto extends QModel<IRegistration> {
	@QRule(async (email: string) => {
		const exists = await db.users.exists({ email });
		return !exists;
	}, 'Email already registered')
	declare email: string;

	// ... other fields
}

// In your route handler
const dto = new RegistrationDto(req.body);
const result = await qCheckRulesAsync(dto, { mode: 'parallel' });
if (!result.valid) {
	return res.status(422).json({ errors: result.errors });
}

Repository Pattern ​

typescript
// repositories/blog-post.repository.ts
export class BlogPostRepository {
	private store = new Map<string, BlogPostModel>();

	create(data: object): object {
		const post = new BlogPostModel(data);
		this.store.set(post.id, post);
		return post.$qSerialize();
	}

	publish(id: string): object | null {
		const post = this.store.get(id);
		if (!post) return null;
		// $qCopy() is IMMUTABLE — capture the new instance
		const published = post.$qCopy({ publishedAt: new Date() });
		this.store.set(id, published);
		return published.$qSerialize();
	}
}

Batch Processing with createMany() ​

typescript
// Process incoming bulk data from message queues or batch APIs
const rawItems: unknown[] = await queue.receive();
const { instances, errors } = OrderItemDto.createMany(rawItems);

// Handle coercion errors
if (errors.length) {
	logger.warn(`${errors.length} items failed coercion`, errors);
}

// Process valid instances
await orderService.bulkCreate(instances.map((i) => i.$qSerialize()));

coercionStrategy: 'loose'

When receiving data from external APIs or message queues, add coercionStrategy: 'loose' to automatically convert strings to the expected primitive types. This avoids throwing on "3" when 3 is expected.

JSON Schema for OpenAPI / Swagger ​

QuickModel can generate a JSON schema for your DTOs:

typescript
const schema = new CreateUserDto({ ... }).getSchema('json');
// Integrate with @fastify/swagger or swagger-jsdoc
fastify.addSchema({
  $id: 'CreateUserBody',
  ...schema,
});