Getting Started
This guide walks you through installing Stingerloom ORM, defining your first entity, and performing create/read/update/delete operations step by step. It should take about 5 minutes.
What Is an ORM?
When you write a web application, your data lives in two very different worlds. In your TypeScript code, data lives in objects -- classes with properties and methods. In your database, data lives in tables -- rows and columns of raw values.
An ORM (Object-Relational Mapper) is the translation layer between these two worlds. Instead of writing SQL strings by hand, you define a TypeScript class, and the ORM figures out how to create the table, insert rows, query data, and give you back typed objects.
Think of it like a universal translator. You speak TypeScript, your database speaks SQL, and the ORM translates every conversation between them.
Without an ORM:
// You write raw SQL strings, get back untyped rows
const result = await pool.query('SELECT * FROM "user" WHERE "id" = $1', [1]);
const user = result.rows[0]; // { id: 1, name: "Alice" } -- no type safetyWith an ORM:
// You work with typed objects, the ORM writes the SQL
const user = await em.findOne(User, { where: { id: 1 } });
// user is User | null -- full type safety, auto-completed propertiesThe ORM handles the translation in both directions: your class definition becomes a CREATE TABLE statement, your save() call becomes an INSERT, and your find() call becomes a SELECT. You will see the exact SQL for each of these operations in this guide.
Prerequisites
- Node.js 20 or higher (latest LTS recommended)
- TypeScript project
- MySQL, PostgreSQL, or SQLite database
Step 1: Installation
Install the core package and reflect-metadata, then add the driver for your database.
npm install @stingerloom/orm reflect-metadatapnpm add @stingerloom/orm reflect-metadatayarn add @stingerloom/orm reflect-metadataThen install the driver for your database:
npm install pg # or pnpm add pg / yarn add pgnpm install mysql2 # or pnpm add mysql2 / yarn add mysql2npm install better-sqlite3 # or pnpm add better-sqlite3 / yarn add better-sqlite3For example, a PostgreSQL project needs:
npm install @stingerloom/orm reflect-metadata pgpnpm add @stingerloom/orm reflect-metadata pgyarn add @stingerloom/orm reflect-metadata pgWhy reflect-metadata?
When you write @Column() on a class property, TypeScript records some information about that property (its type, its name) at compile time. But by default, that information is erased when the code is compiled to JavaScript.
reflect-metadata is a polyfill that makes this metadata available at runtime. The ORM needs it to answer questions like "what type is the email property?" so it can map string to VARCHAR and number to INTEGER automatically. Without it, decorators like @Entity() and @Column() would have no way to know what your class looks like.
You only need to import it once, at the very top of your application entry point. After that, every decorator in the ORM can read the type information it needs.
CJS and ESM
Stingerloom ORM ships as a dual CJS/ESM package. Both require() and import work out of the box -- no extra configuration needed.
// ESM (recommended)
import { EntityManager } from "@stingerloom/orm";
// CommonJS
const { EntityManager } = require("@stingerloom/orm");Subpath exports are also dual:
import { StinglerloomOrmModule } from "@stingerloom/orm/nestjs";
import { PrismaImporter } from "@stingerloom/orm/prisma-import";
import { EntityManager } from "@stingerloom/orm/core"; // core only
import { PostgresDriver } from "@stingerloom/orm/postgres"; // single dialectOptional: class-transformer
By default, the ORM uses a lightweight PlainObjectDeserializer (zero dependencies). If you install class-transformer, it will be auto-detected and used as the deserializer -- no configuration needed.
npm install class-transformer # optional, for advanced deserializationStep 2: TypeScript Configuration
Enable decorator-related options in your tsconfig.json.
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false
}
}Here is what each option does:
experimentalDecorators-- Enables the@Entity(),@Column()syntax. Without this, TypeScript treats@as a syntax error.emitDecoratorMetadata-- Tells the compiler to emit type information thatreflect-metadatacan read at runtime. This is how the ORM knows thatname: stringshould become aVARCHARcolumn.strictPropertyInitialization-- Normally, TypeScript complains if a class property is not assigned in the constructor. Entity properties are populated by the ORM, not the constructor, so we disable this check to avoid needing!:on every property.
Step 3: Define an Entity
An Entity is a TypeScript class that represents a database table. Each instance of the class represents one row. Let's create a simple user entity.
// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from "@stingerloom/orm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column()
name!: string;
@Column()
email!: string;
}What each decorator does:
@Entity()-- Tells the ORM: "this class maps to a database table." The table name defaults to the lowercased class name (user).@PrimaryGeneratedColumn()-- This column is the primary key, and the database auto-generates its value (auto-increment for MySQL,SERIALfor PostgreSQL).@Column()-- A regular column. The ORM infers the SQL type from the TypeScript type:stringbecomesVARCHAR(255),numberbecomesINTEGER,booleanbecomesBOOLEAN.
When synchronize: true is set (next step), the ORM generates this DDL and executes it:
-- PostgreSQL
CREATE TABLE "user" (
"id" SERIAL PRIMARY KEY,
"name" VARCHAR(255),
"email" VARCHAR(255)
);
-- MySQL
CREATE TABLE `user` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255),
`email` VARCHAR(255)
);Notice the identifier wrapping difference: PostgreSQL uses "double quotes", MySQL uses `backticks`. The ORM handles this automatically based on your type setting.
Hint To learn more about entities, refer to the Entities documentation.
Step 4: Connect to the Database
Now connect to the DB using EntityManager and register your entities. Make sure to import reflect-metadata at the very top of your application entry point -- before any other imports that use decorators.
// main.ts
import "reflect-metadata"; // Must be the very first import
import { EntityManager } from "@stingerloom/orm";
import { User } from "./user.entity";
async function main() {
const em = new EntityManager();
await em.register({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "password",
database: "mydb",
entities: [User],
synchronize: true,
});
console.log("DB connection successful!");
}
main().catch(console.error);When synchronize: true is set, the ORM compares your entity definitions against the live database and creates or alters tables to match. If the user table does not exist yet, it executes the CREATE TABLE DDL shown above. If the table exists but is missing a column you added, it runs ALTER TABLE to add it.
Warning Use
synchronize: trueonly in development. In production, it can drop columns or tables that no longer match your entities. Use migrations instead -- they give you full control over what changes reach your production database.
Step 5: Try CRUD
Now that the DB is connected, let's create, read, update, and delete data. Continue writing inside the main() function.
Create
// main.ts (inside main function)
const user = await em.save(User, {
name: "John Doe",
email: "john@example.com",
});
console.log("Saved user:", user);
// { id: 1, name: "John Doe", email: "john@example.com" }em.save() sees that no id is provided, so it performs an INSERT. Here is the actual SQL that gets executed:
-- PostgreSQL
INSERT INTO "user" ("name", "email") VALUES ($1, $2) RETURNING *
-- Parameters: ["John Doe", "john@example.com"]
-- MySQL
INSERT INTO `user` (`name`, `email`) VALUES (?, ?)
-- Parameters: ["John Doe", "john@example.com"]
-- Then: SELECT * FROM `user` WHERE `id` = LAST_INSERT_ID()Notice that user-provided values are never placed directly in the SQL string. They appear as $1, $2 (PostgreSQL) or ? (MySQL) -- this is parameter binding, and it prevents SQL injection. PostgreSQL also supports RETURNING *, which returns the inserted row in a single round-trip. MySQL needs a second query to fetch the auto-generated id.
Read
// main.ts
// Fetch all
const users = await em.find(User);
console.log("All users:", users);
// Find one by condition
const found = await em.findOne(User, { where: { id: 1 } });
console.log("Single user:", found); // User | nullThe generated SQL:
-- find() -- fetch all users
SELECT "id", "name", "email" FROM "user"
-- findOne() -- find by condition
SELECT "id", "name", "email" FROM "user" WHERE "id" = $1 LIMIT 1
-- Parameters: [1]find() returns an array (empty if no rows match). findOne() returns a single typed object or null. The ORM automatically adds LIMIT 1 for findOne() since you only need one row.
Update
// main.ts
const updated = await em.save(User, {
id: 1, // PK present, so UPDATE
name: "John Doe (edited)",
email: "john@example.com",
});
console.log("Updated user:", updated);When save() receives an object with a primary key (id: 1), it performs an UPDATE instead of an INSERT:
-- PostgreSQL
UPDATE "user" SET "name" = $1, "email" = $2 WHERE "id" = $3
-- Parameters: ["John Doe (edited)", "john@example.com", 1]
-- MySQL
UPDATE `user` SET `name` = ?, `email` = ? WHERE `id` = ?
-- Parameters: ["John Doe (edited)", "john@example.com", 1]Delete
// main.ts
const result = await em.delete(User, { id: 1 });
console.log("Rows deleted:", result.affected); // 1The generated SQL:
DELETE FROM "user" WHERE "id" = $1
-- Parameters: [1]Congratulations -- you have completed your first CRUD. Every operation was a single method call, and the ORM handled the SQL generation, parameter binding, and result deserialization behind the scenes.
Using Other Databases
The example above uses PostgreSQL, but you can use other databases by simply changing the type option. The rest of your code stays identical.
| DB | type | port | Notes |
|---|---|---|---|
| PostgreSQL | "postgres" | 5432 | Schema can be specified with the schema option |
| MySQL / MariaDB | "mysql" | 3306 | charset: "utf8mb4" recommended |
| SQLite | "sqlite" | 0 | Specify file path for database (e.g., "./mydb.sqlite") |
// MySQL example
await em.register({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "password",
database: "mydb",
entities: [User],
synchronize: true,
charset: "utf8mb4",
});// SQLite example -- host, port, username, password are empty
await em.register({
type: "sqlite",
host: "",
port: 0,
username: "",
password: "",
database: "./mydb.sqlite",
entities: [User],
synchronize: true,
});Using with NestJS
Stingerloom ORM provides a first-party NestJS integration module via the @stingerloom/orm/nestjs subpath export.
Why a Separate Module?
NestJS uses Dependency Injection (DI) -- instead of creating objects yourself with new, you declare what you need, and NestJS provides it. The ORM module bridges these two worlds: it creates the EntityManager and repositories, then registers them in NestJS's DI container so your services can declare them as constructor parameters.
The flow works like this:
forRoot()creates anEntityManager, connects to the database, and registers it as a global NestJS provider.forFeature([User])creates aBaseRepository<User>and registers it with a unique token derived from theUserclass.@InjectRepository(User)in your service tells NestJS: "give me the repository registered for User."- NestJS resolves the dependency and passes the repository to your service's constructor.
Installation
npm install @stingerloom/orm reflect-metadatapnpm add @stingerloom/orm reflect-metadatayarn add @stingerloom/orm reflect-metadata@nestjs/common and @nestjs/core are listed as optional peer dependencies -- they are already present in any NestJS project.
Root Module Registration
Use StinglerloomOrmModule.forRoot() to initialize the database connection, and StinglerloomOrmModule.forFeature() to register entity repositories.
// app.module.ts
import { Module } from "@nestjs/common";
import { StinglerloomOrmModule } from "@stingerloom/orm/nestjs";
import { User } from "./user.entity";
import { UsersModule } from "./users/users.module";
@Module({
imports: [
StinglerloomOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "password",
database: "mydb",
entities: [User],
synchronize: true,
}),
UsersModule,
],
})
export class AppModule {}Feature Module Registration
// users/users.module.ts
import { Module } from "@nestjs/common";
import { StinglerloomOrmModule } from "@stingerloom/orm/nestjs";
import { User } from "./user.entity";
import { UsersService } from "./users.service";
@Module({
imports: [StinglerloomOrmModule.forFeature([User])],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}Usage in Services
Import InjectRepository from @stingerloom/orm/nestjs to inject typed repositories.
// users/users.service.ts
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@stingerloom/orm/nestjs";
import { BaseRepository } from "@stingerloom/orm";
import { User } from "./user.entity";
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private readonly userRepo: BaseRepository<User>,
) {}
async findAll(): Promise<User[]> {
return (await this.userRepo.find()) as User[];
}
async findById(id: number): Promise<User | null> {
return this.userRepo.findOne({ where: { id } as any });
}
async create(name: string, email: string): Promise<User> {
return (await this.userRepo.save({ name, email })) as User;
}
}Multi-DB (Named Connections)
Pass a connectionName to forRoot() and forFeature() to use multiple databases simultaneously.
// app.module.ts
import { Module } from "@nestjs/common";
import { StinglerloomOrmModule } from "@stingerloom/orm/nestjs";
import { User } from "./user.entity";
import { Event } from "./event.entity";
@Module({
imports: [
// Default connection (MySQL)
StinglerloomOrmModule.forRoot({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "password",
database: "main",
entities: [User],
}),
// Named connection (PostgreSQL)
StinglerloomOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "password",
database: "analytics",
entities: [Event],
}, "analytics"),
UsersModule,
AnalyticsModule,
],
})
export class AppModule {}Specify the connectionName in feature modules:
// analytics/analytics.module.ts
@Module({
imports: [StinglerloomOrmModule.forFeature([Event], "analytics")],
providers: [AnalyticsService],
})
export class AnalyticsModule {}Pass the connectionName to @InjectRepository and @InjectEntityManager in services:
// analytics/analytics.service.ts
import { Injectable } from "@nestjs/common";
import {
InjectRepository,
InjectEntityManager,
} from "@stingerloom/orm/nestjs";
import { BaseRepository, EntityManager } from "@stingerloom/orm";
import { Event } from "./event.entity";
@Injectable()
export class AnalyticsService {
constructor(
@InjectRepository(Event, "analytics")
private readonly eventRepo: BaseRepository<Event>,
@InjectEntityManager("analytics")
private readonly em: EntityManager,
) {}
}When connectionName is omitted, it defaults to
"default", so existing single-DB code works without any changes.
Hint Complete NestJS examples are included in the
examples/nestjs-cats/,examples/nestjs-blog/, andexamples/nestjs-multitenant/directories.
Next Steps
You have learned the basic setup and CRUD. Now try defining richer entities.
- Entities -- Column types, indexes, Soft Delete, lifecycle hooks
- Relations -- Define relationships between tables with
@ManyToOne,@OneToMany - EntityManager -- Find options, aggregation, pagination
- NestJS Integration -- forRoot/forFeature, @InjectRepository, multi-DB
- Configuration -- Pooling, timeouts, Read Replica, and other operational settings