Skip to content

Models

Models represent your database tables and define the structure of your data. This guide explains how to create and use models in StratusTS.

Models are TypeScript function that:

  • Define the structure of database tables
  • Specify column types and constraints
  • Provide methods to query and manipulate data
  • Ensure type safety when working with databases

Think of models as blueprints for your database tables.

Before working with models:

  1. Configure database in src/settings.ts (see Database Setup)
  2. Ensure database connection is working
  3. Understand basic SQL/database concepts

Create a model in src/users/models.ts:

src/users/models.ts
import { Model } from 'stratus-ts';
const User = Model('user', {
name: {
type: 'string',
required: true,
min: 2,
max: 100,
},
email: {
type: 'email',
unique: true,
required: true,
},
password: {
type: 'string',
required: true,
select: false, // Don't return in queries by default
},
age: {
type: 'number',
min: 18,
max: 100,
},
isActive: {
type: 'bool',
default: true,
},
});
export { User };

Model(tableName, schema) - Creates a model

  • First argument: Database table name
  • Second argument: Schema object defining fields

Field definition:

field definition
[fieldName]: {
type: 'string', // Field type
required: true, // Is field required?
unique: true, // Must be unique?
default: 'value', // Default value
min: 2, // Minimum value/length
max: 100, // Maximum value/length
select: false, // Include in queries? # in development
}
src/users/models.ts
const User = Model('user', {
name: {
type: 'string',
min: 2, // Minimum length
max: 100, // Maximum length
required: true,
},
username: {
type: 'string',
min: 3,
max: 30,
unique: true, // Must be unique
required: true,
},
bio: {
type: 'string',
max: 500,
required: false, // Optional field
},
});

Special string type with email validation:

src/users/models.ts
const User = Model('user', {
email: {
type: 'email',
unique: true,
required: true,
},
recoveryEmail: {
type: 'email',
required: false,
},
});
src/users/models.ts
const Product = Model('products', {
price: {
type: 'number',
min: 0, // Minimum value
max: 999999, // Maximum value
required: true,
},
stock: {
type: 'number',
default: 0,
min: 0,
},
rating: {
type: 'number',
min: 0,
max: 5,
default: 0,
},
});
src/users/models.ts
const User = Model('user', {
isActive: {
type: 'bool',
default: true,
},
isVerified: {
type: 'bool',
default: false,
},
isPremium: {
type: 'bool',
default: false,
},
});

Restricts values to specific options:

src/users/models.ts
const User = Model('user', {
gender: {
type: 'enum',
values: ['male', 'female', 'other'],
default: 'other',
},
role: {
type: 'enum',
values: ['user', 'admin', 'moderator'],
default: 'user',
required: true,
},
status: {
type: 'enum',
values: ['active', 'inactive', 'banned', 'pending'],
default: 'pending',
},
});

For date and time values:

src/users/models.ts
const User = Model('user', {
createdAt: {
type: 'timestamp',
default: 'now', // Set to current time
},
updatedAt: {
type: 'timestamp',
default: 'now',
},
lastLogin: {
type: 'timestamp'
},
verifiedAt: {
type: 'timestamp'
},
});

Makes field mandatory:

src/users/models.ts
const User = Model('user', {
email: {
type: 'email',
required: true, // Must be provided
},
phone: {
type: 'string', // Optional
},
});

Ensures field values are unique across all records:

src/users/models.ts
const User = Model('user', {
email: {
type: 'email',
unique: true, // No duplicate emails
},
username: {
type: 'string',
unique: true, // No duplicate usernames
},
age:{
type: 'number' // Optional
}
});

Sets default value if not provided:

src/users/models.ts
const User = Model('user', {
role: {
type: 'enum',
values: ['user', 'admin', 'moderator'],
default: 'user',
},
isActive: {
type: 'bool',
default: true,
},
createdAt: {
type: 'timestamp',
default: 'now',
},
points: {
type: 'number',
default: 0,
},
});

Controls if field is returned in queries:

src/users/models.ts
const User = Model('user', {
password: {
type: 'string',
required: true,
select: false, // Don't include in query results
},
email: {
type: 'email',
required: true,
select: true, // Include in results (default) don't need to add
},
});

For strings (length) and numbers (value):

