Skip to content

Latest commit

 

History

History
114 lines (95 loc) · 3.23 KB

File metadata and controls

114 lines (95 loc) · 3.23 KB
title Define Contracts Upfront with Schema
id define-contracts-with-schema
skillLevel intermediate
applicationPatternId domain-modeling
summary Use Schema to define the types for your data models and function signatures before writing the implementation, creating clear, type-safe contracts.
tags
schema
design
architecture
type-safety
contract-first
data-modeling
rule
description
Define contracts upfront with schema.
related
parse-with-schema-decode
define-config-schema
author Sandro Maglione
lessonOrder 3

Define Contracts Upfront with Schema

Guideline

Before writing implementation logic, define the shape of your data models and function signatures using Effect/Schema.

Rationale

This "schema-first" approach separates the "what" (the data shape) from the "how" (the implementation). It provides a single source of truth for both compile-time static types and runtime validation.

Good Example

import { Schema, Effect, Data } from "effect";

// Define User schema and type
const UserSchema = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
});

type User = Schema.Schema.Type<typeof UserSchema>;

// Define error type
class UserNotFound extends Data.TaggedError("UserNotFound")<{
  readonly id: number;
}> {}

// Create database service implementation
export class Database extends Effect.Service<Database>()("Database", {
  sync: () => ({
    getUser: (id: number) =>
      id === 1
        ? Effect.succeed({ id: 1, name: "John" })
        : Effect.fail(new UserNotFound({ id })),
  }),
}) {}

// Create a program that demonstrates schema and error handling
const program = Effect.gen(function* () {
  const db = yield* Database;

  // Try to get an existing user
  yield* Effect.logInfo("Looking up user 1...");
  const user1 = yield* db.getUser(1);
  yield* Effect.logInfo(`Found user: ${JSON.stringify(user1)}`);

  // Try to get a non-existent user
  yield* Effect.logInfo("\nLooking up user 999...");
  yield* Effect.logInfo("Attempting to get user 999...");
  yield* Effect.gen(function* () {
    const user = yield* db.getUser(999);
    yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);
  }).pipe(
    Effect.catchAll((error) => {
      if (error instanceof UserNotFound) {
        return Effect.logInfo(`Error: User with id ${error.id} not found`);
      }
      return Effect.logInfo(`Unexpected error: ${error}`);
    })
  );

  // Try to decode invalid data
  yield* Effect.logInfo("\nTrying to decode invalid user data...");
  const invalidUser = { id: "not-a-number", name: 123 } as any;
  yield* Effect.gen(function* () {
    const user = yield* Schema.decode(UserSchema)(invalidUser);
    yield* Effect.logInfo(`Decoded user: ${JSON.stringify(user)}`);
  }).pipe(
    Effect.catchAll((error) =>
      Effect.logInfo(`Validation failed:\n${JSON.stringify(error, null, 2)}`)
    )
  );
});

// Run the program
Effect.runPromise(Effect.provide(program, Database.Default));

Explanation:
Defining schemas upfront clarifies your contracts and ensures both type safety and runtime validation.

Anti-Pattern

Defining logic with implicit any types first and adding validation later as an afterthought. This leads to brittle code that lacks a clear contract.