Platform Abstraction with Effect
Effect Source Reference
The Effect v4 source is available at ~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/.
Browse and read files there directly to look up APIs, types, and implementations.
Reference this for:
- FileSystem source:
packages/effect/src/FileSystem.ts - Path source:
packages/effect/src/Path.ts - Crypto source:
packages/effect/src/Crypto.ts - Socket source:
packages/effect/src/unstable/socket/ - Platform layers:
packages/platform/node/,packages/platform/bun/, andpackages/platform/browser/ - Migration guide:
MIGRATION.md - Effect source:
packages/effect/src/
Overview
Effect provides platform-independent abstractions with Node.js and Bun adapters, plus browser adapters for supported services such as HTTP and Crypto. Instead of using runtime-specific APIs directly, you write code once using Effect Platform services and provide the appropriate layer at the edge.
When to use this skill:
- Writing file system operations
- Spawning child processes or executing commands
- Making HTTP requests
- Generating cryptographic random bytes, UUIDs, or digests
- Reading CLI arguments or environment variables
- Performing console/terminal I/O
- Working with paths across different operating systems
- Building cross-platform applications or libraries
Why Effect Platform?
1. Cross-Platform Compatibility
Write once, run anywhere:
import { Effect, FileSystem } from 'effect';
// Works on Node.js and Bun
const readConfig = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return yield* fs.readFileString('config.json');
});
2. Type-Safe Error Handling
All operations track errors in the Effect type signature:
import { Effect, FileSystem } from 'effect';
// Effect<string, PlatformError, FileSystem>
// ↓ ↓ ↓
// Required service
// Typed error channel
// Success value
3. Resource Safety
Automatic cleanup with Scope:
import { Effect, FileSystem } from 'effect';
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// Read entire file into memory
return yield* fs.readFile('data.txt');
});
4. Testability
Easy to mock and stub services:
import { Effect, FileSystem, Layer } from 'effect';
declare const myProgram: Effect.Effect<void, never, FileSystem.FileSystem>;
const TestFileSystem = Layer.succeed(
FileSystem.FileSystem,
FileSystem.make({
readFile: () => Effect.succeed(new Uint8Array())
})
);
const test = myProgram.pipe(Effect.provide(TestFileSystem));
5. Composability
Integrates naturally with Effect's service system:
import { Effect, FileSystem, Layer, Path, Context } from 'effect';
interface ConfigService {
readonly load: (name: string) => Effect.Effect<string>;
}
const ConfigService = Context.Service<ConfigService>('ConfigService');
const ConfigServiceLive = Layer.effect(
ConfigService,
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
return {
load: (name: string) =>
Effect.gen(function* () {
const configPath = path.join('configs', name);
return yield* fs.readFileString(configPath);
})
};
})
);
Core Platform Modules
FileSystem - File Operations
The FileSystem service provides comprehensive file and directory operations.
Anti-Pattern - Direct Node/Bun APIs:
// ❌ WRONG - Platform-specific, not testable
import * as fs from 'fs';
import { readFile } from 'fs/promises';
const content = fs.readFileSync('file.txt', 'utf-8');
const asyncContent = await readFile('file.txt', 'utf-8');
// ❌ WRONG - Bun-specific
declare const Bun: {
file: (path: string) => { text: () => Promise<string> };
};
const file = Bun.file('file.txt');
const content = await file.text();
Correct Pattern - FileSystem Service:
import { Effect, FileSystem } from 'effect';
// ✅ CORRECT - Cross-platform, type-safe, testable
const readFile = (path: string) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return yield* fs.readFileString(path);
});
// Effect<string, PlatformError, FileSystem>
Common Operations:
import { Effect, FileSystem } from 'effect';
const fileOperations = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// Read files
const text = yield* fs.readFileString('data.txt');
const bytes = yield* fs.readFile('binary.dat');
// Write files
yield* fs.writeFileString('output.txt', 'Hello World');
// Directory operations
yield* fs.makeDirectory('new-dir', { recursive: true });
const files = yield* fs.readDirectory('src');
// File metadata
const stats = yield* fs.stat('file.txt');
const exists = yield* fs.exists('config.json');
// Copy and move
yield* fs.copy('source.txt', 'dest.txt');
yield* fs.rename('old.txt', 'new.txt');
// Remove files/directories
yield* fs.remove('temp-file.txt');
yield* fs.remove('temp-dir', { recursive: true });
// Temporary files (auto-cleanup with Scope)
const tempFile = yield* fs.makeTempFileScoped();
yield* fs.writeFileString(tempFile, 'temporary data');
// File automatically deleted when scope closes
});
fs.watch(directory) reports direct-child changes by default; pass { recursive: true } to include nested subdirectories. For open handles, file.seek(offset, 'start' | 'current') returns the new offset as a branded FileSystem.Size. The old FileSystem.File.Descriptor type and file.descriptor property were removed in beta.103; use scoped File methods instead.
Streaming Files:
import { Effect, FileSystem, Stream } from 'effect';
declare const processChunk: (chunk: Uint8Array) => Effect.Effect<void>;
// Stream large files efficiently
const processLargeFile = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// Read as stream
const stream = fs.stream('large-file.txt', { chunkSize: 64 * 1024 });
// Process stream
yield* stream.pipe(
Stream.mapEffect((chunk) => processChunk(chunk)),
Stream.run(fs.sink('output.txt'))
);
});
Path - Path Manipulation
The Path service provides cross-platform path operations.
Anti-Pattern - Manual String Manipulation:
// ❌ WRONG - Breaks on Windows, brittle
import path from 'path';
declare const process: { cwd: () => string };
declare const filename: string;
const configPath = './config/' + filename + '.json';
const absPath = process.cwd() + '/' + configPath;
// ❌ WRONG - Node-specific
const joined = path.join('src', 'components', 'Button.tsx');
Correct Pattern - Path Service:
import { Effect, Path } from 'effect';
// ✅ CORRECT - Cross-platform path handling
const buildPath = (filename: string) =>
Effect.gen(function* () {
const path = yield* Path.Path;
// Join paths correctly for any OS
const configPath = path.join('config', `${filename}.json`);
// Resolve to absolute path
const absolutePath = path.resolve(configPath);
// Extract path components
const dir = path.dirname(absolutePath);
const base = path.basename(absolutePath);
const ext = path.extname(absolutePath);
// Parse path into components
const parsed = path.parse(absolutePath);
// { root, dir, base, ext, name }
return absolutePath;
});
Path Operations:
import { Effect, Path } from 'effect';
const pathOps = Effect.gen(function* () {
const path = yield* Path.Path;
// Platform-specific separator ("/" or "\")
const sep = path.sep;
// Join multiple segments
const filePath = path.join('src', 'lib', 'utils.ts');
// Resolve relative paths
const absolute = path.resolve('..', 'config', 'app.json');
// Get relative path between two paths
const rel = path.relative('/app/src', '/app/dist');
// Check if path is absolute
const isAbs = path.isAbsolute('/usr/local');
// Normalize path (remove "..", ".", etc.)
const normalized = path.normalize('src/../lib/./utils.ts');
// Work with file URLs
const url = yield* path.toFileUrl('/path/to/file');
const fromUrl = yield* path.fromFileUrl(new URL('file:///path/to/file'));
});
In Effect v4, Migrator.fromFileSystem requires both FileSystem.FileSystem and Path.Path because migration module paths are converted to file URLs before dynamic import. Aggregate Node/Bun service layers satisfy both requirements. When providing services individually on Windows, use the host-aware path layer rather than core Path.layer, which has POSIX semantics.
ChildProcess - Process Execution
The ChildProcess and ChildProcessSpawner services enable safe process spawning.
Anti-Pattern - Direct child_process:
// ❌ WRONG - Node-specific, no resource safety
import { spawn, exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
const { stdout } = await execAsync('ls -la');
// ❌ WRONG - Bun-specific
declare const Bun: {
spawn: (cmd: string[]) => { stdout: ReadableStream };
};
declare const Response: {
new (stream: ReadableStream): { text: () => Promise<string> };
};
const proc = Bun.spawn(['ls', '-la']);
const output = await new Response(proc.stdout).text();
Correct Pattern - ChildProcess + ChildProcessSpawner:
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
import { Effect, Stream } from 'effect';
// ✅ CORRECT - Cross-platform command execution
const runCommand = Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
// Create command and collect output as string
const output = yield* spawner.string(ChildProcess.make('ls', ['-la']));
return output;
});
Advanced ChildProcess Usage:
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
import { Console, Effect, Stream } from 'effect';
const commandExamples = Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
// Collect output as string
const stdout = yield* spawner.string(ChildProcess.make('git', ['status']));
// Collect output as lines
const lines = yield* spawner.lines(
ChildProcess.make('git', ['log', '--pretty=format:%s', '-n', '10'])
);
// Pipe commands together
const pipeline = ChildProcess.make('cat', ['file.txt']).pipe(
ChildProcess.pipeTo(ChildProcess.make('grep', ['error'])),
ChildProcess.pipeTo(ChildProcess.make('wc', ['-l']))
);
const pipelineOutput = yield* spawner.string(pipeline);
// Set environment variables
const withEnv = ChildProcess.make('node', ['script.js'], {
env: { NODE_ENV: 'production', API_KEY: 'secret' },
extendEnv: true
});
// Spawn process and stream output
const handle = yield* spawner.spawn(
ChildProcess.make('npm', ['run', 'build'])
);
yield* handle.all.pipe(
Stream.decodeText(),
Stream.splitLines,
Stream.runForEach((line) => Console.log(`[build] ${line}`))
);
const exitCode = yield* handle.exitCode;
return exitCode;
});
Terminal - Terminal I/O
The Terminal service provides interactive terminal capabilities.
Anti-Pattern - Direct Console:
// ❌ WRONG - Uses global console, not testable
declare const console: {
log: (msg: string) => void;
error: (msg: string) => void;
};
declare const process: {
stdout: { write: (msg: string) => void };
};
declare const prompt: (msg: string) => string | null;
console.log('Hello World');
console.error('Error occurred');
process.stdout.write('Output\n');
// ❌ WRONG - Not trackable in Effect type
const input = prompt('Enter name:');
Correct Pattern - Terminal Service:
import { Effect, Terminal } from 'effect';
// ✅ CORRECT - Trackable, testable terminal I/O
const interactiveProgram = Effect.gen(function* () {
const terminal = yield* Terminal.Terminal;
// Display output
yield* terminal.display('Hello World\n');
// Read user input
const name = yield* terminal.readLine;
yield* terminal.display(`Welcome, ${name}!\n`);
// Get terminal dimensions
const cols = yield* terminal.columns;
const rows = yield* terminal.rows;
yield* terminal.display(`Terminal size: ${cols}x${rows}\n`);
});
For Simple Logging - Use Console or Effect.log:
import { Console, Effect } from 'effect';
// ✅ CORRECT - Console service (from effect)
const logging = Effect.gen(function* () {
yield* Console.log('Info message');
yield* Console.error('Error message');
yield* Console.warn('Warning');
yield* Console.debug('Debug info');
});
// ✅ CORRECT - Effect.log with structured logging
const structuredLog = Effect.gen(function* () {
yield* Effect.log('Operation started');
yield* Effect.logDebug('Debug details');
yield* Effect.logError('Error occurred');
// With annotations
yield* Effect.log('User action').pipe(
Effect.annotateLogs('userId', '123'),
Effect.annotateLogs('action', 'login')
);
});
Crypto - Cryptographic Randomness, UUIDs, and Digests
The Crypto.Crypto service provides platform-backed cryptographic random bytes, UUIDv4/v7 generation, and message digests. Prefer it over globalThis.crypto, crypto.randomUUID(), or ad-hoc randomness when code should stay platform-abstract and testable.
import { Crypto, Effect } from 'effect';
const cryptoProgram = Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const bytes = yield* crypto.randomBytes(32);
const uuidV4 = yield* crypto.randomUUIDv4;
const uuidV7 = yield* crypto.randomUUIDv7;
const digest = yield* crypto.digest('SHA-256', bytes);
return { bytes, uuidV4, uuidV7, digest };
});
NodeServices.layer and BunServices.layer include Crypto.Crypto. Browser applications can provide BrowserCrypto.layer from @effect/platform-browser.
HttpClient - HTTP Requests
The HttpClient service provides type-safe HTTP operations. Runtime application and provider code must use it rather than raw fetch. Raw fetch is reserved for an explicitly named low-level platform adapter whose documentation justifies why an Effect transport cannot be used; that adapter must own interruption, status classification, schema decoding, and typed error mapping.
Anti-Pattern - Direct fetch/axios:
// ❌ WRONG - No Effect integration, manual error handling
declare const fetch: (url: string) => Promise<{ json: () => Promise<unknown> }>;
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// ❌ WRONG - External dependency, not in Effect system
import axios from 'axios';
declare const axios: {
get: (url: string) => Promise<{ data: unknown }>;
};
const result = await axios.get('https://api.example.com/data');
Correct Pattern - HttpClient Service:
import { HttpClient, HttpClientResponse } from 'effect/unstable/http';
import { Effect, Schema } from 'effect';
// ✅ CORRECT - Integrated with Effect type system
class ProviderData extends Schema.Class<ProviderData>('ProviderData')({
value: Schema.String
}) {}
const fetchData = Effect.gen(function* () {
const client = yield* HttpClient.HttpClient;
return yield* client.get('https://api.example.com/data').pipe(
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.flatMap(HttpClientResponse.schemaBodyJson(ProviderData))
);
});
Name the adapter service and its effects after the upstream operation. The adapter owns request/auth construction, executes outside database transactions, classifies status before decoding, validates unknown bodies with Schema, and maps failures into typed domain errors. Preserve bounded evidence such as status, provider error code, request ID, and retry metadata, but redact credentials, private fields, query secrets, and full response bodies.
Advanced HTTP Operations:
import {
HttpClient,
HttpClientRequest,
HttpClientResponse
} from 'effect/unstable/http';
import { Effect, Schema, Schedule } from 'effect';
class User extends Schema.Class<User>('User')({
id: Schema.Number,
name: Schema.String,
email: Schema.String
}) {}
const httpExamples = Effect.gen(function* () {
const client = yield* HttpClient.HttpClient;
// GET with query parameters
const getUsers = client.get('https://api.example.com/users', {
urlParams: { page: '1', limit: '10' }
});
// POST with JSON body
const createUser = HttpClientRequest.post(
'https://api.example.com/users'
).pipe(
HttpClientRequest.bodyJsonUnsafe({
name: 'John Doe',
email: 'john@example.com'
}),
client.execute
);
// Custom headers — construct the request, set headers, then execute
const withAuthRequest = HttpClientRequest.get(
'https://api.example.com/protected'
).pipe(HttpClientRequest.setHeader('Authorization', 'Bearer token'));
const withAuth = client.execute(withAuthRequest);
// Classify status, then parse the successful response with Schema
const users = yield* client
.get('https://api.example.com/users')
.pipe(
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.flatMap(
HttpClientResponse.schemaBodyJson(Schema.Array(User))
)
);
// Error handling — all HttpClient errors are "HttpClientError" with a reason field
const safeRequest = client.get('https://api.example.com/data').pipe(
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.catchTag('HttpClientError', (error) => {
switch (error.reason._tag) {
case 'TransportError':
return Effect.succeed({ error: 'Network error' });
case 'StatusCodeError':
return Effect.succeed({
error: `HTTP ${error.response?.status}`
});
default:
return Effect.succeed({
error: `Client error: ${error.reason._tag}`
});
}
})
);
// Retries with backoff: GET is idempotent and attempts are bounded.
const withRetries = client.get('https://api.example.com/data').pipe(
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.retry({
times: 3,
schedule: Schedule.exponential('100 millis')
}),
Effect.tapError((error) =>
Effect.logError('Provider read exhausted retries').pipe(
Effect.annotateLogs({ operation: 'Provider.getData', errorTag: error._tag })
)
)
);
return users;
});
Never install retry automatically on a shared client that also sends non-idempotent POST/PATCH requests. Retry only operations proven idempotent by method/provider contract or protected by a provider-supported idempotency key. Keep exhaustion observable: retain the final typed error and log/measure only redacted status, provider code, request ID, operation, and attempt evidence.
HttpClient.withRateLimiter can automatically retry 429 responses. Use positive times only on a client restricted to proven-idempotent operations; set times: 0 for mixed/non-idempotent clients so the 429 remains visible.
KeyValueStore - Key-Value Storage
The KeyValueStore service provides platform-independent key-value storage.
Anti-Pattern - Direct localStorage/file-based storage:
// ❌ WRONG - Browser-specific
declare const localStorage: {
setItem: (key: string, value: string) => void;
getItem: (key: string) => string | null;
};
localStorage.setItem('key', 'value');
const value = localStorage.getItem('key');
// ❌ WRONG - Node-specific, manual file handling
import fs from 'fs';
declare const fs: {
writeFileSync: (path: string, data: string) => void;
readFileSync: (path: string, encoding: string) => string;
};
fs.writeFileSync('.cache/key', 'value');
const value2 = fs.readFileSync('.cache/key', 'utf-8');
Correct Pattern - KeyValueStore Service:
import { KeyValueStore } from 'effect/unstable/persistence';
import { Effect, Schema } from 'effect';
// ✅ CORRECT - Works on all platforms
const cacheData = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
// Set value
yield* store.set('user:123', 'John Doe');
// Get value — raw string stores return string | undefined
const name = yield* store.get('user:123');
// Binary stores return Uint8Array | undefined
const bytes = yield* store.getUint8Array('avatar:123');
// Check existence
const hasUser = yield* store.has('user:123');
// Remove value
yield* store.remove('user:123');
// Clear all
yield* store.clear;
return name;
});
Schema-Based Store:
import { KeyValueStore } from 'effect/unstable/persistence';
import { Effect, Schema } from 'effect';
class User extends Schema.Class<User>('User')({
id: Schema.Number,
name: Schema.String,
email: Schema.String
}) {}
const typedStore = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
// Create schema-based store
const userStore = KeyValueStore.toSchemaStore(store, User);
// Type-safe operations
yield* userStore.set('user:123', new User({
id: 123,
name: 'John Doe',
email: 'john@example.com'
}));
const user = yield* userStore.get('user:123');
// user: Option.Option<{ id: number, name: string, email: string }>
});
Redis - Commands and Scoped Subscriptions
The portable Redis.Redis service in effect/unstable/persistence provides send, cached script evaluation, and scoped pub/sub. Platform-specific layers provide the client: NodeRedis.layer(...), DenoRedis.layer(...), or BunRedis.layer(...). These are specialized layers and are not included in NodeServices.layer or BunServices.layer.
import { NodeRedis } from '@effect/platform-node';
import { Effect, Queue } from 'effect';
import { Redis } from 'effect/unstable/persistence';
const RedisLayer = NodeRedis.layer({
database: 1,
socket: { host: '127.0.0.1', port: 6379 }
});
const receiveOne = Effect.gen(function* () {
const redis = yield* Redis.Redis;
const subscription = yield* redis.subscribe('events');
return yield* Queue.take(subscription);
}).pipe(Effect.scoped, Effect.provide(RedisLayer));
redis.subscribe(channel) requires Scope and returns a Queue.Dequeue<RedisMessage, RedisError>. Closing the scope shuts down the queue and releases the dedicated subscriber. Node and Deno subscribers reconnect and re-subscribe after interruptions, which can leave message-delivery gaps; Bun subscriptions do not reconnect, so a dropped connection fails the dequeue and callers must subscribe again.
CLI Arguments - effect/unstable/cli
For CLI applications, use effect/unstable/cli instead of direct process.argv.
Anti-Pattern - Direct process.argv:
// ❌ WRONG - Manual parsing, no validation
declare const process: { argv: string[] };
const args = process.argv.slice(2);
const input = args[0];
const verbose = args.includes('--verbose');
// ❌ WRONG - Third-party parser, not Effect-integrated
import yargs from 'yargs';
declare const yargs: (args: string[]) => { argv: Record<string, unknown> };
const argv = yargs(process.argv.slice(2)).argv;
Correct Pattern - effect/unstable/cli:
import { Argument, Command as CliCommand, Flag } from 'effect/unstable/cli';
import { NodeServices, NodeRuntime } from '@effect/platform-node';
import { Console, Effect } from 'effect';
declare const process: { argv: string[] };
declare const someOperation: Effect.Effect<string>;
// ✅ CORRECT - Type-safe CLI with full Effect integration
// Define arguments
const inputArg = Argument.file('input');
// Define flags
const verboseFlag = Flag.boolean('verbose').pipe(Flag.withAlias('v'));
// Define command
const command = CliCommand.make(
'process',
{ input: inputArg, verbose: verboseFlag },
Effect.fn(function* ({ input, verbose }) {
if (verbose) {
yield* Console.log(`Processing file: ${input}`);
}
// Process the file
yield* someOperation;
})
);
// Run CLI
command.pipe(
CliCommand.run({ version: '1.0.0' }),
Effect.provide(NodeServices.layer),
NodeRuntime.runMain
);
Platform Module Reference
Complete reference table of platform abstractions:
| Need | Use | Instead of | Import from |
|---|---|---|---|
| File I/O | FileSystem.FileSystem |
fs, Bun.file |
effect |
| Path Operations | Path.Path |
path, string concat |
effect |
| Process Spawning | ChildProcess + ChildProcessSpawner |
child_process, Bun.spawn |
effect/unstable/process |
| Terminal I/O | Terminal.Terminal |
process.stdin/stdout |
effect |
| Console Logging | Console.log or Effect.log |
console.log |
effect |
| Crypto | Crypto.Crypto |
globalThis.crypto, crypto.randomUUID() |
effect |
| HTTP Client | HttpClient.HttpClient |
fetch, axios |
effect/unstable/http |
| HTTP Server | HttpServer.HttpServer |
http.createServer |
effect/unstable/http |
| Sockets | Socket.Socket / SocketServer.SocketServer |
raw TCP/WebSocket APIs | effect/unstable/socket |
| Key-Value Store | KeyValueStore.KeyValueStore |
localStorage, manual files |
effect/unstable/persistence |
| Redis | Redis.Redis |
direct Redis clients | effect/unstable/persistence |
| CLI Arguments | Argument + Flag + Command |
process.argv, yargs |
effect/unstable/cli |
| Environment Variables | Config from effect |
process.env |
effect |
| Streams | Stream |
Node streams, ReadableStream | effect |
Socket services live in effect/unstable/socket. Use Socket.Socket for scoped bidirectional string/binary frame transports and SocketServer.SocketServer for accepting connections. Provide service-specific layers such as BrowserSocket.layerWebSocket(url), NodeSocket.layerWebSocket(url), NodeSocket.layerNet(options), BunSocket.layerWebSocket(url), or Node/Bun socket-server layers; NodeServices.layer and BunServices.layer do not provide sockets.
Setting Up Platform-Specific Layers
To use platform services, provide the appropriate platform layer.
NodeServices.layer and BunServices.layer provide core process services such as FileSystem, Path, ChildProcessSpawner, Stdio/Terminal, and Crypto.Crypto. They do not provide specialized integrations such as HTTP clients/servers, sockets, workers, or Redis; provide those with service-specific platform layers. For HTTP clients, provide an HTTP-specific layer such as FetchHttpClient.layer, Node's NodeHttpClient.{layerFetch, layerUndici, layerNodeHttp}, BunHttpClient.layer, or Browser's BrowserHttpClient.{layerFetch, layerXMLHttpRequest}. A named provider adapter should export a raw layer that requires HttpClient.HttpClient, plus an optional defaultLayer = layer.pipe(Layer.provide(...transport...)); this keeps the transport dependency graph explicit while offering runtime convenience. For HTTP servers, use server layers such as NodeHttpServer.layer(...), BunHttpServer.layer(...), or their layerHttpServices variants where appropriate.
Node.js:
import { NodeServices, NodeRuntime } from '@effect/platform-node';
import { Effect, FileSystem } from 'effect';
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return yield* fs.readFileString('file.txt');
});
program.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
Bun:
import { BunServices, BunRuntime } from '@effect/platform-bun';
import { Effect, FileSystem } from 'effect';
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return yield* fs.readFileString('file.txt');
});
program.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
Complete Example: Cross-Platform File Processor
import { NodeServices, NodeRuntime } from '@effect/platform-node';
import { BunServices, BunRuntime } from '@effect/platform-bun';
import { Console, Effect, FileSystem, Path, Schema } from 'effect';
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
class FileProcessorConfig extends Schema.Class<FileProcessorConfig>(
'FileProcessorConfig'
)({
inputDir: Schema.String,
outputDir: Schema.String,
compress: Schema.Boolean
}) {}
const processFiles = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
// Load configuration
const configData = yield* fs.readFileString('config.json');
const config = yield* Schema.decodeUnknownEffect(
Schema.fromJsonString(FileProcessorConfig)
)(configData);
// Ensure output directory exists
yield* fs.makeDirectory(config.outputDir, { recursive: true });
// Read input files
const files = yield* fs.readDirectory(config.inputDir);
yield* Console.log(`Processing ${files.length} files...`);
// Process each file
yield* Effect.forEach(
files,
(file) =>
Effect.gen(function* () {
const inputPath = path.join(config.inputDir, file);
const outputPath = path.join(config.outputDir, file);
// Copy file
yield* fs.copy(inputPath, outputPath);
// Optionally compress
if (config.compress) {
yield* spawner.string(
ChildProcess.make('gzip', [outputPath])
);
}
yield* Console.log(`Processed: ${file}`);
}),
{ concurrency: 4 }
);
yield* Console.log('All files processed!');
});
// Run on Node.js
processFiles.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
// Or run on Bun - same code!
processFiles.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
Testing with Platform Abstractions
One major benefit of platform abstractions is testability:
import { Effect, FileSystem, Layer } from 'effect';
declare const myFileProcessor: Effect.Effect<
void,
never,
FileSystem.FileSystem
>;
// Create mock FileSystem using makeNoop for testing
const TestFileSystem = Layer.succeed(
FileSystem.FileSystem,
FileSystem.makeNoop({
readFile: (path) => {
if (path === 'config.json') {
const data = JSON.stringify({ key: 'value' });
return Effect.succeed(new TextEncoder().encode(data));
}
return Effect.fail(new Error('File not found'));
},
exists: (path) => Effect.succeed(true)
})
);
// Test your code
const testProgram = myFileProcessor.pipe(Effect.provide(TestFileSystem));
Effect.runPromise(testProgram);
Quality Checklist
Before completing code that uses platform operations:
- All file I/O uses
FileSystem.FileSystemservice - All path operations use
Path.Pathservice - Process spawning uses
ChildProcess+ChildProcessSpawner - Console output uses
Console.logorEffect.log(notconsole.log) - CLI arguments parsed with
effect/unstable/cli(notprocess.argv) - HTTP requests use
HttpClient.HttpClient(notfetch/axios) - Any raw
fetchis isolated in a named low-level platform adapter with documented justification - HTTP status is classified before success-body schema decoding
- Provider evidence is bounded and redacted; retry exhaustion remains a typed, observable failure
- Provider/network calls execute outside database transactions
- Automatic retries apply only to operations proven idempotent
- Adapter
layerkeepsHttpClient.HttpClientvisible; optionaldefaultLayerowns transport wiring - Cryptographic operations use
Crypto.Crypto(not direct platform crypto APIs) - Platform services accessed through Effect type system
- Appropriate platform/service layer provided (HTTP, sockets, workers, Redis, and other specialized integrations need service-specific layers, not just
NodeServices.layer/BunServices.layer) - No direct imports from
fs,path,child_process,http, etc. - No Bun-specific APIs (
Bun.file,Bun.spawn, etc.) - No browser-specific APIs without platform abstraction
- Code is testable with mock platform services
Common Mistakes to Avoid
1. Mixing Platform APIs
import { Effect, FileSystem } from 'effect';
import fs from 'fs';
// ❌ WRONG - Mixing Effect Platform with direct APIs
const bad = Effect.gen(function* () {
const filesystem = yield* FileSystem.FileSystem;
const content1 = yield* filesystem.readFileString('file1.txt');
const content2 = fs.readFileSync('file2.txt', 'utf-8'); // Don't mix!
});
// ✅ CORRECT - Use platform abstractions consistently
const good = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const content1 = yield* fs.readFileString('file1.txt');
const content2 = yield* fs.readFileString('file2.txt');
});
2. Forgetting Platform Layer
import { NodeServices, NodeRuntime } from '@effect/platform-node';
import { Effect, FileSystem } from 'effect';
// ❌ WRONG - No platform layer provided
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return yield* fs.readFileString('file.txt');
});
Effect.runPromise(program); // Runtime error!
// ✅ CORRECT - Provide platform layer
program.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
3. Using console.log
import { Console, Effect } from 'effect';
declare const someOperation: () => Effect.Effect<string>;
// ❌ WRONG - Direct console usage
const badProgram = Effect.gen(function* () {
console.log('Starting...');
const result = yield* someOperation();
console.log('Done!');
return result;
});
// ✅ CORRECT - Use Console or Effect.log
const goodProgram = Effect.gen(function* () {
yield* Console.log('Starting...');
const result = yield* someOperation();
yield* Console.log('Done!');
return result;
});
Migration Guide
From Node.js fs to FileSystem
import { Effect, FileSystem } from 'effect';
import fs from 'fs/promises';
// Before (Node.js)
declare const fs: {
readFile: (path: string, encoding: string) => Promise<string>;
writeFile: (path: string, data: string) => Promise<void>;
existsSync: (path: string) => boolean;
};
const data = await fs.readFile('file.txt', 'utf-8');
await fs.writeFile('output.txt', data);
const exists = fs.existsSync('config.json');
// After (Effect Platform)
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const data = yield* fs.readFileString('file.txt');
yield* fs.writeFileString('output.txt', data);
const exists = yield* fs.exists('config.json');
});
From fetch to HttpClient
import { Effect, Schema } from 'effect';
import {
HttpClient,
HttpClientRequest,
HttpClientResponse
} from 'effect/unstable/http';
// Before (fetch)
declare const fetch: (
url: string,
options: {
method: string;
headers: Record<string, string>;
body: string;
}
) => Promise<{ json: () => Promise<unknown> }>;
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
});
const data = await response.json();
// After (Effect HttpClient)
class CreateData extends Schema.Class<CreateData>('CreateData')({
key: Schema.String
}) {}
class CreatedData extends Schema.Class<CreatedData>('CreatedData')({
id: Schema.String,
key: Schema.String
}) {}
const program = Effect.gen(function* () {
const client = yield* HttpClient.HttpClient;
return yield* HttpClientRequest.post('https://api.example.com/data').pipe(
HttpClientRequest.schemaBodyJson(CreateData)(new CreateData({ key: 'value' })),
Effect.flatMap(client.execute),
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.flatMap(HttpClientResponse.schemaBodyJson(CreatedData))
);
});
This POST is intentionally not retried. Add retry only if the provider offers a documented idempotency guarantee and the request supplies the required idempotency key.
From child_process to ChildProcess
import { Effect } from 'effect';
import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
import { exec } from 'child_process';
import { promisify } from 'util';
// Before (child_process)
declare const exec: (
cmd: string,
callback: (error: Error | null, result: { stdout: string }) => void
) => void;
declare const promisify: <T>(fn: T) => (...args: any[]) => Promise<any>;
const execAsync = promisify(exec);
const { stdout } = await execAsync('git status');
// After (Effect ChildProcess)
const program = Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const stdout = yield* spawner.string(ChildProcess.make('git', ['status']));
return stdout;
});
Summary
Effect provides a complete abstraction layer over platform-specific APIs, enabling you to:
- Write once, run anywhere - Same code works on Node.js and Bun
- Type-safe operations - All errors tracked in Effect type signatures
- Resource safety - Automatic cleanup with Scope
- Easy testing - Mock services without touching the filesystem
- Full Effect integration - Compose with services, layers, and error handling
Always prefer Effect platform abstractions over direct platform APIs for maximum portability, safety, and testability.