@teever/ez-hook-effect
v0.5.1
Published
A Discord webhook library built with Effect - type-safe, composable, and resilient
Readme
@teever/ez-hook-effect
A Discord webhook library built with Effect - type-safe, composable, and resilient.
Features
- Type-safe - Runtime validation with Effect schemas
- Composable - Functional programming patterns with Effect
- Error Handling - Comprehensive error tracking with structured validation issues
- Retry Logic - Built-in exponential backoff with jitter
- Dependency Injection - Layer-based architecture
- Validated - All Discord webhook constraints enforced
- One Dependency - Only requires Effect
- Environment Support - Configure via environment variables
- CRUD Operations - Send, get, modify, delete, and validate webhooks
Installation
bunx jsr add @teever/ez-hook-effect
# or
npx jsr add @teever/ez-hook-effectQuick Start
import { Effect, pipe } from 'effect'
import { makeDefaultLayer, sendWebhook, Webhook } from '@teever/ez-hook-effect'
// Configure your webhook
const WEBHOOK_URL = 'https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN'
// Create service layer
const AppLayer = makeDefaultLayer(WEBHOOK_URL)
// Send a simple message
const program = pipe(
Webhook.make,
Webhook.setContent('Hello from Effect!'),
Webhook.setUsername('My Bot'),
Webhook.build,
Effect.flatMap(sendWebhook)
)
// Run the program
Effect.runPromise(program.pipe(Effect.provide(AppLayer)))Pipe-First API (Recommended)
Prefer composing with pipe for ergonomic, Effect-style data assembly. This library uses a single, pipe-first API (no OO builders).
import { Effect, pipe } from 'effect'
import { Webhook, Embed, sendWebhook } from '@teever/ez-hook-effect'
const program = pipe(
Embed.make,
Embed.setTitle('System Status'),
Embed.setDescription('All systems operational'),
Embed.setColor(0x00ff00),
Embed.addField('CPU', '45%', true),
Embed.setTimestamp(),
Embed.build,
Effect.flatMap((embed) =>
pipe(
Webhook.make,
Webhook.setContent('📊 Status Update'),
Webhook.setUsername('Ez-Hook Bot'),
Webhook.addEmbed(embed),
Webhook.build
)
),
Effect.flatMap(sendWebhook)
)Building Rich Embeds
const embedProgram = pipe(
Embed.make,
Embed.setTitle('Server Status'),
Embed.setDescription('All systems operational'),
Embed.setColor(0x00FF00),
Embed.addField('CPU', '45%', true),
Embed.addField('Memory', '2.1GB', true),
Embed.addField('Uptime', '15 days', true),
Embed.build,
Effect.flatMap((embed) =>
pipe(
Webhook.make,
Webhook.setContent('Daily Report'),
Webhook.addEmbed(embed),
Webhook.build
)
),
Effect.flatMap(sendWebhook)
)Error Handling
Retries for rate limits (429) and transient errors (5xx) are handled automatically using configurable exponential backoff. sendWebhook fails the Effect on any non-204 response, so errors always surface to your error handling paths.
import { Effect, pipe } from 'effect'
import { Webhook, sendWebhook, ValidationError, HttpError, NetworkError, RateLimitError, WebhookError } from '@teever/ez-hook-effect'
const program = pipe(
Webhook.make,
Webhook.setContent('Hello!'),
Webhook.build,
Effect.flatMap(sendWebhook),
Effect.catchTag('ValidationError', (error) =>
Effect.logError(`Validation failed: ${error.message}`)
),
Effect.catchTag('RateLimitError', (error) =>
Effect.logError(`Rate limited: retry after ${error.retryAfter}ms`)
),
Effect.catchTag('HttpError', (error) =>
Effect.logError(`Request failed: ${error.message}`)
),
Effect.catchTag('NetworkError', (error) =>
Effect.logError(`Network error: ${error.message}`)
),
Effect.catchTag('WebhookError', (error) =>
Effect.logError(`Webhook error: ${error.message}`)
)
)Structured Validation Errors
Validation errors include detailed issue information:
Effect.catchTag('ValidationError', (error) =>
Effect.logError(`Validation failed:\n${error.format()}`)
)Error Helpers
import { Effect, pipe } from 'effect'
import { Webhook, sendWebhook, formatWebhookError, webhookErrorToLogObject } from '@teever/ez-hook-effect'
const program = pipe(
Webhook.make,
Webhook.setContent('Hello!'),
Webhook.build,
Effect.flatMap(sendWebhook),
Effect.catchAll((error) =>
Effect.logError(formatWebhookError(error)).pipe(
Effect.tap(() => Effect.log(JSON.stringify(webhookErrorToLogObject(error))))
)
)
)Raw Response
When you need status/body, use sendWebhookRaw:
import { Effect, pipe } from 'effect'
import { sendWebhookRaw, Webhook } from '@teever/ez-hook-effect'
const program = pipe(
Webhook.make,
Webhook.setContent('Hello!'),
Webhook.build,
Effect.flatMap(sendWebhookRaw),
Effect.tap((res) => Effect.log(`Status: ${res.status}`))
)Validate Only
import { Effect, pipe } from 'effect'
import { Webhook } from '@teever/ez-hook-effect'
const program = pipe(
Webhook.validate({ content: 'Hello!' }),
Effect.tap(() => Effect.log('Valid payload'))
)Configuration Options
Programmatic Configuration
const AppLayer = WebhookServiceLive.pipe(
Layer.provide(makeConfigLayer(WEBHOOK_URL, {
maxRetries: 5, // Maximum retry attempts
baseDelayMs: 1000, // Base delay for exponential backoff
maxDelayMs: 60000, // Maximum delay between retries
enableJitter: true // Add randomness to retry delays
})),
Layer.provide(HttpClientLive)
)One-Liner Configuration
import { makeDefaultLayer } from '@teever/ez-hook-effect'
const AppLayer = makeDefaultLayer(WEBHOOK_URL, {
maxRetries: 5,
baseDelayMs: 1000,
})Environment Variables
import { ConfigFromEnv, HttpClientLive, WebhookServiceLive } from '@teever/ez-hook-effect'
// Reads from DISCORD_WEBHOOK_URL, WEBHOOK_MAX_RETRIES, etc.
const AppLayer = WebhookServiceLive.pipe(
Layer.provide(ConfigFromEnv),
Layer.provide(HttpClientLive)
)
# Available environment variables:
# DISCORD_WEBHOOK_URL - Webhook URL (required)
# WEBHOOK_MAX_RETRIES - Maximum retry attempts (default: 3)
# WEBHOOK_BASE_DELAY_MS - Base delay in ms (default: 1000)
# WEBHOOK_MAX_DELAY_MS - Maximum delay in ms (default: 60000)
# WEBHOOK_ENABLE_JITTER - Enable jitter (default: true)Parse Webhook URL
Extract webhook ID and token from a URL:
import { parseWebhookUrl } from '@teever/ez-hook-effect'
const result = await Effect.runPromise(
parseWebhookUrl('https://discord.com/api/webhooks/123456789/token')
)
// result: { id: '123456789', token: 'token' }Webhook Operations
Beyond sending messages, the library supports full webhook CRUD:
import { Effect, Layer, pipe } from 'effect'
import { getWebhook, modifyWebhook, deleteWebhook, validateWebhook, WebhookService, WebhookServiceLive, makeConfigLayer, HttpClientLive } from '@teever/ez-hook-effect'
const AppLayer = WebhookServiceLive.pipe(
Layer.provide(makeConfigLayer(WEBHOOK_URL)),
Layer.provide(HttpClientLive)
)
const program = Effect.gen(function* () {
const service = yield* WebhookService
// Check if webhook is valid and accessible
const isValid = yield* service.validateWebhook()
// Get webhook information
const info = yield* service.getWebhook()
// Modify webhook settings
const modified = yield* service.modifyWebhook({
name: 'New Name',
})
// Delete the webhook
const deleted = yield* service.deleteWebhook()
})
Effect.runPromise(Effect.provide(program, AppLayer))Testing Utilities
import { Effect, Layer, pipe } from 'effect'
import { makeTestHttpClient, WebhookServiceLive, makeConfigLayer } from '@teever/ez-hook-effect'
const TestHttpClient = makeTestHttpClient((_req) =>
Effect.succeed({
status: 204,
statusText: 'No Content',
headers: {},
body: null,
text: '',
})
)
const AppLayer = WebhookServiceLive.pipe(
Layer.provide(makeConfigLayer('https://discord.com/api/webhooks/123/abc')),
Layer.provide(TestHttpClient)
)Development
bun run lint # Biome lint + format
bun run typecheck # TypeScript type checkingTesting
The library includes comprehensive tests for all features:
bun test # Run all tests
bun test:watch # Watch mode
bun test:coverage # Coverage reportBuilding
bun run build # Build the library
bun run build:standalone # Create standalone executableExamples
Check out the examples/ directory:
01-basic-webhook.ts- Send a simple message02-rich-embeds.ts- Build embeds with fields, colors, metadata03-error-handling.ts- Validation errors and recovery patterns04-multiple-embeds.ts- Add several embeds to one webhook05-service-layer.ts- Dependency injection with layers and CRUD operations
License
MIT