src/users/models.ts
const User = Model('user', {
username: {
type: 'string',
min: 3, // Minimum 3 characters
max: 30, // Maximum 30 characters
},
age: {
type: 'number',
min: 13, // Minimum value 13
max: 120, // Maximum value 120
},
bio: {
type: 'string',
max: 500, // Maximum 500 characters
},
});
src/users/models.ts
import { Model } from 'stratus-ts';
const User = Model('user', {
// Basic info
username: {
type: 'string',
min: 3,
max: 30,
unique: true,
required: true,
},
email: {
type: 'email',
unique: true,
required: true,
},
password: {
type: 'string',
required: true,
min: 8,
select: false, // Doesn't return password by default untill ask
},
// Profile info
name: {
type: 'string',
min: 2,
max: 100,
},
age: {
type: 'number',
min: 13,
max: 120,
},
gender: {
type: 'enum',
values: ['male', 'female', 'other'],
default: 'other',
},
bio: {
type: 'string',
max: 500,
},
// Account status
role: {
type: 'enum',
values: ['user', 'admin', 'moderator'],
default: 'user',
},
isActive: {
type: 'bool',
default: true,
},
isVerified: {
type: 'bool',
default: false,
},
// Timestamps
verifiedAt: {
type: 'timestamp',
},
createdAt: {
type: 'timestamp',
default: 'now',
},
lastLogin: {
type: 'timestamp',
},
});
export { User };
src/users/controllers.ts
import { User } from './models';
src/users/controllers.ts
const createUser: ControllerType = async (question, reply) => {
const { body:{
username, email, password, age
} } = question.body();
const user = await User.create({
username,
email,
password,
age,
});
reply.status(created()).json({
success: true,
data: user,
});
};

Find all:

src/users/controllers.ts
const listUsers: ControllerType = async (question, reply) => {
const users = await User.find();
reply.status(ok()).json({
success: true,
data: users,
});
};

Find one by Primary Key/Id:

src/users/controllers.ts
const getUser: ControllerType = async (question, reply) => {
const { id } = question.params();
const user = await User.findByPK(id);
if (!user) {
return reply.status(notFound()).json({
success: false,
error: 'User not found',
});
}
reply.status(ok()).json({
success: true,
data: user,
});
};

Find with conditions:

src/users/services.ts
// Find by email
const user = await User.findOne({ email: 'alice@example.com' });
// Find active users
const activeUsers = await User.find({ isActive: true });
// Find verified users
const verifiedUsers = await User.find({ isVerified: true });
// Find by multiple conditions
// this will led to 'and' query
const admins = await User.find({
role: 'admin',
isActive: true,
});
src/users/controllers.ts
const updateUser: ControllerType = async (question, reply) => {
const { id } = question.params();
const {
body:{
name, bio, age
}
} = question.body();
const user = await User.findByPK(id);
if (!user) {
return reply.status(notFound()).json({
success: false,
error: 'User not found',
});
}
// Update fields
const updatedUser = await User.updateById(id, {
name,
bio,
age,
});
reply.status(ok()).json({
success: true,
data: updatedUser,
});
};
src/users/controllers.ts
const deleteUser: ControllerType = async (question, reply) => {
const { id } = question.params();
const result = await User.destroyByPK(id);
if (!result) {
return reply.status(notFound()).json({
success: false,
error: 'User not found',
});
}
reply.status(ok()).json({
success: true,
message: 'User deleted successfully',
});
};
src/users/controllers.ts
const listUsers: ControllerType = async (question, reply) => {
const { page = '1', limit = '10' } = question.query;
const pageNumber = parseInt(page);
const pageLimit = parseInt(limit);
const users = await User.find(undefined, undefined, {
skip: (pageNumber - 1) * pageLimit,
limit: pageLimit,
});
const total = (await User.find(undefined, {_id:true})).length;
reply.status(ok()).json({
success: true,
data: users,
pagination: {
page: pageNumber,
limit: pageLimit,
total,
pages: Math.ceil(total / pageLimit),
},
});
};
src/users/controllers.ts
// Sort by createdAt descending
const users = await User.find(undefined,undefined,{
orderBy:{
createdAt: 'DESC'
}
});
// Sort by username ascending
const users = await User.find(undefined,undefined,{
orderBy:{
username:'ASC'
}
});
// Multiple sort fields
const users = await User.find(undefined,undefined,{
orderBy:{
role:'ASC',
createdAt: 'DESC'
}
});
src/users/controllers.ts
// Only get username and email
const users = await User.find(undefined,{
email: true,
username: true
});
// Exclude password (if select: false)
const users = await User.find(undefined,{
email: true,
username: true
});
// Include password (if select: false)
const users = await User.find(undefined,{
email: true,
username: true,
password: true
});
src/users/controllers.ts
// Search by username
const users = await User.find({
username:'john'
});
// Search in multiple fields
const users = await User.find({
email: 'test@example.com',
username:'john'
});

