MSW Integration
Mock Service Worker (MSW) intercepts network requests in tests and the browser. QuickModel works as the data layer inside MSW handlers — coercing request bodies into typed DTOs, validating them with @QRule, and returning well-typed serialized responses.
Key Patterns
| Pattern | QuickModel API |
|---|---|
| Request body coercion | new Dto(body) + coercionStrategy: 'loose' |
| Handler-side validation | dto.$qCheckRules() → 422 on error |
| Response serialization | dto.$qSerialize() → HttpResponse.json() |
| Fixture factories | new Dto(defaults) with $qSerialize() |
| Bulk mock data | Dto.createMany(seedArray) |
| Private field stripping | unknownPropertyPolicy: 'strip' |
Installation
npm install quickmodel mswModel Setup
Define separate DTOs for read (includes id) and write (user-supplied data only):
import { QModel, Quick, QRule, QField, QComputed } from 'quickmodel';
interface IUser {
id: string;
username: string;
email: string;
age: number;
role: string;
}
interface ICreateUser {
username: string;
email: string;
age: number;
role: string;
}
// Read DTO — for GET responses and store
@Quick(
{
id: 'string',
username: 'string',
email: 'string',
age: 'number',
role: 'string',
},
{ unknownPropertyPolicy: 'strip', coercionStrategy: 'loose' }
)
class UserDto extends QModel<IUser> {
declare id: string;
declare username: string;
declare email: string;
declare age: number;
declare role: string;
@QComputed()
get displayName(): string {
return `${this.username} (${this.role})`;
}
}
// Write DTO — for POST/PUT request bodies
@Quick(
{ username: 'string', email: 'string', age: 'number', role: 'string' },
{ unknownPropertyPolicy: 'strip', coercionStrategy: 'loose' }
)
class CreateUserDto extends QModel<ICreateUser> {
@QField({ label: 'Username', required: true })
@QRule((v: string) => v.length >= 3, 'Username too short')
@QRule((v: string) => /^[a-z0-9_]+$/.test(v), 'Invalid username format')
declare username: string;
@QField({ label: 'Email', widget: 'email', required: true })
@QRule(
(v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
'Invalid email address'
)
declare email: string;
@QField({ label: 'Age' })
@QRule((v: number) => v >= 18, 'Must be 18 or older')
declare age: number;
@QField({ label: 'Role' })
@QRule(
(v: string) => ['user', 'admin', 'editor'].includes(v),
'Invalid role'
)
declare role: string;
}
coercionStrategy: 'loose'onCreateUserDtohandles FormData or JSON requests that sendageas a string ("25"→25).
GET Handler
import { http, HttpResponse } from 'msw';
// In-memory store — seeded from typed DTOs
const { instances: seedUsers } = UserDto.createMany([
{
id: 'u1',
username: 'alice',
email: 'alice@example.com',
age: 30,
role: 'admin',
},
{
id: 'u2',
username: 'bob',
email: 'bob@example.com',
age: 25,
role: 'user',
},
]);
const store = new Map(seedUsers.map((u) => [u.id, u]));
export const handlers = [
http.get('/api/users/:id', ({ params }) => {
const user = store.get(params.id as string);
if (!user) {
return HttpResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
// $qSerialize() produces a JSON-safe plain object
return HttpResponse.json(user.$qSerialize(), { status: 200 });
}),
http.get('/api/users', () => {
const users = [...store.values()].map((u) => u.$qSerialize());
return HttpResponse.json(users, { status: 200 });
}),
];POST Handler — Validate Request Body
Coerce the request body with CreateUserDto then validate with qCheckRules():
http.post('/api/users', async ({ request }) => {
const body = await request.json();
// Coerce & sanitize — strips unknown fields automatically
const dto = new CreateUserDto(body);
const { valid, errors } = dto.$qCheckRules();
if (!valid) {
return HttpResponse.json({ errors }, { status: 422 });
}
// Add server-generated fields and persist
const created: IUser = {
...(dto.$qSerialize() as ICreateUser),
id: crypto.randomUUID(),
};
const savedUser = new UserDto(created);
store.set(savedUser.id, savedUser);
return HttpResponse.json(savedUser.$qSerialize(), { status: 201 });
});Reusable Fixture Factory
Create typed fixture factories for test data:
function createUserFixture(overrides: Partial<IUser> = {}): UserDto {
return new UserDto({
id: 'fixture-id',
username: 'test_user',
email: 'test@example.com',
age: 25,
role: 'user',
...overrides,
});
}
// Usage in tests
const admin = createUserFixture({ role: 'admin', username: 'admin_user' });
const response = HttpResponse.json(admin.$qSerialize(), { status: 200 });Using Fixtures in Tests
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get('/api/users/:id', ({ params }) => {
const fixture = createUserFixture({ id: params.id as string });
return HttpResponse.json(fixture.$qSerialize());
})
);
beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
test('GET /api/users/:id returns typed user', async () => {
const res = await fetch('/api/users/fixture-id');
const data = await res.json();
const user = new UserDto(data);
expect(user).toBeInstanceOf(QModel);
expect(user.displayName).toBe('test_user (user)');
});Stripping Private Fields
unknownPropertyPolicy: 'strip' ensures internal server fields (e.g., _csrf, _hash) are removed before serialization, preventing accidental exposure:
const rawServerResponse = {
id: 'u1',
username: 'alice',
email: 'alice@example.com',
age: 30,
role: 'admin',
_csrf: 'secret-token', // internal — will be stripped
_hash: 'abc123', // internal — will be stripped
};
const user = new UserDto(rawServerResponse);
const serialized = user.$qSerialize();
// serialized has no _csrf or _hash — safe to send to clientDate Coercion in Responses
QuickModel transforms ISO strings back to Date objects automatically:
@Quick(
{ id: 'string', title: 'string', body: 'string', createdAt: Date },
{ unknownPropertyPolicy: 'strip' }
)
class PostDto extends QModel<IPost> {
declare id: string;
declare title: string;
declare body: string;
declare createdAt: Date;
@QComputed()
get preview(): string {
return this.body.length > 100
? this.body.slice(0, 100) + '...'
: this.body;
}
}
// In handler — ISO string from DB becomes Date automatically:
const post = new PostDto({
id: '1',
title: 'Hello',
body: 'World',
createdAt: '2024-01-15T10:30:00Z',
});
console.log(post.createdAt instanceof Date); // true