Repositories

Type-safe data access with the repository pattern

Repositories provide a clean abstraction for data access in JustScale. They implement the repository pattern, separating your domain logic from storage details. This allows you to write business logic that works with any storage backend — PostgreSQL for production, in-memory for testing.

The Modern Approach

JustScale uses a layered approach: define your domain model once with defineModel, then wrap it with storage-specific configuration using createPgModel and createPgRepository:

user.model.tsTypeScript
import { defineModel, field } from '@justscale/core/models'
import { createPgModel, createPgRepository } from '@justscale/postgres'

// 1. Define domain model (pure, no storage details)
export class User extends defineModel({
  email: field.string().max(255).unique(),
  name: field.string().max(100),
  status: field.enum('UserStatus', ['active', 'inactive', 'banned'] as const)
    .default('active'),
}) {}

// 2. Create PostgreSQL storage model
export const PgUser = createPgModel(User, {
  table: 'users',  // Optional: defaults to snake_case pluralized
})

// 3. Create repository service for DI
export const UserRepository = createPgRepository(PgUser)

Why This Pattern?

Clean Separation

Your domain model (User) is pure TypeScript with no database dependencies. Storage configuration (PgUser) lives separately. Services only depend on the abstract repository interface.

Type-Safe Queries

Field expressions like User.fields.email.eq(value) are fully typed. TypeScript catches typos in field names and invalid comparison values at compile time:

type-safe-queries.tsTypeScript
// TypeScript catches these errors:
User.fields.email.eq(123)           // Error: expected string
User.fields.status.eq('unknown')    // Error: not a valid status
User.fields.emial.eq('test')        // Error: 'emial' doesn't exist

// Correct usage - fully typed:
User.fields.email.eq('test@example.com')  // OK
User.fields.status.eq('active')           // OK

Easy Testing

Swap PostgreSQL for in-memory storage in tests without changing your services:

user.service.test.tsTypeScript
import { createInMemoryRepository } from '@justscale/core/models'
import { User, UserRepository } from './user.model'
import { UserService } from './user.service'
import { TestContainer } from '@justscale/testing'

test('find by email', async () => {
  const container = new TestContainer()

  // Use in-memory repository for fast, isolated tests
  container.bind(UserRepository, createInMemoryRepository(User))

  const service = container.resolve(UserService)
  await service.create({ email: 'test@example.com', name: 'Test' })

  const user = await service.findByEmail('test@example.com')
  expect(user?.name).toBe('Test')
})

Repository Methods

All repositories implement the same interface. Methods use Ref<T> for entity lookups — not string IDs:

repository-api.tsTypeScript
// Query methods
await users.find({ where, orderBy, limit, offset })  // Find multiple
await users.get(ref)                                  // Get by reference
await users.getMany(refs)                             // Batch get
await users.findOne(where)                            // Find first match
await users.count(where?)                             // Count matching
await users.exists(where)                             // Check existence

// Mutation methods
await users.insert(data)                              // Insert one
await users.insertMany([data1, data2])                // Bulk insert
await users.update(ref, data)                         // Update by reference
await users.save(entity)                              // Smart insert/update
await users.delete(ref)                               // Delete by reference
await users.deleteWhere(where)                        // Delete matching

// References — not string IDs
const userRef = User.ref`${userId}`                   // At boundaries
await users.get(userRef)                              // Type-safe lookup
await users.update(persistentUser, { name: 'New' })   // Entity IS a ref
💡

Tip

A Persistent<T> entity is itself a valid Ref<T>. You can pass it directly to update(), delete(), or any method that accepts a reference.

Wiring It Together

Register your repository and services with the cluster builder. The PostgreSQL client connection is shared across all repositories:

main.tsTypeScript
import JustScale from '@justscale/core'
import { createPostgresClient } from '@justscale/postgres'
import { UserRepository } from './user.model'
import { UserService } from './user.service'
import { UserController } from './user.controller'

const pgClient = createPostgresClient({
  connectionString: process.env.DATABASE_URL,
})

const app = JustScale()
  .add(pgClient)
  .add(UserRepository)
  .add(UserService)
  .add(UserController)
  .build()

await app.serve({ http: 3000 })

Available Adapters

PostgreSQL

Production-ready adapter with full query support, transactions, migrations, and advanced features like advisory locks and LISTEN/NOTIFY.

Learn about PostgreSQL adapter

In-Memory

Fast, zero-dependency storage for testing and prototyping. Implements the full repository interface with JavaScript Maps.

Learn about In-Memory Repository