effect-http-bridge
v0.1.1
Published
Type-safe HTTP client for Effect HTTP api with tRPC-like ergonomics.
Maintainers
Readme
effect-http-bridge
Type-safe HTTP client for Effect HTTP APIs with tRPC-like ergonomics. Server-side focused, returns Promise<Result<A, E>> for clean async/await usage in React Server Components, server actions, and other async contexts.
Why?
Effect's HTTP API is powerful, but adopting Effect across your entire stack can be daunting. This library lets you incrementally adopt Effect or leverage its amazing capabilities on the backend without needing to learn the Effect runtime:
- 🎯 tRPC-like DX:
client.group.endpoint(params)- fully typed - 🔄 Promise-based: Works with
async/await- no Effect runtime needed - 🎨 Type-safe errors: Pattern match on specific error types
- 💎 Errors as values: No exceptions thrown, all errors are explicit in the type system
- ⚡ Server-first: Perfect for Next.js Server Components & Actions
- 📦 Tiny: Minimal wrapper around Effect Platform
Installation
npm install effect-http-bridgepnpm add effect-http-bridgebun add effect-http-bridgeQuick Start
1. Define Your API
First, define your Effect HTTP API using @effect/platform:
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform";
import { Schema } from "effect";
class Api extends HttpApi.make("api").add(
HttpApiGroup.make("counter")
.add(HttpApiEndpoint.get("count", "/count").addSuccess(Schema.Number))
.add(HttpApiEndpoint.post("increment", "/increment"))
) {}2. Create Your Client
Use HttpBridge.Tag to create a typed client with automatic setup:
import { HttpBridge } from "effect-http-bridge";
import { FetchHttpClient } from "@effect/platform";
import { Api } from "./api";
export class ApiClient extends HttpBridge.Tag<ApiClient>()("ApiClient", {
api: Api,
httpClient: FetchHttpClient.layer,
baseUrl: "http://localhost:3000",
}) {}3. Use It Anywhere
The client works with async/await and returns Promise<Result<A, E>>:
// In a React Server Component
const result = await ApiClient.query("counter", "count", {});
// In a Server Action
const result = await ApiClient.mutation("counter", "increment")({});Usage Examples
React Server Components
Use Result.builder() for type-safe pattern matching in your UI:
import { Result } from "effect-http-bridge";
import { ApiClient } from "./api";
export default async function Page() {
const result = await ApiClient.query("counter", "count", {});
return (
<div>
{Result.builder(result)
.onSuccess((value, success) => (
<div>
<p>Count: {value}</p>
<p className="text-xs text-gray-400">
Updated: {new Date(success.timestamp).toISOString()}
</p>
</div>
))
.onErrorTag("RequestError", (error) => (
<div className="text-red-600">
Network error: Could not connect to server
</div>
))
.onErrorTag("ResponseError", (error) => (
<div className="text-red-600">Server error: {error.message}</div>
))
.onDefect((defect) => (
<div className="text-red-600">Unexpected error: {String(defect)}</div>
))
.orElse(() => (
<div className="text-red-600">Unknown error</div>
))}
</div>
);
}Server Actions
Multiple patterns for handling results in server actions:
Pattern 1: Type Guards (Simplest)
"use server";
import { Result } from "effect-http-bridge";
import { Cause } from "effect";
import { ApiClient } from "./api";
export async function incrementCounter() {
const result = await ApiClient.mutation("counter", "increment")({});
if (Result.isFailure(result)) {
return {
success: false,
error: Cause.pretty(result.cause),
};
}
return {
success: true,
data: result.value,
};
}Pattern 2: Result.match() for Control Flow
"use server";
import { Result } from "effect-http-bridge";
import { Cause } from "effect";
import { ApiClient } from "./api";
export async function getCount() {
const result = await ApiClient.query("counter", "count", {});
return Result.match(result, {
onSuccess: (s) => ({
success: true as const,
data: s.value,
}),
onFailure: (f) => ({
success: false as const,
error: Cause.pretty(f.cause),
}),
});
}Pattern 3: Result.builder() for Specific Errors
"use server";
import { Result } from "effect-http-bridge";
import { ApiClient } from "./api";
export async function deleteUser(userId: string) {
const result = await ApiClient.mutation(
"users",
"delete"
)({
path: { id: userId },
});
return Result.builder(result)
.onSuccess((value) => ({
success: true as const,
message: "User deleted successfully",
data: value,
}))
.onErrorTag("NotFound", (error) => ({
success: false as const,
error: "User not found",
code: "NOT_FOUND",
}))
.onErrorTag("Unauthorized", (error) => ({
success: false as const,
error: "You don't have permission to delete this user",
code: "UNAUTHORIZED",
}))
.onError((error) => ({
success: false as const,
error: "Failed to delete user",
code: "UNKNOWN_ERROR",
}))
.orElse(() => ({
success: false as const,
error: "An unexpected error occurred",
code: "UNEXPECTED",
}));
}Pattern 4: Extract Value or Throw
"use server";
import { Result } from "effect-http-bridge";
import { ApiClient } from "./api";
export async function getCountOrThrow() {
const result = await ApiClient.query("counter", "count", {});
// Throws if failure, returns value if success
return Result.getOrThrow(result);
}Pattern 5: Extract Value with Default
"use server";
import { Result } from "effect-http-bridge";
import { ApiClient } from "./api";
export async function getCountWithDefault() {
const result = await ApiClient.query("counter", "count", {});
// Returns default if failure
return Result.getOrElse(result, () => 0);
}API Reference
HttpBridge.Tag
Creates a typed client from your Effect HTTP API definition.
class YourClient extends HttpBridge.Tag<YourClient>()("YourClient", {
api: YourApi, // Your HttpApi definition
httpClient: FetchHttpClient.layer, // HTTP client layer
baseUrl: "http://localhost:3000", // Base URL for requests
}) {}Client Methods
query(group, endpoint, params): Make a GET requestmutation(group, endpoint): Returns a function for POST/PUT/DELETE requests
Result Type
type Result<A, E> = Success<A, E> | Failure<A, E>;
interface Success<A, E> {
_tag: "Success";
value: A;
timestamp: number;
}
interface Failure<A, E> {
_tag: "Failure";
cause: Cause.Cause<E>;
}Result Utilities
Result.builder(result): Fluent API for pattern matching.onSuccess((value, success) => T): Handle success case.onErrorTag(tag, (error) => T): Handle specific error types.onErrorTag([tag1, tag2], (error) => T): Handle multiple error types.onError((cause) => T): Handle any error.onDefect((defect, failure) => T): Handle unexpected errors.orElse(() => T): Fallback handler
Result.match(result, { onSuccess, onFailure }): Simple pattern matchingResult.isSuccess(result): Type guard for successResult.isFailure(result): Type guard for failureResult.getOrThrow(result): Extract value or throwResult.getOrElse(result, fallback): Extract value or use default
TypeScript Support
Full type inference for:
- Request parameters (path, query, payload)
- Success values
- Error types (tagged unions)
- Pattern matching exhaustiveness
Acknowledgments
This library was inspired by and borrows concepts & code from effect-atom by @tim-smart. Special thanks for the excellent work on bridging Effect with React and demonstrating patterns for integrating Effect's HttpApi in a more accessible way.
License
MIT
