Skip to content

Latest commit

 

History

History
148 lines (127 loc) · 3.59 KB

File metadata and controls

148 lines (127 loc) · 3.59 KB
id schema-web-standards-url
title URL Validation
category web-standards-validation
skillLevel beginner
tags
schema
validation
web-standards
url
branded-types
http
lessonOrder 30
rule
description
URL Validation using Schema.
summary You accept URLs from users—as links, webhooks, or API endpoints. A string type doesn't validate that it's actually a valid URL. Users submit malformed URLs like "htp://example" or "not a url". You...

Problem

You accept URLs from users—as links, webhooks, or API endpoints. A string type doesn't validate that it's actually a valid URL. Users submit malformed URLs like "htp://example" or "not a url". You need to validate URLs at runtime, ensure they use allowed protocols, and create a distinct type so URLs can't be confused with plain strings.

Solution

import { Schema, Effect } from "effect"

// 1. Define branded URL type
const HttpUrl = Schema.String.pipe(
  Schema.trimmed(),
  Schema.filter((s) => {
    try {
      const url = new URL(s)
      // Only allow http/https
      return (
        url.protocol === "http:" || url.protocol === "https:"
      )
    } catch {
      return false
    }
  }).pipe(
    Schema.annotations({
      description: "Valid HTTP/HTTPS URL",
    })
  ),
  Schema.brand("HttpUrl")
)

type HttpUrl = typeof HttpUrl.Type

// 2. Alternative: Allow multiple protocols
const WebUrl = Schema.String.pipe(
  Schema.trimmed(),
  Schema.filter((s) => {
    try {
      new URL(s)
      return true
    } catch {
      return false
    }
  }).pipe(
    Schema.annotations({
      description: "Valid URL (any protocol)",
    })
  ),
  Schema.brand("WebUrl")
)

type WebUrl = typeof WebUrl.Type

// 3. Use in webhook configuration
const WebhookConfig = Schema.Struct({
  name: Schema.String,
  url: HttpUrl,
  events: Schema.Array(
    Schema.Literal("user.created", "user.deleted")
  ),
  retryLimit: Schema.Number.pipe(
    Schema.between(0, 10)
  ).pipe(Schema.withDefault(3)),
})

type WebhookConfig = typeof WebhookConfig.Type

// 4. Validate and extract URL parts
const configureWebhook = (input: unknown) =>
  Effect.gen(function* () {
    const config = yield* Schema.decodeUnknown(
      WebhookConfig
    )(input).pipe(
      Effect.mapError((error) => ({
        _tag: "ConfigError" as const,
        message: `Invalid webhook config: ${error.message}`,
      }))
    )

    // Extract hostname from validated URL
    const urlObj = new URL(config.url)
    console.log(
      `Webhook for ${urlObj.hostname} registered`
    )

    return config
  })

// 5. Usage
const webhookInput = {
  name: "User Events",
  url: "https://webhook.example.com/users",
  events: ["user.created"],
}

Effect.runPromise(configureWebhook(webhookInput))
  .then((config) => {
    console.log(`✅ Webhook: ${config.name}`)
  })
  .catch((error) =>
    console.error(`Error: ${error.message}`)
  )

Why This Works

Concept Explanation
new URL(s) Native browser/Node.js API—validates RFC 3986
Schema.filter Custom validation logic beyond regex
Protocol check Only allow safe protocols (http/https)
Schema.brand Creates nominal type—HttpUrl ≠ string
URL extraction Can safely call new URL() again since already validated
Type safety Typed callbacks receive validated URLs

When to Use

  • Webhook registration
  • API endpoint configuration
  • Link collection and storage
  • Redirect targets
  • External resource URLs
  • API client endpoints

Related Patterns