Skip to main content

Overview

The Drizzle integration provides Effect-first database operations with automatic error handling and built-in transaction support. Create a Drizzle client wrapper once and use it throughout your application with full type safety.

Installation

bun add ff-effect effect drizzle-orm
You’ll also need a Drizzle database driver (e.g., @libsql/client, postgres, @electric-sql/pglite).

Exports

import { createDrizzle, DrizzleError } from 'ff-effect/for/drizzle';

createDrizzle

Create an Effect-based Drizzle client wrapper.

Signature

function createDrizzle<
  TClient extends AnyDrizzleClient,
  E,
  R,
  T extends string = '@ff-effect/Drizzle'
>(
  createClient: Effect.Effect<TClient, E, R>,
  opts?: { tagId?: T }
): {
  db: <T>(fn: (client: Client) => Promise<T>) => Effect.Effect<T, DrizzleError, Drizzle>;
  tx: <T>(fn: (client: Tx) => Promise<T>) => Effect.Effect<T, DrizzleError, DrizzleTx>;
  Drizzle: Context.Tag<Drizzle, Client>;
  DrizzleTx: Context.Tag<DrizzleTx, Tx>;
  withTransaction: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | DrizzleError, Exclude<R, DrizzleTx>>;
  layer: Layer<Drizzle, E, R>;
}

Parameters

createClient
Effect.Effect<TClient, E, R>
required
An Effect that yields a Drizzle client instance. This can include any setup logic, connection pooling, etc.
opts.tagId
string
Optional custom tag identifier. Useful when working with multiple databases. Defaults to '@ff-effect/Drizzle'.

Returns

Returns an object with:
db
function
Execute database operations using the main client or transaction client (if inside withTransaction).
tx
function
Execute operations that require being inside a transaction. Type-safe - only works within withTransaction.
Drizzle
Context.Tag
Context tag for the database client. Use this to access the raw client if needed.
DrizzleTx
Context.Tag
Context tag for transaction clients. Only available inside withTransaction.
withTransaction
function
Execute an Effect within a database transaction. Automatically rolls back on errors.
layer
Layer
Layer that provides the database client to Effects.

Basic Usage

Setup

import { createDrizzle } from 'ff-effect/for/drizzle';
import { drizzle } from 'drizzle-orm/postgres-js';
import { Effect } from 'effect';
import postgres from 'postgres';

const sql = postgres(process.env.DATABASE_URL);
const db = drizzle(sql);

const Database = createDrizzle(
  Effect.succeed(db)
);

Simple Queries

import { Effect } from 'effect';
import { users } from './schema';

const program = Effect.gen(function* () {
  // Select all users
  const allUsers = yield* Database.db((db) => 
    db.select().from(users)
  );
  
  // Insert a user
  yield* Database.db((db) =>
    db.insert(users).values({ name: 'Alice', email: 'alice@example.com' })
  );
  
  // Update users
  yield* Database.db((db) =>
    db.update(users)
      .set({ name: 'Alice Updated' })
      .where(eq(users.email, 'alice@example.com'))
  );
  
  return allUsers;
}).pipe(Effect.provide(Database.layer));

Transactions

Use withTransaction to execute multiple operations atomically.

Basic Transaction

import { Effect } from 'effect';
import { users, accounts } from './schema';

const program = Effect.gen(function* () {
  const result = yield* Database.withTransaction(
    Effect.gen(function* () {
      // Both operations run in the same transaction
      const [user] = yield* Database.db((db) =>
        db.insert(users)
          .values({ name: 'Bob', email: 'bob@example.com' })
          .returning()
      );
      
      yield* Database.db((db) =>
        db.insert(accounts)
          .values({ userId: user.id, balance: 100 })
      );
      
      return user;
    })
  );
  
  return result;
}).pipe(Effect.provide(Database.layer));

Automatic Rollback

Transactions automatically roll back on errors:
const program = Effect.gen(function* () {
  const result = yield* Database.withTransaction(
    Effect.gen(function* () {
      yield* Database.db((db) =>
        db.insert(users).values({ name: 'Charlie', email: 'charlie@example.com' })
      );
      
      // This will cause a rollback
      yield* Effect.fail(new Error('Something went wrong'));
      
      // This never executes, and the user insert is rolled back
      yield* Database.db((db) =>
        db.insert(accounts).values({ userId: 1, balance: 100 })
      );
    })
  ).pipe(Effect.either);
  
  // result._tag === 'Left'
  // Database remains unchanged
  return result;
}).pipe(Effect.provide(Database.layer));

Transaction-Only Operations with tx

Use tx for operations that must run inside a transaction (enforced at compile-time):
const program = Effect.gen(function* () {
  const result = yield* Database.withTransaction(
    Effect.gen(function* () {
      // tx is type-safe - only available inside withTransaction
      const [user] = yield* Database.tx((tx) =>
        tx.insert(users)
          .values({ name: 'Dave', email: 'dave@example.com' })
          .returning()
      );
      
      // You can mix db and tx calls
      yield* Database.db((db) =>
        db.insert(accounts).values({ userId: user.id, balance: 50 })
      );
      
      return user;
    })
  );
  
  return result;
}).pipe(Effect.provide(Database.layer));

