Betalink Docs
Effect Integration

Effect Examples

Complete examples of using the Betalink API client with Effect

Complete Setup Example

import {
  BetalinkApiClient,
  BetalinkApiClientLive,
  makeConfigLayer,
  UnauthorizedApiError,
  CompanyNotFoundApiError,
} from "@betalink/api-client/effect";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Config from "effect/Config";

// Create config from environment
const configLayer = Layer.unwrapEffect(
  Effect.gen(function* () {
    const apiKey = yield* Config.string("BETALINK_API_KEY");
    const baseUrl = yield* Config.string("BETALINK_API_URL").pipe(
      Config.withDefault("https://betalink.dev/api")
    );

    return makeConfigLayer({ baseUrl, apiKey });
  })
);

// Full client layer
const clientLayer = Layer.provide(BetalinkApiClientLive, configLayer);

// Your program
const main = Effect.gen(function* () {
  const client = yield* BetalinkApiClient;

  const companies = yield* client.listCompanies({ limit: 10 });
  console.log(`Found ${companies.meta.total} companies`);

  for (const company of companies.data) {
    console.log(`- ${company.cvr}: ${company.name}`);
  }
});

// Run
Effect.runPromise(Effect.provide(main, clientLayer));

Fetching All Pages

const fetchAllCompanies = Effect.gen(function* () {
  const client = yield* BetalinkApiClient;
  const allCompanies: Company[] = [];
  let after: string | undefined;

  while (true) {
    const response = yield* client.listCompanies({ limit: 100, after });
    allCompanies.push(...response.data);

    if (!response.meta.hasMore) break;
    after = response.data[response.data.length - 1].id;
  }

  return allCompanies;
});

Parallel Requests

const fetchMultipleCompanies = (cvrs: string[]) =>
  Effect.gen(function* () {
    const client = yield* BetalinkApiClient;

    // Fetch all companies in parallel
    const companies = yield* Effect.all(
      cvrs.map((cvr) => client.getCompany(cvr)),
      { concurrency: 5 } // Limit concurrent requests
    );

    return companies;
  });

Error Recovery with Fallback

const getCompanyOrDefault = (cvr: string) =>
  Effect.gen(function* () {
    const client = yield* BetalinkApiClient;
    return yield* client.getCompany(cvr);
  }).pipe(
    Effect.catchTag("CompanyNotFoundApiError", () =>
      Effect.succeed({
        id: `unknown-${cvr}`,
        cvr,
        name: "Unknown Company",
        address: null,
        startDate: new Date().toISOString(),
        endDate: new Date().toISOString(),
        status: "CREATED" as const,
        senderRegNumber: null,
        senderAccountNumber: null,
        receiverRegNumber: null,
        receiverAccountNumber: null,
        defaultAmountCents: null,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      })
    )
  );

Retry with Exponential Backoff

import * as Schedule from "effect/Schedule";
import * as Duration from "effect/Duration";

const fetchWithRetry = Effect.gen(function* () {
  const client = yield* BetalinkApiClient;
  return yield* client.listCompanies();
}).pipe(
  Effect.retry(
    Schedule.exponential(Duration.seconds(1)).pipe(
      Schedule.compose(Schedule.recurs(3))
    )
  )
);

Rate Limit Handling

const handleRateLimit = Effect.gen(function* () {
  const client = yield* BetalinkApiClient;
  return yield* client.listCompanies();
}).pipe(
  Effect.catchTag("RateLimitExceededApiError", (error) =>
    Effect.gen(function* () {
      const waitTime = error.retryAfter ?? 60;
      yield* Effect.sleep(Duration.seconds(waitTime));
      const client = yield* BetalinkApiClient;
      return yield* client.listCompanies();
    })
  )
);

Testing with Mocks

import { TestBetalinkApiClient } from "@betalink/api-client/effect";

const testProgram = Effect.gen(function* () {
  const testClient = yield* TestBetalinkApiClient;

  // Configure mock responses
  yield* testClient.setCompanies([
    {
      id: "test-1",
      cvr: "12345678",
      name: "Test Company",
      status: "REGISTERED",
      // ... other fields
    },
  ]);

  // Now BetalinkApiClient will return the mock data
  const client = yield* BetalinkApiClient;
  const companies = yield* client.listCompanies();

  return companies.data[0].name; // "Test Company"
});

// Run with test layer
const result = await Effect.runPromise(
  Effect.provide(testProgram, BetalinkApiClient.Test)
);

Building a Service Layer

import * as Context from "effect/Context";

// Define your service interface
interface CompanyAnalytics {
  readonly getCompanyStats: (cvr: string) => Effect.Effect<
    {
      company: Company;
      transactionCount: number;
      totalAmount: number;
    },
    UnauthorizedApiError | CompanyNotFoundApiError | ApiRequestError
  >;
}

class CompanyAnalyticsService extends Context.Tag("CompanyAnalyticsService")<
  CompanyAnalyticsService,
  CompanyAnalytics
>() {}

// Implement the service
const CompanyAnalyticsLive = Layer.effect(
  CompanyAnalyticsService,
  Effect.gen(function* () {
    const client = yield* BetalinkApiClient;

    return {
      getCompanyStats: (cvr: string) =>
        Effect.gen(function* () {
          const company = yield* client.getCompany(cvr);

          // Fetch all transactions
          const allTransactions: Transaction[] = [];
          let after: string | undefined;

          while (true) {
            const response = yield* client.getCompanyTransactions(cvr, {
              limit: 100,
              after,
            });
            allTransactions.push(...response.data);

            if (!response.meta.hasMore) break;
            after = response.data[response.data.length - 1].id;
          }

          // Calculate stats
          const totalAmount = allTransactions.reduce(
            (sum, tx) => sum + parseFloat(tx.amountDkk),
            0
          );

          return {
            company,
            transactionCount: allTransactions.length,
            totalAmount,
          };
        }),
    };
  })
);

// Compose layers
const appLayer = Layer.provide(
  CompanyAnalyticsLive,
  Layer.provide(BetalinkApiClientLive, configLayer)
);

// Use in your app
const main = Effect.gen(function* () {
  const analytics = yield* CompanyAnalyticsService;
  const stats = yield* analytics.getCompanyStats("12345678");

  console.log(`Company: ${stats.company.name}`);
  console.log(`Transactions: ${stats.transactionCount}`);
  console.log(`Total Amount: ${stats.totalAmount} DKK`);
});

Effect.runPromise(Effect.provide(main, appLayer));

Integration with Effect Runtime

For applications with an existing Effect runtime:

import * as Runtime from "effect/Runtime";
import * as ManagedRuntime from "effect/ManagedRuntime";

// Create a managed runtime with the client layer
const runtime = ManagedRuntime.make(clientLayer);

// Use in your application
async function handleRequest() {
  return runtime.runPromise(
    Effect.gen(function* () {
      const client = yield* BetalinkApiClient;
      return yield* client.listCompanies();
    })
  );
}

// Cleanup on shutdown
process.on("SIGTERM", () => {
  runtime.dispose();
});

On this page