GraphQL / Apollo Server Integration
QuickModel works naturally as the Input/Output DTO layer in a GraphQL API. It handles coercion of raw resolver arguments, validates before persisting, serializes clean responses, and provides computed fields without resolver logic.
Architecture
GraphQL Client ──► Resolver args ──► QModel Input DTO ──► Service / DB
GraphQL Client ◄── Resolver return ◄── QModel Output DTO ◄── DB rowsInput DTO in a mutation resolver
import { QModel, Quick, QRule, QField } from 'quickmodel';
import { qCheckRules } from 'quickmodel/forms';
interface ICreateUserInput {
name: string;
email: string;
age: number;
role: string;
}
@Quick(
{ name: 'string', email: 'string', age: 'number', role: 'string' },
{ unknownPropertyPolicy: 'strip', coercionStrategy: 'loose' }
)
class CreateUserInput extends QModel<ICreateUserInput> {
@QField({ label: 'Name', required: true })
@QRule((val: string) => val.trim().length >= 2, 'Name is too short')
declare name: string;
@QField({ label: 'Email', required: true })
@QRule((val: string) => val.includes('@'), 'Invalid email')
declare email: string;
@QField({ label: 'Age' })
@QRule((val: number) => val >= 0 && val <= 120, 'Age out of range')
declare age: number;
@QField({ label: 'Role', required: true })
@QRule(
(val: string) => ['admin', 'user', 'guest'].includes(val),
'Invalid role'
)
declare role: string;
}
// Apollo Server / GraphQL Yoga resolver
const resolvers = {
Mutation: {
createUser: async (_: unknown, args: { input: ICreateUserInput }) => {
const dto = new CreateUserInput(args.input);
const { valid, errors } = qCheckRules(dto);
if (!valid) {
throw new GraphQLError('Validation failed', {
extensions: {
code: 'BAD_USER_INPUT',
errors: errors.map(({ field, message }) => ({
field,
message,
})),
},
});
}
const saved = await userRepository.save(dto.$qToInterface());
return new UserResponse(saved);
},
},
};Async rules — uniqueness check before mutation
import { qCheckRulesAsync } from 'quickmodel/forms';
@Quick({ name: 'string', email: 'string', age: 'number', role: 'string' }, {})
class CreateUserInput extends QModel<ICreateUserInput> {
declare name: string;
@QRule(
(val: string) => Promise.resolve(!existingEmails.has(val)),
'Email already taken'
)
declare email: string;
declare age: number;
declare role: string;
}
// In resolver:
const { valid, errors } = await qCheckRulesAsync(dto);
if (!valid)
throw new GraphQLError('Duplicate email', { extensions: { errors } });Output DTO with @QComputed fields
Use @QComputed() to add derived fields to the response without putting logic in the resolver:
@Quick(
{
id: 'number',
name: 'string',
email: 'string',
role: 'string',
bio: 'string',
},
{ unknownPropertyPolicy: 'strip' }
)
class UserResponse extends QModel<IUserResponse> {
declare id: number;
declare name: string;
declare email: string;
declare role: string;
declare bio: string;
@QComputed()
get displayName(): string {
return `${this.name} (${this.role})`;
}
@QComputed()
get isAdmin(): boolean {
return this.role === 'admin';
}
@QComputed()
get initials(): string {
return this.name
.split(' ')
.map((w) => w[0] ?? '')
.join('')
.toUpperCase();
}
}
// In query resolver:
const resolvers = {
Query: {
user: async (_: unknown, { id }: { id: number }) => {
const raw = await db.users.findById(id);
return new UserResponse(raw); // @QComputed fields available in response
},
},
};createMany() for list queries
const resolvers = {
Query: {
users: async () => {
const rows = await db.users.findAll();
const { instances, errors } = UserResponse.createMany(rows);
if (errors.length > 0) {
console.warn('Skipping invalid rows:', errors);
}
return instances.map((dto) => dto.$qSerialize());
},
},
};$qCopy() in update mutations
const resolvers = {
Mutation: {
updatePost: async (
_: unknown,
{ id, input }: { id: number; input: IUpdatePostInput }
) => {
const existing = await postRepository.findById(id);
if (!existing)
throw new GraphQLError('Not found', {
extensions: { code: 'NOT_FOUND' },
});
const updated = existing.$qCopy(input);
const { valid, errors } = qCheckRules(updated);
if (!valid)
throw new GraphQLError('Validation failed', {
extensions: { errors },
});
await postRepository.update(id, updated.$qToInterface());
return updated.$qSerialize();
},
},
};Validation middleware (GraphQL Yoga useValidation pattern)
// Generic validator — works with any QModel Input class
function validateInput<T>(
DtoClass: { new (data: T): QModel<T> },
input: T
): { dto: QModel<T>; errors: Array<{ field: string; message: string }> } {
const dto = new DtoClass(input);
const { errors } = qCheckRules(dto);
return { dto, errors };
}
// Usage in resolvers:
const { dto, errors } = validateInput(CreateUserInput, args.input);
if (errors.length)
throw new GraphQLError('Validation failed', { extensions: { errors } });Schema integration with getSchema('graphql') and fromSchema('graphql', ...)
Export a GraphQL SDL type from a QModel
Import quickmodel/schema once, then call getSchema('graphql') to get a GraphQL Schema Definition Language (SDL) type string:
import 'quickmodel/schema';
import { QModel, Quick } from 'quickmodel';
interface IUser {
name: string;
email: string;
birthDate: Date;
score: number;
}
@Quick({ birthDate: Date, score: Number })
class User extends QModel<IUser> {
declare name: string;
declare email: string;
declare birthDate: Date;
declare score: number;
}
const sdl = User.getSchema('graphql');
/*
type User {
name: String!
email: String!
birthDate: String!
score: Float!
}
*/Use the SDL with code-gen tools
import 'quickmodel/schema';
import { makeExecutableSchema } from '@graphql-tools/schema';
// Build a full GraphQL schema from your QModel definitions
const typeDefs = [
User.getSchema('graphql'),
Product.getSchema('graphql'),
`type Query { user(id: ID!): User, products: [Product!]! }`,
];
const schema = makeExecutableSchema({ typeDefs, resolvers });Scaffold a QModel from existing GraphQL SDL
fromSchema('graphql', ...) converts a GraphQL SDL type block into a ready-to-use QModel class definition. This is scaffolding — the result is source code to save as a .ts file:
import 'quickmodel/schema';
const sdl = `
type Product {
id: ID!
name: String!
price: Float!
createdAt: String!
inStock: Boolean!
}
`;
const code = QModel.fromSchema('graphql', sdl, 'Product');
// → TypeScript source:
// interface IProduct { id: string; name: string; price: number; createdAt: string; inStock: boolean; }
// @Quick({ price: Number, inStock: Boolean })
// class Product extends QModel<IProduct> { ... }
// Save to disk:
// fs.writeFileSync('src/models/product.model.ts', code);Generate JSON / OpenAPI schemas for code-gen tools
// graphql-codegen, Pothos, type-graphql tooling
const inputSchema = CreateUserInput.getSchema('json');
const responseSchema = UserResponse.getSchema('openapi');NestJS Code First — @InputType() compatibility
QModel decorators (@QField, @QRule, @QGroup) are separate from NestJS decorators and can be stacked:
import { InputType, Field } from '@nestjs/graphql';
import { QModel, Quick, QField, QRule } from 'quickmodel';
@InputType()
@Quick({ name: 'string', email: 'string' }, { unknownPropertyPolicy: 'strip' })
class CreateUserInput extends QModel<{ name: string; email: string }> {
@Field()
@QField({ label: 'Name', required: true })
@QRule((v: string) => v.length >= 2, 'Too short')
declare name: string;
@Field()
@QField({ label: 'Email', required: true })
@QRule((v: string) => v.includes('@'), 'Invalid email')
declare email: string;
}