// This won't compile - tx requires DrizzleTx context
const invalid = Database.tx((tx) => tx.select().from(users));
// Error: Missing DrizzleTx requirement

Multiple Databases

Use custom tagId to work with multiple databases:
import { createDrizzle } from 'ff-effect/for/drizzle';
import { Effect } from 'effect';

// Primary database
const MainDb = createDrizzle(
  Effect.succeed(drizzle(mainConnection))
);

// Analytics database with custom tag
const AnalyticsDb = createDrizzle(
  Effect.succeed(drizzle(analyticsConnection)),
  { tagId: 'AnalyticsDb' }
);

const program = Effect.gen(function* () {
  // Use both databases
  const users = yield* MainDb.db((db) => db.select().from(usersTable));
  
  yield* AnalyticsDb.db((db) =>
    db.insert(eventsTable).values({ event: 'users_fetched', count: users.length })
  );
  
  return users;
}).pipe(
  Effect.provide(MainDb.layer),
  Effect.provide(AnalyticsDb.layer)
);

Error Handling

DrizzleError

All database operations can fail with DrizzleError:
import { DrizzleError } from 'ff-effect/for/drizzle';
import { Effect } from 'effect';

const program = Effect.gen(function* () {
  const users = yield* Database.db((db) =>
    db.select().from(usersTable)
  ).pipe(
    Effect.catchTag('ff-effect/DrizzleError', (error) =>
      Effect.gen(function* () {
        console.error('Database error:', error.message);
        console.error('Cause:', error.cause);
        // Provide fallback
        return [];
      })
    )
  );
  
  return users;
}).pipe(Effect.provide(Database.layer));

Transaction Errors

Transaction errors preserve the original error type:
class ValidationError extends Data.TaggedError('ValidationError')<{
  field: string;
}> {}

const program = Effect.gen(function* () {
  const result = yield* Database.withTransaction(
    Effect.gen(function* () {
      yield* Database.db((db) =>
        db.insert(users).values({ name: 'Eve', email: 'eve@example.com' })
      );
      
      // Custom error - will cause rollback
      yield* Effect.fail(new ValidationError({ field: 'email' }));
    })
  ).pipe(
    Effect.catchTags({
      'ValidationError': (error) =>
        Effect.sync(() => console.log(`Validation failed: ${error.field}`)),
      'ff-effect/DrizzleError': (error) =>
        Effect.sync(() => console.log(`Database failed: ${error.message}`))
    })
  );
  
  return result;
}).pipe(Effect.provide(Database.layer));

Real-World Example

import { createDrizzle } from 'ff-effect/for/drizzle';
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
import { Effect, Layer } from 'effect';
import { users, posts, comments } from './schema';

// Setup
const Database = createDrizzle(
  Effect.sync(() => {
    const client = createClient({
      url: process.env.DATABASE_URL!,
      authToken: process.env.DATABASE_AUTH_TOKEN
    });
    return drizzle(client);
  })
);

// Service using database
class PostService extends Effect.Service<PostService>()('PostService', {
  dependencies: [Database.layer],
  effect: Effect.gen(function* () {
    return {
      createPostWithComments: (data: {
        title: string;
        content: string;
        comments: string[];
      }) =>
        Database.withTransaction(
          Effect.gen(function* () {
            // Insert post
            const [post] = yield* Database.db((db) =>
              db.insert(posts)
                .values({ title: data.title, content: data.content })
                .returning()
            );
            
            // Insert comments
            if (data.comments.length > 0) {
              yield* Database.db((db) =>
                db.insert(comments).values(
                  data.comments.map((text) => ({
                    postId: post.id,
                    text
                  }))
                )
              );
            }
            
            return post;
          })
        ),
      
      getPostWithComments: (postId: number) =>
        Effect.gen(function* () {
          const [post] = yield* Database.db((db) =>
            db.select().from(posts).where(eq(posts.id, postId))
          );
          
          const postComments = yield* Database.db((db) =>
            db.select().from(comments).where(eq(comments.postId, postId))
          );
          
          return { ...post, comments: postComments };
        })
    };
  })
}) {}

// Usage
const program = Effect.gen(function* () {
  const postService = yield* PostService;
  
  const post = yield* postService.createPostWithComments({
    title: 'Hello Effect!',
    content: 'This is my first post.',
    comments: ['Great post!', 'Thanks for sharing!']
  });
  
  const fullPost = yield* postService.getPostWithComments(post.id);
  return fullPost;
}).pipe(Effect.provide(PostService.Default));

Type Exports

The integration exports various Effect internal type IDs to avoid TypeScript declaration file generation issues. You don’t need to use these directly.
import {
  TagTypeId,
  ChannelTypeId,
  EffectTypeId,
  NodeInspectSymbol,
  STMTypeId,
  SinkTypeId,
  StreamTypeId,
  Unify
} from 'ff-effect/for/drizzle';

See Also