npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

effect-http-bridge

v0.1.1

Published

Type-safe HTTP client for Effect HTTP api with tRPC-like ergonomics.

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-bridge
pnpm add effect-http-bridge
bun add effect-http-bridge

Quick 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 request
  • mutation(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 matching
  • Result.isSuccess(result): Type guard for success
  • Result.isFailure(result): Type guard for failure
  • Result.getOrThrow(result): Extract value or throw
  • Result.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