| id |
schema-basic-async |
| title |
Basic Async Validation with Schema.filterEffect |
| category |
async-validation |
| skillLevel |
beginner |
| tags |
schema |
async-validation |
effects |
filtering |
validation-rules |
|
| lessonOrder |
6 |
| rule |
| description |
Basic Async Validation with Schema.filterEffect. |
|
| summary |
You have sync validation rules (format, length) handled by schema. But some rules require async operations: checking a username isn't taken, verifying a discount code is valid, calling an external... |
You have sync validation rules (format, length) handled by schema. But some rules require async operations: checking a username isn't taken, verifying a discount code is valid, calling an external API. You need schemas that perform async validation without leaving the schema layer.
import { Schema, Effect } from "effect"
// ============================================
// 1. Basic async filter
// ============================================
const ValidUsername = Schema.String.pipe(
Schema.minLength(3),
Schema.maxLength(20),
Schema.pattern(/^[a-zA-Z0-9_-]+$/),
Schema.filterEffect((username) =>
Effect.gen(function* () {
// Simulate async check
yield* Effect.sleep(Duration.millis(100))
// Check if username is taken
const isTaken = ["admin", "root", "system"].includes(username)
if (isTaken) {
return yield* Effect.fail(
new Error(`Username "${username}" is already taken`)
)
}
return username
})
)
)
// ============================================
// 2. Email with async verification
// ============================================
const VerifiedEmail = Schema.String.pipe(
Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
Schema.filterEffect((email) =>
Effect.gen(function* () {
// Simulate async email verification
yield* Effect.sleep(Duration.millis(200))
// Check against a denylist
const bannedDomains = ["temp-mail.com", "10minutemail.com"]
const [, domain] = email.split("@")
if (bannedDomains.includes(domain)) {
return yield* Effect.fail(
new Error(`Email domain "${domain}" not allowed`)
)
}
return email
})
)
)
// ============================================
// 3. Discount code validation
// ============================================
const ValidDiscountCode = Schema.String.pipe(
Schema.toUpperCase(),
Schema.filterEffect((code) =>
Effect.gen(function* () {
// Simulate database lookup
yield* Effect.sleep(Duration.millis(150))
const validCodes: Record<string, number> = {
WELCOME10: 10,
SAVE20: 20,
VIP50: 50,
}
if (!validCodes[code]) {
return yield* Effect.fail(
new Error(`Discount code "${code}" is invalid`)
)
}
return code
})
)
)
// ============================================
// 4. SignUp form with async validation
// ============================================
const SignUpForm = Schema.Struct({
username: ValidUsername,
email: VerifiedEmail,
password: Schema.String.pipe(Schema.minLength(8)),
discountCode: Schema.optional(ValidDiscountCode),
})
type SignUpForm = typeof SignUpForm.Type
// ============================================
// 5. Processing async validation
// ============================================
const validateSignup = (
data: unknown
): Effect.Effect<SignUpForm, Error> =>
Effect.gen(function* () {
console.log("Validating signup form...")
const result = yield* Effect.tryPromise({
try: () => Schema.decodeUnknown(SignUpForm)(data),
catch: (error) => {
const msg = error instanceof Error ? error.message : String(error)
return new Error(`Validation failed: ${msg}`)
},
})
console.log("✅ All validations passed")
return result
})
// ============================================
// 6. Application logic
// ============================================
const appLogic = Effect.gen(function* () {
console.log("=== Basic Async Validation ===\n")
console.log("1. Valid signup:\n")
const validData = {
username: "alice_dev",
email: "alice@example.com",
password: "SecurePassword123",
discountCode: "WELCOME10",
}
const result1 = yield* validateSignup(validData)
console.log(`✓ User: ${result1.username}`)
console.log(`✓ Email: ${result1.email}`)
console.log("\n2. Invalid username (taken):\n")
const invalidUsername = {
username: "admin",
email: "admin@example.com",
password: "SecurePassword123",
}
const result2 = yield* validateSignup(invalidUsername).pipe(
Effect.either
)
if (result2._tag === "Left") {
console.log(`✗ Error: ${result2.left.message}`)
}
console.log("\n3. Invalid email domain:\n")
const invalidEmail = {
username: "bob_coder",
email: "bob@temp-mail.com",
password: "SecurePassword123",
}
const result3 = yield* validateSignup(invalidEmail).pipe(
Effect.either
)
if (result3._tag === "Left") {
console.log(`✗ Error: ${result3.left.message}`)
}
console.log("\n4. Invalid discount code:\n")
const invalidCode = {
username: "charlie",
email: "charlie@example.com",
password: "SecurePassword123",
discountCode: "INVALID99",
}
const result4 = yield* validateSignup(invalidCode).pipe(
Effect.either
)
if (result4._tag === "Left") {
console.log(`✗ Error: ${result4.left.message}`)
}
return result1
})
const { Duration } = require("effect")
// Run application
Effect.runPromise(appLogic)
.then(() => console.log("\n✅ Async validation complete"))
.catch((error) => console.error(`Error: ${error.message}`))
| Concept |
Explanation |
| filterEffect |
Run Effect during schema validation |
| Composable |
Chain sync validation with async filters |
| Error handling |
Async errors become validation errors |
| Type safe |
Output type same as input schema |
| Readable |
Validation logic near schema definition |
| Reusable |
Each filter is independently composable |
- Username availability checks
- Email verification
- Discount code validation
- API-based validation rules
- Database lookup during parse
- Rate limit checks
- License verification