In-Memory DB
The InMemoryDBProvider is a built-in provider that ships with @expressots/core. It offers a high-performance, Prisma-like query API backed entirely by in-process memory.
The in-memory database is designed for development, testing, and rapid prototyping — not as a durable production datastore. Data lives in process memory (with optional file snapshots), so it does not provide the crash safety, concurrency, or multi-process guarantees of a real database engine.
The provider implements the universal IDataAdapter contract, so when you are ready for production you can swap it for a real database adapter (Prisma, TypeORM, etc.) without rewriting your repositories.
Register the provider
InMemoryDBProvider is not auto-registered. Bind it explicitly as a singleton in configureServices():
import { AppExpress } from "@expressots/adapter-express";
import { InMemoryDBProvider, Scope } from "@expressots/core";
export class App extends AppExpress {
async configureServices(): Promise<void> {
this.Provider.register(InMemoryDBProvider, Scope.Singleton);
}
}
Registering as a singleton is required so data persists across requests and so the provider's lifecycle hooks (bootstrap, shutdown) run on a single shared instance.
Optional configuration
Configure global behavior by retrieving the provider and calling configure():
async configureServices(): Promise<void> {
this.Provider.register(InMemoryDBProvider, Scope.Singleton);
const db = this.Provider.get(InMemoryDBProvider);
db.configure({
timestamps: true, // auto-manage createdAt / updatedAt (default: true)
softDelete: false, // soft-delete via deletedAt (default: false)
logging: false, // log operations to console (default: false)
persist: {
storage: "file",
path: "./data/db.json",
interval: 30000, // auto-save every 30s
},
});
}
Define an entity
Use the schema decorators to describe your entity. See Entities for the full decorator reference.
import { Entity, PrimaryKey, AutoGenerate, Index, Unique } from "@expressots/core";
@Entity({ name: "users", timestamps: true })
export class User {
@PrimaryKey()
@AutoGenerate("uuid")
id!: string;
name!: string;
@Index()
@Unique()
email!: string;
age!: number;
}
Access a table
Get a table adapter from the provider with table<T>(name, EntityClass). Passing the entity class enables schema metadata such as indexes, auto-generated fields, and relation include resolution.
import { IEntity } from "@expressots/core";
interface UserModel extends IEntity {
name: string;
email: string;
age: number;
}
const users = db.table<UserModel>("users", User);
Prisma-like queries
The table adapter exposes a Prisma-compatible API:
// Create
const user = await users.create({
});
// Create many
await users.createMany({
data: [
],
});
// Find with complex filters
const adults = await users.findMany({
where: {
OR: [{ name: { contains: "Jo" } }, { age: { gte: 18 } }],
},
orderBy: { age: "desc" },
take: 10,
});
// Find one
const byId = await users.findUnique({ where: { id: user.id } });
// Update
await users.update({ where: { id: user.id }, data: { age: 31 } });
await users.updateMany({ where: { age: { lt: 18 } }, data: { age: 18 } });
// Upsert
await users.upsert({
where: { id: "missing-id" },
update: { name: "Updated" },
});
// Delete
await users.delete({ where: { id: user.id } });
await users.deleteMany({ where: { age: { gte: 65 } } });
// Count & aggregate
const total = await users.count({ where: { age: { gte: 18 } } });
const stats = await users.aggregate({ _avg: { age: true }, _max: { age: true } });
Supported where operators include equals, not, in, notIn, lt, lte, gt, gte, contains, startsWith, endsWith, and case-insensitive matching via mode: "insensitive", combined with the logical operators AND, OR, and NOT.
Repository pattern
For a cleaner separation, extend BaseRepository<T> and inject the provider. The repository delegates to the table adapter and is where you add custom query methods.
import { BaseRepository, InMemoryDBProvider, provide, inject } from "@expressots/core";
import { User } from "../entities/user.entity";
interface UserModel {
id?: string;
name: string;
email: string;
age: number;
}
@provide(UserRepository)
export class UserRepository extends BaseRepository<UserModel> {
constructor(@inject(InMemoryDBProvider) db: InMemoryDBProvider) {
super(db, "users", User);
}
async findByEmail(email: string): Promise<UserModel | null> {
return this.findFirst({ where: { email } });
}
}
BaseRepository exposes findUnique, findFirst, findMany, create, createMany, update, updateMany, upsert, delete, deleteMany, count, aggregate, and transaction.
Usage in a use case
import { provide, inject } from "@expressots/core";
import { UserRepository } from "../repositories/user.repository";
@provide(UserCreateUseCase)
export class UserCreateUseCase {
constructor(@inject(UserRepository) private userRepo: UserRepository) {}
async execute(payload: IUserCreateRequestDTO): Promise<IUserCreateResponseDTO> {
const user = await this.userRepo.create({
data: { name: payload.name, email: payload.email, age: payload.age },
});
return { id: user.id, name: user.name, email: user.email };
}
}
Relations
Declare relations with @HasOne, @HasMany, @BelongsTo, and @ManyToMany, then resolve them with include:
const userWithPosts = await users.findUnique({
where: { id: user.id },
include: { posts: true },
});
Relation include requires the table adapters to be created with their entity classes (db.table("posts", Post)) so the schema metadata is available. See Entities → Relation decorators.
Transactions
Run multiple operations atomically. If the function throws, all changes roll back.
await db.transaction(async (tx) => {
const user = await tx.table<UserModel>("users", User).create({
});
await tx.table<PostModel>("posts", Post).create({
data: { title: "Hello", authorId: user.id },
});
});
Persistence
By default data lives only in memory and is cleared on restart. Enable file-based snapshots through configure({ persist: ... }) (see above). Snapshots are written as a JSON dump of all tables and reloaded on bootstrap(). This is intended for dev convenience, not high-throughput durability.
Testing
The in-memory DB shines in tests — it is fast, isolated, and requires no external services.
Unit testing a repository
import { InMemoryDBProvider } from "@expressots/core";
import { UserRepository } from "../repositories/user.repository";
describe("UserRepository", () => {
let repository: UserRepository;
beforeEach(() => {
const db = new InMemoryDBProvider();
repository = new UserRepository(db);
});
it("creates and finds a user", async () => {
const user = await repository.create({
});
expect(user.id).toBeDefined();
name: "John",
});
});
});
E2E testing with controllers
describe("User CRUD E2E", () => {
let app: IWebServerPublic;
let port: number;
beforeAll(async () => {
app = await bootstrap(App, { port: 0 });
port = await app.getPort();
});
afterAll(async () => {
await app.close();
});
it("creates and retrieves a user", async () => {
const createRes = await fetch(`http://localhost:${port}/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
const created = await createRes.json();
expect(created.id).toBeDefined();
const getRes = await fetch(`http://localhost:${port}/users/${created.id}`);
const user = await getRes.json();
expect(user.name).toBe("John");
});
});
For a dedicated lightweight test store, see also createTestDatabase() in Testing.
Health checks and metrics
InMemoryDBProvider implements IHealthCheck and IMetrics, so it contributes to the aggregated health and metrics dashboards automatically:
const health = await this.Provider.checkHealth();
// includes "In-Memory Database Provider": { status: "healthy", details: { tables, records } }
const metrics = this.Provider.collectMetrics();
// includes db.tables, db.records.total, db.queries.total, per-table records/memory
Migrating to a real database
Because repositories depend on the table adapter's IDataAdapter contract rather than on the in-memory implementation directly, moving to a production database is a swap rather than a rewrite: register a real adapter implementing the same contract and keep your repository and use-case code unchanged.
Support the Project
ExpressoTS is MIT-licensed open source. See the support guide to contribute.