<!-- Markdown mirror of https://justscale.sh/docs/repositories/overview -->

# 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

```typescript
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
// 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

```typescript
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` for entity lookups — not string IDs:

repository-api.tsTypeScript

```typescript
// 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` entity is itself a valid `Ref`. 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:

app.tsTypeScript

```typescript
import JustScale, { defineApp } from '@justscale/core'
import { PostgresFeature } from '@justscale/postgres'
import type { AppEnv } from './env-contract'
import { UserRepository } from './user.model'
import { UserService } from './user.service'
import { UserController } from './user.controller'

// PostgresFeature reads its connection from the env contract; the app
// doesn't construct a client by hand. `just dev` runs this; the
// cluster socket and HTTP listener are wired by defineApp.
export default defineApp(import.meta, (env: AppEnv) =>
  JustScale()
    .add(env)
    .add(PostgresFeature)
    .add(UserRepository)
    .add(UserService)
    .add(UserController)
)
```

## 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](https://justscale.sh/docs/postgres/overview)

### In-Memory

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

[Learn about In-Memory Repository](https://justscale.sh/docs/repositories/in-memory)

## Next Steps

- References & ID-Free Domain
- Models Overview
- Queries Overview
- PostgreSQL Overview
