Define Services

Create a Service

import { Context, Effect, Layer, Schema } from "effect"

class DatabaseError extends Schema.TaggedError<DatabaseError>()("DatabaseError", {
  cause: Schema.Defect()
}) {}

export class Database extends Context.Service<Database, {
  query(sql: string): Effect.Effect<unknown[], DatabaseError>
  insert(table: string, row: Record<string, unknown>): Effect.Effect<void, DatabaseError>
}>()("myapp/db/Database") {}

Context.Service defines a service interface. The first type parameter is the class itself (for type identity). The second is the interface object listing methods. The string argument is a unique identifier, conventionally package/path/ServiceName.

Tip: The identifier string should include your package name and the subdirectory path to the service file. This avoids collisions when multiple packages define services with the same class name.

Implement with Layer.effect

export class Database extends Context.Service<Database, {
  query(sql: string): Effect.Effect<unknown[], DatabaseError>
  insert(table: string, row: Record<string, unknown>): Effect.Effect<void, DatabaseError>
}>()("myapp/db/Database") {
  static readonly Live = Layer.effect(
    Database,
    Effect.gen(function*() {
      const query = Effect.fn("Database.query")(function*(sql: string) {
        yield* Effect.log("Executing SQL:", sql)
        return [{ id: 1, name: "Alice" }]
      })

      const insert = Effect.fn("Database.insert")(function*(table: string, row: Record<string, unknown>) {
        yield* Effect.log(`Inserting into ${table}:`, JSON.stringify(row))
      })

      return Database.of({ query, insert })
    })
  )
}

Layer.effect builds an implementation. Inside the generator, you define methods (preferably with Effect.fn for named stack traces), then construct the service with Database.of({ ... }).

Access a Service in a Program

const getUser = Effect.fn("getUser")(
  function*(id: number) {
    const db = yield* Database
    const results = yield* db.query(`SELECT * FROM users WHERE id = ${id}`)
    return results[0]
  }
)

yield* Database extracts the service from the context. The compiler knows the type of db from the service interface, so db.query returns Effect<unknown[], DatabaseError>.

Provide a Layer to a Program

const program = Effect.gen(function*() {
  const user = yield* getUser(1)
  yield* Effect.logInfo(`User: ${JSON.stringify(user)}`)
  return user
})

Effect.runPromise(
  program.pipe(Effect.provide(Database.Live))
)

Effect.provide supplies the layer that fulfills the service requirement. Without it, the program won’t compile because Database appears in the Requirements type parameter.

Mock a Service for Tests

import { Layer } from "effect"

const mockRows = [
  { id: 1, name: "Test User" },
  { id: 2, name: "Another User" }
]

export const DatabaseTest = Layer.succeed(
  Database,
  Database.of({
    query: () => Effect.succeed(mockRows),
    insert: () => Effect.succeed(void 0)
  })
)

// Use in tests
const testProgram = Effect.gen(function*() {
  const user = yield* getUser(1)
  return user
}).pipe(Effect.provide(DatabaseTest))

Layer.succeed creates a layer from a plain service instance. For mocks that don’t need async setup, this is simpler than Layer.effect.

Complete Example: Full Service Wiring

import { Context, Effect, Layer, Schema, Console } from "effect"

// --- Errors ---

class DatabaseError extends Schema.TaggedError<DatabaseError>()("DatabaseError", {
  message: Schema.String
}) {}

// --- Service Definition ---

export class Database extends Context.Service<Database, {
  query(sql: string): Effect.Effect<unknown[], DatabaseError>
  insert(table: string, row: Record<string, unknown>): Effect.Effect<void, DatabaseError>
}>()("myapp/db/Database") {
  static readonly Live = Layer.effect(
    Database,
    Effect.gen(function*() {
      const query = Effect.fn("Database.query")(function*(sql: string) {
        yield* Effect.log("Executing SQL:", sql)
        return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
      })

      const insert = Effect.fn("Database.insert")(
        function*(table: string, row: Record<string, unknown>) {
          yield* Effect.log(`Inserting into ${table}:`, JSON.stringify(row))
        }
      )

      return Database.of({ query, insert })
    })
  )

  static readonly Test = Layer.succeed(
    Database,
    Database.of({
      query: () => Effect.succeed([{ id: 1, name: "Test User" }]),
      insert: () => Effect.succeed(void 0)
    })
  )
}

// --- Service Consumers ---

export class UserService extends Context.Service<UserService, {
  getById(id: number): Effect.Effect<unknown, DatabaseError>
}>()("myapp/user/UserService") {
  static readonly Live = Layer.effect(
    UserService,
    Effect.gen(function*() {
      const db = yield* Database

      const getById = Effect.fn("UserService.getById")(
        function*(id: number) {
          const results = yield* db.query(`SELECT * FROM users WHERE id = ${id}`)
          if (results.length === 0) {
            return yield* new DatabaseError({ message: `User ${id} not found` })
          }
          return results[0]
        }
      )

      return UserService.of({ getById })
    })
  )
}

// --- Program ---

const program = Effect.gen(function*() {
  const userService = yield* UserService
  const user = yield* userService.getById(1)
  yield* Console.log(`Found user: ${JSON.stringify(user)}`)
  return user
})

// --- Run with Live Layers ---

const MainLive = Layer.provide(UserService.Live, Database.Live)

Effect.runPromise(
  program.pipe(Effect.provide(MainLive))
)
// Output:
// Found user: {"id":1,"name":"Alice"}

Layer.provide composes layers. UserService.Live depends on Database, so you provide Database.Live to it. The resulting MainLive layer satisfies both service requirements.

Gotcha: Layer composition order matters. Layer.provide(UserService.Live, Database.Live) means “provide Database.Live to UserService.Live.” The outer layer consumes the inner layer’s output.

Next Steps