Getting started
Installation
# Install Objection.js and Knex
npm install objection knex
# Install database driver (choose one)
npm install pg # PostgreSQL
npm install mysql # MySQL
npm install sqlite3 # SQLite3
Knex Configuration
const Knex = require("knex");
const { Model } = require("objection");
// Initialize knex
const knex = Knex({
client: "pg",
connection: {
host: "127.0.0.1",
user: "postgres",
password: "password",
database: "mydb",
},
});
// Bind all Models to knex instance
Model.knex(knex);
Basic Model
const { Model } = require("objection");
class Person extends Model {
static get tableName() {
return "persons";
}
static get idColumn() {
return "id"; // Optional, defaults to 'id'
}
}
module.exports = Person;
Person model mapped to persons table.
Model Definition
Required Properties
| Property | Description |
|---|---|
tableName |
Database table name |
class Person extends Model {
static get tableName() {
return "persons";
}
}
Optional Properties
| Property | Description |
|---|---|
idColumn |
Primary key column(s) |
jsonSchema |
JSON Schema for validation |
relationMappings |
Model relationships |
class Person extends Model {
static get idColumn() {
return "id";
}
// Composite primary key
static get idColumn() {
return ["id", "tenant_id"];
}
}
JSON Schema Validation
class Person extends Model {
static get tableName() {
return "persons";
}
static get jsonSchema() {
return {
type: "object",
required: ["firstName", "lastName"],
properties: {
id: { type: "integer" },
firstName: { type: "string", minLength: 1, maxLength: 255 },
lastName: { type: "string", minLength: 1, maxLength: 255 },
age: { type: "number" },
email: { type: "string", format: "email" },
},
};
}
}
Validates data on insert, update, and patch operations.
Relationships
Relation Types
| Type | Description |
|---|---|
BelongsToOneRelation |
Many-to-one / one-to-one |
HasManyRelation |
One-to-many |
HasOneRelation |
One-to-one |
ManyToManyRelation |
Many-to-many via join table |
HasOneThroughRelation |
One-to-one via join table |
BelongsToOneRelation
const { Model } = require("objection");
class Pet extends Model {
static get tableName() {
return "pets";
}
static get relationMappings() {
return {
owner: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: "pets.owner_id",
to: "persons.id",
},
},
};
}
}
Pet belongs to one Person.
HasManyRelation
class Person extends Model {
static get tableName() {
return "persons";
}
static get relationMappings() {
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Pet,
join: {
from: "persons.id",
to: "pets.owner_id",
},
},
};
}
}
Person has many Pets.
ManyToManyRelation
class Person extends Model {
static get relationMappings() {
return {
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: "persons.id",
through: {
from: "persons_movies.person_id",
to: "persons_movies.movie_id",
},
to: "movies.id",
},
},
};
}
}
Person has many Movies through join table.
HasOneRelation
class Person extends Model {
static get relationMappings() {
return {
profile: {
relation: Model.HasOneRelation,
modelClass: Profile,
join: {
from: "persons.id",
to: "profiles.person_id",
},
},
};
}
}
Person has one Profile.
Circular Dependencies
// person.js
class Person extends Model {
static get relationMappings() {
// Require inside function to avoid circular dependency
const Pet = require("./pet");
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Pet,
join: {
from: "persons.id",
to: "pets.owner_id",
},
},
};
}
}
Use dynamic require inside relationMappings().
Query Operations
Find Queries
// Find all
const people = await Person.query();
// Find by ID
const person = await Person.query().findById(1);
// Where clause
const adults = await Person.query().where("age", ">=", 18);
// Multiple conditions
const results = await Person.query()
.where("age", ">", 18)
.where("firstName", "like", "J%");
// Order and limit
const people = await Person.query().orderBy("lastName").limit(10).offset(20);
Insert Operations
// Insert single record
const person = await Person.query().insert({
firstName: "John",
lastName: "Doe",
age: 30,
});
// Insert multiple records
const people = await Person.query().insert([
{ firstName: "John", lastName: "Doe" },
{ firstName: "Jane", lastName: "Smith" },
]);
// Insert and return
const person = await Person.query()
.insert({ firstName: "John" })
.returning("*");
Update Operations
// Update by ID
const updated = await Person.query().findById(1).patch({ age: 31 });
// Update with where
const count = await Person.query()
.where("age", "<", 18)
.patch({ status: "minor" });
// Update and fetch
const person = await Person.query().patchAndFetchById(1, { age: 31 });
// Full update (requires all fields)
await Person.query()
.findById(1)
.update({ firstName: "John", lastName: "Doe", age: 31 });
patch() for partial updates, update() for full replacement.
Delete Operations
// Delete by ID
await Person.query().deleteById(1);
// Delete with where
const count = await Person.query().where("age", "<", 18).delete();
// Delete and return
const deleted = await Person.query().delete().where("id", 1).returning("*");
Complex Queries
// Raw SQL
const people = await Person.query().whereRaw("age > ?", [18]);
// Subqueries
const people = await Person.query().where(
"age",
">",
Person.query().avg("age"),
);
// Joins
const results = await Person.query()
.join("pets", "persons.id", "pets.owner_id")
.where("pets.name", "Fluffy");
// Select specific columns
const people = await Person.query().select("id", "firstName", "lastName");
// Grouping
const counts = await Person.query()
.groupBy("age")
.select("age")
.count("* as count");
Relation Queries
$relatedQuery (Instance)
// Find related records
const person = await Person.query().findById(1);
const pets = await person.$relatedQuery("pets");
// Filter related
const dogs = await person.$relatedQuery("pets").where("species", "dog");
// Insert related
const newPet = await person
.$relatedQuery("pets")
.insert({ name: "Fluffy", species: "cat" });
// Update related
await person
.$relatedQuery("pets")
.patch({ vaccinated: true })
.where("species", "dog");
// Delete related
await person.$relatedQuery("pets").delete().where("id", 5);
Works on model instances.
relatedQuery (Static)
// Same operations on class
const pets = await Person.relatedQuery("pets").for(1);
// For multiple parents
const pets = await Person.relatedQuery("pets").for([1, 2, 3]);
// With where on parent
const pets = await Person.relatedQuery("pets").for(
Person.query().where("lastName", "Doe"),
);
Works on model classes.
Relate/Unrelate
// Relate existing records (many-to-many)
const person = await Person.query().findById(1);
// Add existing movie to person
await person.$relatedQuery("movies").relate(5);
// Relate multiple
await person.$relatedQuery("movies").relate([5, 6, 7]);
// Unrelate
await person.$relatedQuery("movies").unrelate().where("id", 5);
// Unrelate all
await person.$relatedQuery("movies").unrelate();
For existing records in many-to-many relationships.
Eager Loading
withGraphFetched
// Basic eager loading
const people = await Person.query().withGraphFetched("pets");
// Multiple relations
const people = await Person.query().withGraphFetched("[pets, children]");
// Nested relations
const people = await Person.query().withGraphFetched(
"[pets, children.[pets, movies]]",
);
// Recursive relations
const people = await Person.query().withGraphFetched("[children.^]"); // All descendants
// Limited recursion
const people = await Person.query().withGraphFetched("[children.^3]"); // 3 levels
Uses multiple queries (safer for large datasets).
withGraphJoined
// Eager loading with joins
const people = await Person.query().withGraphJoined("pets");
// Nested with joins
const people = await Person.query().withGraphJoined("[pets, children.pets]");
// Filter by relation
const people = await Person.query()
.withGraphJoined("pets")
.where("pets.species", "dog");
Uses joins (faster but can cause issues with large datasets).
Relation Expression Syntax
| Expression | Description |
|---|---|
pets |
Load pets relation |
[pets, movies] |
Load multiple relations |
children.pets |
Nested relation |
children.[pets, movies] |
Multiple nested |
children.^ |
Recursive (all levels) |
children.^3 |
Recursive (3 levels) |
children.^1 |
Recursive (1 level) |
Modifiers
// Define modifiers in model
class Person extends Model {
static get modifiers() {
return {
dogs(builder) {
builder.where("species", "dog");
},
orderByName(builder) {
builder.orderBy("name");
},
};
}
}
// Use in queries
const people = await Person.query().withGraphFetched("pets(dogs, orderByName)");
// Multiple modifiers
const people = await Person.query().withGraphFetched("pets(dogs, orderByName)");
modifyGraph
// Modify eager loaded relations
const people = await Person.query()
.withGraphFetched("[pets, children.pets]")
.modifyGraph("pets", (builder) => {
builder.where("species", "dog");
})
.modifyGraph("children.pets", (builder) => {
builder.orderBy("name");
});
Runtime modification of relation queries.
Graph Inserts
Basic Graph Insert
// Insert with nested relations
const person = await Person.query().insertGraph({
firstName: "John",
lastName: "Doe",
pets: [
{ name: "Fluffy", species: "cat" },
{ name: "Spot", species: "dog" },
],
children: [
{
firstName: "Jane",
pets: [{ name: "Max", species: "dog" }],
},
],
});
Inserts person, pets, and children in one operation.
References
// Reference syntax for relations
const person = await Person.query().insertGraph({
firstName: "John",
pets: [
{
"#id": "fluffy",
name: "Fluffy",
species: "cat",
},
],
children: [
{
firstName: "Jane",
// Relate to Fluffy using reference
pets: [{ "#ref": "fluffy" }],
},
],
});
#id creates reference, #ref uses it.
Insert Options
// Allow references
await Person.query().insertGraph(graph, {
allowRefs: true,
});
// Relate existing records
await Person.query().insertGraph(
{
firstName: "John",
movies: [
{ id: 10 }, // Relate existing movie
{ name: "New Movie" }, // Insert new
],
},
{
relate: true,
},
);
// Restrict allowed relations
await Person.query().insertGraph(graph, {
allowGraph: "[pets, children.pets]",
});
Graph Upserts
Basic Upsert
// Update if ID present, insert if not
await Person.query().upsertGraph({
id: 1, // Existing person - will update
firstName: "John",
pets: [
{ id: 5, name: "Fluffy" }, // Update existing
{ name: "Spot" }, // Insert new
],
});
Automatically inserts or updates based on presence of ID.
Upsert Options
// Relate existing records
await Person.query().upsertGraph(graph, {
relate: true,
});
// Unrelate missing records
await Person.query().upsertGraph(
{
id: 1,
movies: [{ id: 5 }],
},
{
unrelate: true, // Unrelate all movies except 5
},
);
// Don't delete missing records
await Person.query().upsertGraph(graph, {
noDelete: true,
});
// Restrict allowed relations
await Person.query().upsertGraph(graph, {
allowGraph: "[pets, children]",
});
Complex Upsert
await Person.query().upsertGraph(
{
id: 1,
firstName: "John",
pets: [
{ id: 5, name: "Fluffy Updated" }, // Update
{ name: "New Pet" }, // Insert
// Pet with id=6 will be deleted if existed
],
movies: [
{ id: 10 }, // Relate existing
],
},
{
relate: ["movies"], // Only relate for movies
unrelate: ["movies"], // Unrelate missing movies
noDelete: ["pets"], // Don't delete pets
},
);
Transactions
Transaction Callback
const { transaction } = require("objection");
// Automatic transaction
const person = await transaction(Person.knex(), async (trx) => {
const person = await Person.query(trx).insert({ firstName: "John" });
await person.$relatedQuery("pets", trx).insert({ name: "Fluffy" });
return person;
});
// Automatically commits on success, rolls back on error
Recommended pattern - handles commit/rollback automatically.
Binding Models
const trx = await Person.startTransaction();
try {
// Bind models to transaction
const BoundPerson = Person.bindTransaction(trx);
const BoundPet = Pet.bindTransaction(trx);
const person = await BoundPerson.query().insert({ firstName: "John" });
await BoundPet.query().insert({ name: "Fluffy", owner_id: person.id });
await trx.commit();
} catch (err) {
await trx.rollback();
throw err;
}
Bind models to avoid passing trx to every query.
Manual Transactions
const knex = Person.knex();
const trx = await knex.transaction();
try {
await Person.query(trx).insert({ firstName: "John" });
await Pet.query(trx).insert({ name: "Fluffy" });
await trx.commit();
} catch (err) {
await trx.rollback();
throw err;
}
Manual control over commit/rollback.
Validation
JSON Schema
class Person extends Model {
static get jsonSchema() {
return {
type: "object",
required: ["firstName", "lastName"],
properties: {
id: { type: "integer" },
firstName: {
type: "string",
minLength: 1,
maxLength: 255,
},
lastName: {
type: "string",
minLength: 1,
maxLength: 255,
},
age: {
type: "number",
minimum: 0,
maximum: 150,
},
email: {
type: "string",
format: "email",
},
},
};
}
}
Validates on insert, update, and patch.
Validation Errors
const { ValidationError } = require("objection");
try {
await Person.query().insert({
firstName: "J", // Too short
age: -5, // Invalid
});
} catch (err) {
if (err instanceof ValidationError) {
console.log(err.data); // Validation error details
// {
// firstName: [...],
// age: [...]
// }
}
}
Custom Validation
class Person extends Model {
$beforeInsert(context) {
await super.$beforeInsert(context);
this.createdAt = new Date().toISOString();
// Custom validation
if (this.age < 0) {
throw new Error('Age must be positive');
}
}
$beforeUpdate(opt, context) {
await super.$beforeUpdate(opt, context);
this.updatedAt = new Date().toISOString();
}
}
Lifecycle Hooks
Instance Hooks
class Person extends Model {
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
this.createdAt = new Date().toISOString();
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
console.log("Person inserted:", this.id);
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
this.updatedAt = new Date().toISOString();
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
console.log("Person updated:", this.id);
}
async $beforeDelete(queryContext) {
await super.$beforeDelete(queryContext);
console.log("About to delete:", this.id);
}
async $afterDelete(queryContext) {
await super.$afterDelete(queryContext);
console.log("Person deleted");
}
}
Called for each model instance.
Static Hooks
class Person extends Model {
static async beforeInsert({ asFindQuery, inputItems }) {
// Called once before all inserts
console.log("Inserting", inputItems.length, "items");
}
static async afterInsert({ asFindQuery, inputItems }) {
// Called once after all inserts
console.log("Inserted", inputItems.length, "items");
}
static async beforeUpdate({ asFindQuery, inputItems }) {
// Called once before update
}
static async afterUpdate({ asFindQuery, inputItems }) {
// Called once after update
}
}
Called once per query, not per instance.
Data Transformation
class Person extends Model {
// Parse JSON from database
$parseDatabaseJson(json) {
json = super.$parseDatabaseJson(json);
// Convert snake_case to camelCase
if (json.first_name) {
json.firstName = json.first_name;
delete json.first_name;
}
return json;
}
// Format JSON for database
$formatDatabaseJson(json) {
json = super.$formatDatabaseJson(json);
// Convert camelCase to snake_case
if (json.firstName) {
json.first_name = json.firstName;
delete json.firstName;
}
return json;
}
// Parse external JSON (from API)
$parseJson(json, opt) {
json = super.$parseJson(json, opt);
return json;
}
// Format for external JSON
$formatJson(json) {
json = super.$formatJson(json);
return json;
}
}
Examples
Complete CRUD Example
const { Model } = require("objection");
class Person extends Model {
static get tableName() {
return "persons";
}
}
// Create
const person = await Person.query().insert({
firstName: "John",
lastName: "Doe",
age: 30,
});
// Read
const allPeople = await Person.query();
const onePerson = await Person.query().findById(1);
const filtered = await Person.query().where("age", ">", 18).orderBy("lastName");
// Update
await Person.query().findById(1).patch({ age: 31 });
// Delete
await Person.query().deleteById(1);
Relationship Example
// Models
class Person extends Model {
static get tableName() {
return "persons";
}
static get relationMappings() {
const Pet = require("./Pet");
const Movie = require("./Movie");
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Pet,
join: {
from: "persons.id",
to: "pets.owner_id",
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: "persons.id",
through: {
from: "persons_movies.person_id",
to: "persons_movies.movie_id",
},
to: "movies.id",
},
},
};
}
}
// Queries
const people = await Person.query().withGraphFetched("[pets, movies]");
const person = await Person.query().findById(1);
const pets = await person.$relatedQuery("pets");
await person.$relatedQuery("pets").insert({
name: "Fluffy",
species: "cat",
});
Eager Loading with Modifiers
class Person extends Model {
static get modifiers() {
return {
// Filter modifier
adults(builder) {
builder.where("age", ">=", 18);
},
// Sort modifier
orderByAge(builder) {
builder.orderBy("age", "desc");
},
// Select modifier
minimal(builder) {
builder.select("id", "firstName", "lastName");
},
};
}
}
// Use modifiers
const people = await Person.query().modify("adults").modify("orderByAge");
// In eager loading
const people = await Person.query().withGraphFetched(
"children(adults, orderByAge)",
);
// Runtime modification
const people = await Person.query()
.withGraphFetched("[pets, children]")
.modifyGraph("children", (builder) => {
builder.where("age", ">", 10);
});
Transaction Example
const { transaction } = require("objection");
async function transferOwnership(petId, newOwnerId) {
return await transaction(Person.knex(), async (trx) => {
// Get pet
const pet = await Pet.query(trx).findById(petId);
if (!pet) throw new Error("Pet not found");
// Get new owner
const newOwner = await Person.query(trx).findById(newOwnerId);
if (!newOwner) throw new Error("Owner not found");
// Update pet
await pet.$query(trx).patch({ owner_id: newOwnerId });
// Log transfer
await TransferLog.query(trx).insert({
pet_id: petId,
new_owner_id: newOwnerId,
transferred_at: new Date(),
});
return pet;
});
}
Complex Query Example
// Find people with specific pets and movies
const people = await Person.query()
.withGraphFetched("[pets(dogs), movies(action)]")
.modifiers({
dogs(builder) {
builder.where("species", "dog");
},
action(builder) {
builder.where("genre", "action");
},
})
.where("age", ">", 18)
.orderBy("lastName")
.page(0, 20);
// Aggregation query
const stats = await Person.query()
.select("age")
.count("* as count")
.avg("age as avgAge")
.groupBy("age")
.having("count", ">", 1);
// Subquery
const activePeople = await Person.query().where(
"id",
"in",
Person.query()
.select("owner_id")
.from("pets")
.where("last_checkup", ">", "2025-01-01"),
);
Also see
- Objection.js Documentation - Official documentation
- Knex.js Documentation - Query builder used by Objection
- JSON Schema - Validation schema reference
- Objection.js GitHub - Source code and examples