| name | platform-abstraction |
| description | Use @effect/platform abstractions for cross-platform file I/O, process spawning, HTTP clients, and terminal operations. Apply this skill when writing code that interacts with the filesystem, spawns processes, makes HTTP requests, or performs console I/O to ensure portability across Node.js, Bun, and browser environments. |
Platform Abstraction with @effect/platform
Overview
The @effect/platform library provides platform-independent abstractions that work seamlessly across Node.js, Bun, and browser environments. Instead of using runtime-specific APIs directly, you write code once using Effect Platform services and run it anywhere.
When to use this skill:
- Writing file system operations
- Spawning child processes or executing commands
- Making HTTP requests
- 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 { FileSystem } from "@effect/platform"
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:
3. Resource Safety
Automatic cleanup with Scope:
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const file = yield* fs.open("data.txt")
return yield* file.readAllBytes()
})
4. Testability
Easy to mock and stub services:
const TestFileSystem = Layer.succeed(
FileSystem.FileSystem,
FileSystem.make({
readFileString: () => Effect.succeed('{"mock": "data"}')
})
)
const test = myProgram.pipe(Effect.provide(TestFileSystem))
5. Composability
Integrates naturally with Effect's service system:
export const ConfigService = Context.Tag<ConfigService>()
export 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:
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")
const file = Bun.file("file.txt")
const content = await file.text()
Correct Pattern - FileSystem Service:
import { FileSystem } from "@effect/platform"
import { Effect } from "effect"
const readFile = (path: string) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
return yield* fs.readFileString(path)
})
Common Operations:
import { FileSystem } from "@effect/platform"
const fileOperations = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const text = yield* fs.readFileString("data.txt")
const bytes = yield* fs.readFile("binary.dat")
yield* fs.writeFileString("output.txt", "Hello World")
yield* fs.makeDirectory("new-dir", { recursive: true })
const files = yield* fs.readDirectory("src")
const stats = yield* fs.stat("file.txt")
const exists = yield* fs.exists("config.json")
yield* fs.copy("source.txt", "dest.txt")
yield* fs.rename("old.txt", "new.txt")
yield* fs.remove("temp-file.txt")
yield* fs.remove("temp-dir", { recursive: true })
const tempFile = yield* fs.makeTempFileScoped()
yield* fs.writeFileString(tempFile, "temporary data")
})
Streaming Files:
import { FileSystem } from "@effect/platform"
import { Stream } from "effect"
const processLargeFile = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const stream = fs.stream("large-file.txt", { chunkSize: 64 * 1024 })
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:
import path from "path"
const configPath = "./config/" + filename + ".json"
const absPath = process.cwd() + "/" + configPath
const joined = path.join("src", "components", "Button.tsx")
Correct Pattern - Path Service:
import { Path } from "@effect/platform"
import { Effect } from "effect"
const buildPath = (filename: string) =>
Effect.gen(function* () {
const path = yield* Path.Path
const configPath = path.join("config", `${filename}.json`)
const absolutePath = path.resolve(configPath)
const dir = path.dirname(absolutePath)
const base = path.basename(absolutePath)
const ext = path.extname(absolutePath)
const parsed = path.parse(absolutePath)
return absolutePath
})
Path Operations:
import { Path } from "@effect/platform"
const pathOps = Effect.gen(function* () {
const path = yield* Path.Path
const sep = path.sep
const filePath = path.join("src", "lib", "utils.ts")
const absolute = path.resolve("..", "config", "app.json")
const rel = path.relative("/app/src", "/app/dist")
const isAbs = path.isAbsolute("/usr/local")
const normalized = path.normalize("src/../lib/./utils.ts")
const url = yield* path.toFileUrl("/path/to/file")
const fromUrl = yield* path.fromFileUrl(new URL("file:///path/to/file"))
})
Command - Process Execution
The Command and CommandExecutor services enable safe process spawning.
Anti-Pattern - Direct child_process:
import { spawn, exec } from "child_process"
import { promisify } from "util"
const execAsync = promisify(exec)
const { stdout } = await execAsync("ls -la")
const proc = Bun.spawn(["ls", "-la"])
const output = await new Response(proc.stdout).text()
Correct Pattern - Command Service:
import { Command, CommandExecutor } from "@effect/platform"
import { Effect, Stream } from "effect"
const runCommand = Effect.gen(function* () {
const executor = yield* CommandExecutor.CommandExecutor
const cmd = Command.make("ls", "-la")
const output = yield* cmd.pipe(
Command.stdout("string"),
Effect.flatMap((proc) => proc.exitCode)
)
return output
})
Advanced Command Usage:
import { Command, CommandExecutor } from "@effect/platform"
const commandExamples = Effect.gen(function* () {
const executor = yield* CommandExecutor.CommandExecutor
const ls = Command.make("ls", "-la")
const exitCode = yield* Command.exitCode(ls)
const git = Command.make("git", "status")
const stdout = yield* Command.string(git)
const lines = yield* Command.lines(git)
const stream = Command.stdout(git)
yield* stream.pipe(
Stream.mapEffect((chunk) => Effect.log(chunk.toString())),
Stream.runDrain
)
const pipeline = Command.make("cat", "file.txt").pipe(
Command.pipeTo(Command.make("grep", "error")),
Command.pipeTo(Command.make("wc", "-l"))
)
const withEnv = Command.make("node", "script.js").pipe(
Command.env({
NODE_ENV: "production",
API_KEY: "secret"
})
)
const withCwd = Command.make("npm", "install").pipe(
Command.workingDirectory("/path/to/project")
)
const withInput = Command.make("base64").pipe(
Command.feed("Hello World")
)
const process = yield* executor.start(git)
const code = yield* process.exitCode
return code
})
Terminal - Terminal I/O
The Terminal service provides interactive terminal capabilities.
Anti-Pattern - Direct Console:
console.log("Hello World")
console.error("Error occurred")
process.stdout.write("Output\n")
const input = prompt("Enter name:")
Correct Pattern - Terminal Service:
import { Terminal } from "@effect/platform"
import { Effect } from "effect"
const interactiveProgram = Effect.gen(function* () {
const terminal = yield* Terminal.Terminal
yield* terminal.display("Hello World\n")
const name = yield* terminal.readLine
yield* terminal.display(`Welcome, ${name}!\n`)
const cols = yield* terminal.columns
yield* terminal.display(`Terminal width: ${cols}\n`)
})
For Simple Logging - Use Console or Effect.log:
import { Console, Effect } 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")
})
const structuredLog = Effect.gen(function* () {
yield* Effect.log("Operation started")
yield* Effect.logDebug("Debug details")
yield* Effect.logError("Error occurred")
yield* Effect.log("User action").pipe(
Effect.annotateLogs("userId", "123"),
Effect.annotateLogs("action", "login")
)
})
HttpClient - HTTP Requests
The HttpClient service provides type-safe HTTP operations.
Anti-Pattern - Direct fetch/axios:
const response = await fetch("https://api.example.com/data")
const data = await response.json()
import axios from "axios"
const result = await axios.get("https://api.example.com/data")
Correct Pattern - HttpClient Service:
import { HttpClient, HttpClientRequest } from "@effect/platform"
import { Effect } from "effect"
const fetchData = Effect.gen(function* () {
const client = yield* HttpClient.HttpClient
const response = yield* client.get("https://api.example.com/data")
const data = yield* response.json
return data
})
Advanced HTTP Operations:
import {
HttpClient,
HttpClientRequest,
HttpClientResponse
} from "@effect/platform"
import { Effect, Schema } from "effect"
const User = Schema.Struct({
id: Schema.Number,
name: Schema.String,
email: Schema.String
})
const httpExamples = Effect.gen(function* () {
const client = yield* HttpClient.HttpClient
const getUsers = client.get("https://api.example.com/users", {
urlParams: { page: "1", limit: "10" }
})
const createUser = client.post("https://api.example.com/users", {
body: HttpClientRequest.jsonBody({
name: "John Doe",
email: "john@example.com"
})
})
const withAuth = client.get("https://api.example.com/protected").pipe(
HttpClientRequest.setHeader("Authorization", "Bearer token")
)
const users = yield* client.get("https://api.example.com/users").pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Array(User)))
)
const safeRequest = client.get("https://api.example.com/data").pipe(
Effect.catchTag("RequestError", (error) =>
Effect.succeed({ error: "Network error" })
),
Effect.catchTag("ResponseError", (error) =>
Effect.succeed({ error: `HTTP ${error.status}` })
)
)
const withRetries = client.get("https://api.example.com/data").pipe(
Effect.retry({
times: 3,
schedule: Schedule.exponential("100 millis")
})
)
return users
})
KeyValueStore - Key-Value Storage
The KeyValueStore service provides platform-independent key-value storage.
Anti-Pattern - Direct localStorage/file-based storage:
localStorage.setItem("key", "value")
const value = localStorage.getItem("key")
import fs from "fs"
fs.writeFileSync(".cache/key", "value")
const value = fs.readFileSync(".cache/key", "utf-8")
Correct Pattern - KeyValueStore Service:
import { KeyValueStore } from "@effect/platform"
import { Effect, Schema } from "effect"
const cacheData = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore
yield* store.set("user:123", "John Doe")
const name = yield* store.get("user:123")
const hasUser = yield* store.has("user:123")
yield* store.remove("user:123")
yield* store.clear
return name
})
Schema-Based Store:
import { KeyValueStore } from "@effect/platform"
import { Schema } from "effect"
const User = Schema.Struct({
id: Schema.Number,
name: Schema.String,
email: Schema.String
})
const typedStore = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore
const userStore = store.forSchema(User)
yield* userStore.set("user:123", {
id: 123,
name: "John Doe",
email: "john@example.com"
})
const user = yield* userStore.get("user:123")
})
CLI Arguments - @effect/cli
For CLI applications, use @effect/cli instead of direct process.argv.
Anti-Pattern - Direct process.argv:
const args = process.argv.slice(2)
const input = args[0]
const verbose = args.includes("--verbose")
import yargs from "yargs"
const argv = yargs(process.argv.slice(2)).argv
Correct Pattern - @effect/cli:
import { Args, Command as CliCommand, Options } from "@effect/cli"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"
const inputArg = Args.file({ name: "input", exists: "yes" })
const verboseOpt = Options.boolean("verbose").pipe(
Options.withAlias("v")
)
const command = CliCommand.make("process", { input: inputArg, verbose: verboseOpt },
({ input, verbose }) =>
Effect.gen(function* () {
if (verbose) {
yield* Console.log(`Processing file: ${input}`)
}
})
)
const cli = CliCommand.run(command, {
name: "File Processor",
version: "1.0.0"
})
cli(process.argv).pipe(
Effect.provide(NodeContext.layer),
NodeRuntime.runMain
)
Platform Module Reference
Complete reference table of platform abstractions:
| Need | Use | Instead of | Package |
|---|
| File I/O | FileSystem.FileSystem | fs, Bun.file | @effect/platform |
| Path Operations | Path.Path | path, string concat | @effect/platform |
| Process Spawning | Command + CommandExecutor | child_process, Bun.spawn | @effect/platform |
| Terminal I/O | Terminal.Terminal | process.stdin/stdout | @effect/platform |
| Console Logging | Console.log or Effect.log | console.log | effect |
| HTTP Client | HttpClient.HttpClient | fetch, axios | @effect/platform |
| HTTP Server | HttpServer.HttpServer | http.createServer | @effect/platform |
| Key-Value Store | KeyValueStore.KeyValueStore | localStorage, manual files | @effect/platform |
| CLI Arguments | @effect/cli Args | process.argv, yargs | @effect/cli |
| Environment Variables | Config from effect | process.env | effect |
| Workers | Worker.Worker | Worker, worker_threads | @effect/platform |
| Sockets | Socket.Socket | net.Socket, WebSocket | @effect/platform |
| Streams | Stream | Node streams, ReadableStream | effect |
Setting Up Platform-Specific Layers
To use platform services, provide the appropriate platform layer:
Node.js:
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
return yield* fs.readFileString("file.txt")
})
program.pipe(
Effect.provide(NodeContext.layer),
NodeRuntime.runMain
)
Bun:
import { BunContext, BunRuntime } from "@effect/platform-bun"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
return yield* fs.readFileString("file.txt")
})
program.pipe(
Effect.provide(BunContext.layer),
BunRuntime.runMain
)
Browser:
import { BrowserContext, BrowserRuntime } from "@effect/platform-browser"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const http = yield* HttpClient.HttpClient
return yield* http.get("https://api.example.com/data")
})
program.pipe(
Effect.provide(BrowserContext.layer),
BrowserRuntime.runMain
)
Complete Example: Cross-Platform File Processor
import { FileSystem, Path, Command, CommandExecutor } from "@effect/platform"
import { Effect, Console, Schema } from "effect"
const Config = Schema.Struct({
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 executor = yield* CommandExecutor.CommandExecutor
const configData = yield* fs.readFileString("config.json")
const config = yield* Schema.decode(Config)(JSON.parse(configData))
yield* fs.makeDirectory(config.outputDir, { recursive: true })
const files = yield* fs.readDirectory(config.inputDir)
yield* Console.log(`Processing ${files.length} files...`)
yield* Effect.forEach(files, (file) =>
Effect.gen(function* () {
const inputPath = path.join(config.inputDir, file)
const outputPath = path.join(config.outputDir, file)
yield* fs.copyFile(inputPath, outputPath)
if (config.compress) {
const cmd = Command.make("gzip", outputPath)
yield* Command.exitCode(cmd)
}
yield* Console.log(`Processed: ${file}`)
}),
{ concurrency: 4 }
)
yield* Console.log("All files processed!")
})
import { NodeContext, NodeRuntime } from "@effect/platform-node"
processFiles.pipe(
Effect.provide(NodeContext.layer),
NodeRuntime.runMain
)
import { BunContext, BunRuntime } from "@effect/platform-bun"
processFiles.pipe(
Effect.provide(BunContext.layer),
BunRuntime.runMain
)
Testing with Platform Abstractions
One major benefit of platform abstractions is testability:
import { FileSystem } from "@effect/platform"
import { Effect, Layer } from "effect"
const TestFileSystem = Layer.succeed(
FileSystem.FileSystem,
FileSystem.make({
readFileString: (path) => {
if (path === "config.json") {
return Effect.succeed('{"key": "value"}')
}
return Effect.fail(new Error("File not found"))
},
writeFileString: (path, content) => {
console.log(`Would write to ${path}: ${content}`)
return Effect.void
},
exists: (path) => Effect.succeed(true),
})
)
const testProgram = myFileProcessor.pipe(
Effect.provide(TestFileSystem)
)
Effect.runPromise(testProgram)
Quality Checklist
Before completing code that uses platform operations:
Common Mistakes to Avoid
1. Mixing Platform APIs
import { FileSystem } from "@effect/platform"
import fs from "fs"
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")
})
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
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
return yield* fs.readFileString("file.txt")
})
Effect.runPromise(program)
program.pipe(
Effect.provide(NodeContext.layer),
NodeRuntime.runMain
)
3. Using console.log
const program = Effect.gen(function* () {
console.log("Starting...")
const result = yield* someOperation()
console.log("Done!")
return result
})
const program = 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 fs from "fs/promises"
const data = await fs.readFile("file.txt", "utf-8")
await fs.writeFile("output.txt", data)
const exists = fs.existsSync("config.json")
import { FileSystem } from "@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
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()
import { HttpClient, HttpClientRequest } from "@effect/platform"
const program = Effect.gen(function* () {
const client = yield* HttpClient.HttpClient
const response = yield* client.post("https://api.example.com/data", {
body: HttpClientRequest.jsonBody({ key: "value" })
})
const data = yield* response.json
return data
})
From child_process to Command
import { exec } from "child_process"
import { promisify } from "util"
const execAsync = promisify(exec)
const { stdout } = await execAsync("git status")
import { Command } from "@effect/platform"
const program = Effect.gen(function* () {
const cmd = Command.make("git", "status")
const stdout = yield* Command.string(cmd)
return stdout
})
Summary
Effect Platform provides a complete abstraction layer over platform-specific APIs, enabling you to:
- Write once, run anywhere - Same code works on Node.js, Bun, and browsers
- 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.