Models automatically validate based on schema:

src/users/models.ts
const User = Model('user', {
email: {
type: 'email', // Validates email format
required: true, // Validates presence
unique: true, // Validates uniqueness
},
age: {
type: 'number',
min: 18, // Validates minimum value
max: 120, // Validates maximum value
},
username: {
type: 'string',
min: 3, // Validates minimum length
max: 30, // Validates maximum length
},
gender: {
type: 'enum',
values: ['male', 'female', 'other'], // Validates allowed values
},
});
// This will fail validation
try {
await User.create({
email: 'invalid-email', // Invalid email format
age: 15, // Below minimum
username: 'ab', // Too short
gender: 'unknown', // Not in enum values
});
} catch (error) {
// Validation error with details
console.error(error.message);
}
src/users/models.ts
// ✅ Good
const User = Model('user', {
username: { type: 'string' },
isEmailVerified: { type: 'bool' },
lastLoginAt: { type: 'timestamp' },
});
// ❌ Bad
const User = Model('user', {
uname: { type: 'string' },
verified: { type: 'bool' },
login: { type: 'timestamp' },
});
src/users/models.ts
const User = Model('user', {
password: {
type: 'string',
required: true,
select: false, // Never return passwords!
},
apiKey: {
type: 'string',
select: false, // Hide API keys
},
});
src/users/models.ts
// ✅ Good - type safe
const User = Model('user', {
role: {
type: 'enum',
values: ['user', 'admin', 'moderator'],
default: 'user',
},
});
// ❌ Bad - allows any string
const User = Model('user', {
role: {
type: 'string',
default: 'user',
},
});
src/users/models.ts
const User = Model('user', {
username: {
type: 'string',
min: 3, // Prevent single-character usernames
max: 30, // Prevent extremely long usernames
unique: true,
},
age: {
type: 'number',
min: 13, // Minimum age requirement
max: 120, // Reasonable maximum
},
});
src/users/models.ts
const User = Model('user', {
createdAt: {
type: 'timestamp',
default: 'now',
},
updatedAt: {
type: 'timestamp',
default: 'now',
},
});
src/users/models.ts
const User = Model('user', {
role: {
type: 'enum',
values: ['user', 'admin'],
default: 'user', // New users are 'user' by default
},
isActive: {
type: 'bool',
default: true, // Active by default
},
points: {
type: 'number',
default: 0, // Start with 0 points
},
});
src/users/models.ts
const User = Model('user', {
username: {
type: 'string',
min: 3,
max: 30,
unique: true,
required: true,
},
email: {
type: 'email',
unique: true,
required: true,
},
password: {
type: 'string',
required: true,
select: false,
},
role: {
type: 'enum',
values: ['user', 'admin'],
default: 'user',
},
isActive: {
type: 'bool',
default: true,
},
createdAt: {
type: 'timestamp',
default: 'now',
},
});
src/product/models.ts
const Product = Model('products', {
name: {
type: 'string',
required: true,
min: 2,
max: 200,
},
description: {
type: 'string',
max: 1000,
},
price: {
type: 'number',
required: true,
min: 0,
},
stock: {
type: 'number',
default: 0,
min: 0,
},
category: {
type: 'enum',
values: ['electronics', 'clothing', 'books', 'food'],
required: true,
},
isAvailable: {
type: 'bool',
default: true,
},
createdAt: {
type: 'timestamp',
default: 'now',
},
});
src/post/models.ts
const Post = Model('posts', {
title: {
type: 'string',
required: true,
min: 5,
max: 200,
},
slug: {
type: 'string',
unique: true,
required: true,
},
content: {
type: 'string',
required: true,
},
author: {
type: 'string',
required: true,
},
status: {
type: 'enum',
values: ['draft', 'published', 'archived'],
default: 'draft',
},
publishedAt: {
type: 'timestamp',
},
createdAt: {
type: 'timestamp',
default: 'now',
},
});

Problem: Create/update fails with validation error

Check:

  • Required fields are provided
  • String lengths within min/max
  • Number values within min/max
  • Enum values are valid
  • Email format is correct

Problem: “Duplicate entry” error

Cause: Trying to insert duplicate value for unique field

Solution:

src/users/models.ts
// Check if exists before creating
const existing = await User.findOne({ email });
if (existing) {
return reply.status(badRequest()).json({
error: 'Email already registered',
});
}

Problem: Field missing in query results

Check:

src/users/models.ts
// Is select: false?
const users = await User.find(undefined,{
password: false
});

Now that you understand models: