EffectPatterns
Copilot instructions807 starsChanged 8 months agoMITView on GitHub
Effect-Patterns is a community-driven knowledge base of practical patterns for building robust applications with Effect-TS. The project includes: All services must use the modern Effect.Service pattern: Define tagged errors for type-safety: Build HTTP servers with @effect/platform: Place tests adjacent to implementation:
- Reads credentials
We split this copilot-instructions.md into the rules it gives an agent
10 rules. About 0 are common, 7 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 16
All services must use the modern Effect.Service pattern:
- Very specific: line 45
Use Layer-based DI with Layer.merge
- Distinctive: line 122
Start MCP server for AI assistance:
- Distinctive: line 126
Run server in dev mode:
- Distinctive: line 139
Place tests adjacent to implementation:
- Very specific: line 175
Use Effect.gen for sequential operations
- Distinctive: line 177
Follow TypeScript strict mode conventions
- Distinctive: line 193
Use type-safe configuration with Effect's Config service:
- Distinctive: line 268
Always run smoke tests:
- Distinctive: line 387
Use Schema for type-safe option validation:
≈ “Common” is an estimate: at least 1 in 5 of the 3,669 Copilot instructions files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Effect-Patterns AI Agent Instructions
Project Overview
Effect-Patterns is a community-driven knowledge base of practical patterns for building robust applications with Effect-TS. The project includes:
- A pattern server for serving API endpoints
- Documentation and examples of Effect-TS patterns
- Rules and guidelines for AI coding agents
- MCP server integration for context-aware coding assistance
Key Architectural Patterns
Service Pattern
All services must use the modern Effect.Service pattern:
export class UserService extends Effect.Service<UserService>()("UserService", {
// Enable static accessor methods
accessors: true,
// Define implementation with dependencies
effect: Effect.gen(function* () {
// Get dependencies
const logger = yield* LoggerService;
const db = yield* DatabaseService;
return {
getUser: (id: string) =>
Effect.gen(function* () {
yield* logger.log(`Fetching user ${id}`);
return yield* db.query(`SELECT * FROM users WHERE id = ${id}`);
}),
};
}),
// Declare dependencies
dependencies: [LoggerService.Default, DatabaseService.Default],
}) {}Dependency Injection
- Use Layer-based DI with
Layer.merge - Compose layers logically:
const mainLayer = Layer.merge(
DatabaseService.Default,
LoggerService.Default,
NodeContext.layer
);
const program = Effect.gen(function* () {
// Program logic
}).pipe(Effect.provide(mainLayer));Error Handling
Define tagged errors for type-safety:
export class ServiceError extends Data.TaggedError("ServiceError")<{
message: string;
cause?: unknown;
}> {}
// Usage
Effect.gen(function* () {
try {
// Operation
} catch (cause) {
yield* Effect.fail(
new ServiceError({
message: "Operation failed",
cause,
})
);
}
});HTTP Server
Build HTTP servers with @effect/platform:
const app = HttpRouter.empty.pipe(
HttpRouter.get("/health", () =>
Effect.succeed({ status: "ok" }).pipe(
Effect.flatMap(HttpServerResponse.json)
)
)
);
const server = NodeHttpServer.layer(() => require("node:http").createServer(), {
port: 3001,
});
const serverLayer = HttpServer.serve(app);Project Structure
/
├── api/ # API endpoint implementations
├── app/ # Next.js web application
├── content/ # Pattern documentation content
├── docs/ # Project documentation
├── packages/ # Shared packages
├── rules/ # AI coding rules
├── scripts/ # Build/deployment scripts
├── server/ # Pattern server implementation
└── services/ # Shared servicesDevelopment Workflow
- Start MCP server for AI assistance:
npx @effect/mcp-server --layer src/layers.ts:AppLayer - Run server in dev mode:
bun run mcp:dev - Run tests:
bun test
Testing Guidelines
Test Structure
Place tests adjacent to implementation:
services/
my-service/
service.ts
types.ts
errors.ts
__tests__/
service.test.tsTest Pattern
describe("MyService", () => {
const testLayer = Layer.provide(MyService.Default, NodeContext.layer);
it("should perform operation", () =>
Effect.gen(function* () {
const service = yield* MyService;
const result = yield* service.myMethod("test");
expect(result).toBe("expected");
}).pipe(Effect.provide(testLayer)));
it("should handle errors", () =>
Effect.gen(function* () {
const service = yield* MyService;
const error = yield* service.riskyMethod().pipe(Effect.flip);
expect(error).toBeInstanceOf(ServiceError);
}).pipe(Effect.provide(testLayer)));
});Common Patterns
- Use
Effect.genfor sequential operations - Handle data validation with
Schema.struct() - Follow TypeScript strict mode conventions
Use direct imports:
// ✅ Preferred import { Effect, Layer } from "effect"; import { FileSystem } from "@effect/platform"; // ❌ Avoid import * as Effect from "effect";
Configuration & Deployment
Configuration Pattern
Use type-safe configuration with Effect's Config service:
// Define config schema
const ServerConfig = Config.nested("SERVER")(
Config.all({
host: Config.string("HOST"),
port: Config.number("PORT"),
})
);
// Create config service
class AppConfig extends Effect.Service<AppConfig>()("AppConfig", {
effect: Effect.gen(function* () {
const config = yield* ServerConfig;
return {
getConfig: () => Effect.succeed(config),
};
}),
}) {}
// Use in application
const program = Effect.gen(function* () {
const config = yield* AppConfig;
const { host, port } = yield* config.getConfig();
});Environment Setup
Required environment variables:
# API Security
PATTERN_API_KEY=your-secret-api-key-here
# OpenTelemetry Configuration
OTLP_ENDPOINT=http://localhost:4318/v1/traces
OTLP_HEADERS=
SERVICE_NAME=effect-patterns-mcp-server
# Server Configuration
NODE_ENV=development
PORT=3000Deployment Process
Build and test:
bun run build bun testDeploy to staging:
cd services/mcp-server vercel --env stagingVerify deployment:
bun run smoke-test https://your-deployment-url.vercel.app- Deploy to production:
vercel --prod
Post-Deployment Verification
Always run smoke tests:
# Health check
curl https://your-deployment-url.vercel.app/api/health
# Authenticated endpoint
curl -H "x-api-key: $PATTERN_API_KEY" \
https://your-deployment-url.vercel.app/api/patternsCLI Usage
The project includes a CLI tool (ep) for managing patterns and AI rules:
Installation
# Install dependencies
bun install
# Link CLI globally
bun link
# Verify installation
ep --help
# Check version
ep --versionCommon Commands
Pattern Management
# Create new pattern
ep pattern new # Interactive wizard
ep pattern new --title "My Pattern" # With options
ep pattern new --skill-level intermediate # Specify level
# Validate patterns
ep admin validate # Basic validation
ep admin validate --verbose # Detailed output
ep admin validate --fix # Auto-fix issues
# Run pattern tests
ep admin test # Run all tests
ep admin test --verbose # Detailed output
ep admin test --pattern "error-handling" # Test specific patternAI Tool Integration
# List supported AI tools
ep install list
# Install rules into an AI tool
ep install add --tool cursor # All rules
ep install add --tool cursor --skill-level beginner # By skill level
ep install add --tool cursor --use-case error-handling # By use case
ep install add --tool agents --server-url http://localhost:3002 # Custom server
# Multiple filters
ep install add --tool cursor \
--skill-level intermediate \
--use-case error-handling,testingRepository Management
# Run complete pipeline
ep admin pipeline # Full validation
ep admin pipeline --quick # Skip long-running checks
# Generate documentation
ep admin generate # Generate README
ep admin generate --verify # Verify only
ep admin rules generate # Generate AI rules
ep admin rules generate --tool cursor # For specific tool
# Release management
ep admin release preview # Preview next version
ep admin release create # Create release
ep admin release create --dry-run # Test release processCLI Architecture
The CLI is built with @effect/cli following Effect-TS patterns:
Error Types
Define tagged errors for type-safe error handling:
// CLI-specific errors
export class ToolError extends Data.TaggedError("ToolError")<{
tool: string;
reason: string;
}> {}
export class ValidationError extends Data.TaggedError("ValidationError")<{
field: string;
message: string;
value?: unknown;
}> {}
export class ApiError extends Data.TaggedError("ApiError")<{
endpoint: string;
statusCode: number;
body?: unknown;
}> {}Input Validation
Use Schema for type-safe option validation:
// --- Schema Composition and Transformation ---
// Base types with refinements
const SkillLevel = Schema.literal("beginner", "intermediate", "advanced");
const UseCase = Schema.array(Schema.string);
const Version = Schema.string.pipe(
Schema.filter((v) => /^\d+\.\d+\.\d+$/.test(v)),
Schema.transformer((v: string) => ({
major: parseInt(v.split(".")[0]),
minor: parseInt(v.split(".")[1]),
patch: parseInt(v.split(".")[2]),
}))
);
// Composable schema components
const TimestampFields = Schema.struct({
createdAt: Schema.number,
updatedAt: Schema.number,
});
const MetadataFields = Schema.struct({
author: Schema.string,
tags: Schema.array(Schema.string),
draft: Schema.boolean,
});
// Custom validation transforms
const NonEmptyString = Schema.string.pipe(
Schema.filter((s) => s.trim().length > 0, {
message: "String must not be empty",
}),
Schema.transformer((s) => s.trim())
);
const HttpUrl = Schema.string.pipe(
Schema.filter(
(url) => {
try {
new URL(url);
return url.startsWith("http");
} catch {
return false;
}
},
{
message: "Must be a valid HTTP URL",
}
)
);
const EmailList = Schema.array(Schema.string).pipe(
Schema.filter(
(emails) =>
emails.every((email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)),
{ message: "All entries must be valid email addresses" }
)
);
// Composable validation pipeline
const withValidation = <I, O>(schema: Schema.Schema<I, O>) =>
Schema.struct({
data: schema,
validation: Schema.struct({
enabled: Schema.boolean,
rules: Schema.array(Schema.string),
severity: Schema.literal("error", "warn", "info"),
}),
});
// Complex schema composition with transforms
const PatternSchema = Schema.struct({
// Core fields with validation
id: NonEmptyString,
title: Schema.string.pipe(
Schema.filter((t) => t.length >= 3 && t.length <= 100),
Schema.transformer((t) => t.trim())
),
description: NonEmptyString,
skillLevel: SkillLevel,
useCase: Schema.array(NonEmptyString),
// Content validation
content: Schema.string.pipe(
Schema.filter((c) => c.includes("## Good Example")),
Schema.transformer((content) => ({
raw: content,
// Use the shared utility to split content into sections by markdown headings
// (handles CRLF, multiple heading levels, and trims results).
sections: splitSections(content),
examples: content.match(/\`\`\`[^\n]*([\s\S]*?)\`\`\`/g) ?? [],
}))
),
// Links and references
links: Schema.array(HttpUrl),
relatedPatterns: Schema.array(Schema.string),
contributors: Schema.array(
Schema.struct({
name: NonEmptyString,
email: Schema.string.pipe(
Schema.filter((e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))
),
})
),
// Metadata composition
...TimestampFields.fields,
...MetadataFields.fields,
// Validation rules
validation: Schema.struct({
lintRules: Schema.array(Schema.string),
testCoverage: Schema.number.pipe(Schema.filter((n) => n >= 0 && n <= 100)),
reviewers: EmailList,
}),
}).pipe(
// Add computed fields
Schema.transformer((pattern) => ({
...pattern,
slug: pattern.title.toLowerCase().replace(/\s+/g, "-"),
readingTime: Math.ceil(pattern.content.raw.split(/\s+/).length / 200),
}))
);
// Command option schemas with cross-field validation
const AddOptions = Schema.struct({
tool: Schema.string,
skillLevel: Schema.optional(SkillLevel),
useCase: Schema.optional(UseCase),
serverUrl: Schema.optional(
Schema.string.pipe(Schema.filter((url) => url.startsWith("http")))
),
});
const ReleaseOptions = Schema.struct({
version: Version,
dryRun: Schema.optional(Schema.boolean),
force: Schema.optional(Schema.boolean),
}).pipe(
// Cross-field validation
Schema.filter((opts) => !(opts.force && opts.dryRun), {
message: "Cannot use both force and dry-run",
})
);
// --- Validation Helpers ---
// Basic option validation
const validateOptions = (raw: unknown) =>
Effect.gen(function* () {
const result = yield* Schema.decode(AddOptions)(raw).pipe(
Effect.mapError(
(errors) =>
new ValidationError({
field: "options",
message: "Invalid options provided",
value: errors,
})
)
);
// Business rule validation
if (result.skillLevel && result.useCase) {
yield* Effect.fail(
new ValidationError({
field: "options",
message: "Cannot specify both skillLevel and useCase",
})
);
}
return result;
});
// Pattern content validation with custom rules
const validatePattern = (pattern: unknown) =>
Effect.gen(function* () {
// Schema validation
const result = yield* Schema.decode(PatternSchema)(pattern);
// Content structure validation
if (!result.content.includes("## Rationale")) {
yield* Effect.fail(
new ValidationError({
field: "content",
message: "Missing Rationale section",
})
);
}
// Code example validation
const hasTypeScript = yield* validateCodeExamples(result.content);
if (!hasTypeScript) {
yield* Effect.fail(
new ValidationError({
field: "content",
message: "Missing TypeScript example",
})
);
}
// Cross-reference validation
yield* validateReferences(result);
return result;
}).pipe(
Effect.tap(() => Effect.log("Pattern validation successful")),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logError(`Pattern validation failed: ${error.message}`);
return Effect.fail(error);
})
)
);
// Configuration validation with defaults and constraints
const validateConfig = (config: unknown) =>
Effect.gen(function* () {
// Define complex nested schema
const ConfigSchema = Schema.struct({
server: Schema.struct({
url: Schema.string.pipe(
Schema.filter((url) => url.startsWith("http")),
Schema.description("Server URL must start with http/https")
),
port: Schema.number.pipe(
Schema.filter((p) => p >= 1000 && p <= 65535),
Schema.description("Port must be between 1000-65535")
),
timeout: Schema.number.pipe(
Schema.filter((t) => t >= 0),
Schema.description("Timeout must be non-negative")
),
}),
api: Schema.struct({
key: Schema.string.pipe(
Schema.pattern(/^[A-Za-z0-9_-]{32}$/),
Schema.description("API key must be 32 characters [A-Za-z0-9_-]")
),
version: Schema.string.pipe(
Schema.pattern(/^v\d+$/),
Schema.description("API version must match pattern v1, v2, etc")
),
}),
logging: Schema.struct({
level: Schema.literal("debug", "info", "warn", "error"),
format: Schema.literal("json", "text"),
destination: Schema.union(
Schema.literal("stdout"),
Schema.literal("file"),
Schema.struct({ path: Schema.string })
),
}),
features: Schema.record(
Schema.string,
Schema.union(
Schema.boolean,
Schema.struct({
enabled: Schema.boolean,
config: Schema.record(Schema.string, Schema.unknown),
})
)
),
}).pipe(
// Set defaults for optional fields
Schema.withDefaults({
server: {
timeout: 5000,
port: 3000,
},
logging: {
level: "info",
format: "json",
destination: "stdout",
},
})
);
return yield* Schema.decode(ConfigSchema)(config);
});
// --- Validation Testing Patterns ---
describe("Pattern Validation", () => {
// Setup test data
const validPattern = {
id: "error-handling",
title: "Effect Error Handling",
description: "Best practices for handling errors in Effect",
skillLevel: "intermediate",
useCase: ["error-handling"],
content: "# Error Handling\n\n## Good Example\n\n```ts\n// code\n```",
links: ["https://effect.website"],
contributors: [{ name: "John", email: "john@effect.website" }],
createdAt: Date.now(),
updatedAt: Date.now(),
author: "Team Effect",
tags: ["error", "patterns"],
draft: false,
validation: {
lintRules: ["no-any"],
testCoverage: 100,
reviewers: ["reviewer@effect.website"],
},
};
describe("Schema Validation", () => {
it("should validate valid pattern", () =>
Effect.gen(function* () {
const pattern = yield* validatePattern(validPattern);
// Test core fields
expect(pattern.id).toBe("error-handling");
expect(pattern.skillLevel).toBe("intermediate");
// Test computed fields
expect(pattern.slug).toBe("effect-error-handling");
expect(typeof pattern.readingTime).toBe("number");
// Test content parsing
expect(pattern.content.examples).toHaveLength(1);
expect(pattern.content.sections).toHaveLength(2);
}));
it("should validate and transform content", () =>
Effect.gen(function* () {
const pattern = yield* validatePattern({
...validPattern,
title: " Spaces ",
content:
"## Section 1\n```ts\ncode1\n```\n## Section 2\n```ts\ncode2\n```",
});
expect(pattern.title).toBe("Spaces");
expect(pattern.content.sections).toHaveLength(2);
expect(pattern.content.examples).toHaveLength(2);
}));
});
describe("Custom Validation Rules", () => {
it("should validate required sections", () =>
Effect.gen(function* () {
const error = yield* validatePattern({
...validPattern,
content: "# Missing Good Example",
}).pipe(Effect.flip);
expect(error).toBeInstanceOf(ValidationError);
expect(error.message).toContain("Missing Good Example");
}));
it("should validate email formats", () =>
Effect.gen(function* () {
const error = yield* validatePattern({
...validPattern,
contributors: [{ name: "John", email: "invalid" }],
}).pipe(Effect.flip);
expect(error).toBeInstanceOf(ValidationError);
expect(error.message).toContain("email");
}));
});
describe("Validation Pipeline", () => {
const pipeline = withValidation(PatternSchema);
it("should apply validation rules", () =>
Effect.gen(function* () {
const result = yield* Schema.decode(pipeline)({
data: validPattern,
validation: {
enabled: true,
rules: ["format", "links"],
severity: "error",
},
});
expect(result.validation.enabled).toBe(true);
expect(result.validation.rules).toContain("format");
}));
});
describe("Error Reporting", () => {
it("should collect all validation errors", () =>
Effect.gen(function* () {
const error = yield* validatePattern({
...validPattern,
title: "", // Empty title
links: ["not-a-url"], // Invalid URL
validation: {
...validPattern.validation,
testCoverage: 101, // Invalid range
},
}).pipe(Effect.flip);
expect(error.errors).toHaveLength(3);
expect(error.errors).toContainEqual(
expect.objectContaining({
field: "title",
message: expect.stringContaining("empty"),
})
);
}));
it("should report validation errors with context", () =>
Effect.gen(function* () {
const result = yield* validatePattern({
...validPattern,
title: "",
}).pipe(
Effect.tapError((error) =>
Effect.sync(() => {
// Test error reporting format
expect(error.toJSON()).toEqual({
_tag: "ValidationError",
field: "title",
message: expect.any(String),
context: expect.any(Object),
});
})
),
Effect.flip
);
expect(result).toBeDefined();
}));
});
});Command Definition
Commands with validation and error handling:
const installAddCommand = Command.make("add", {
options: {
tool: Options.string("tool").pipe(
Options.withDescription("AI tool to configure")
),
skillLevel: Options.optional(Options.string("skill-level")),
useCase: Options.optional(Options.string("use-case")),
},
}).pipe(
Command.withDescription("Install Effect patterns rules into AI tools"),
Command.withHandler(({ options }) =>
Effect.gen(function* () {
// Validate options
const validOptions = yield* validateOptions(options);
// Verify tool is supported
const tool = yield* verifyTool(validOptions.tool);
// Fetch rules with error handling
const rules = yield* fetchRules(validOptions).pipe(
Effect.retry(Schedule.exponential(1000)),
Effect.catchTag("ApiError", (error) =>
Effect.gen(function* () {
yield* Effect.logError(
`API error: ${error.statusCode} - ${error.endpoint}`
);
return [];
})
)
);
// Install rules with rollback
yield* installRules(tool, rules).pipe(
Effect.catchTag("ToolError", (error) =>
Effect.gen(function* () {
yield* Effect.logError(`Failed to install rules: ${error.reason}`);
yield* cleanup(tool);
yield* Effect.fail(error);
})
)
);
yield* Effect.log(`Successfully installed rules for ${tool}`);
})
)
);
// Compose commands with proper error handling
const epCommand = Command.make("ep").pipe(
Command.withDescription("Effect Patterns CLI"),
Command.withSubcommands([
patternCommand,
installCommand.pipe(
Command.tapError((error) =>
Effect.gen(function* () {
yield* Effect.logError(`Command failed: ${error._tag}`);
yield* reportError(error);
})
)
),
adminCommand,
])
);Runtime Configuration
The CLI uses Effect's runtime system for dependency injection and error handling:
// Define runtime layers with error handling
const RuntimeLayers = Layer.mergeAll(
// HTTP client with retry logic
FetchHttpClient.layer.pipe(
Layer.provide(
RetryConfig.layer({
maxAttempts: 3,
backoff: "exponential",
})
)
),
// Node.js context
NodeContext.layer,
// CLI configuration with validation
ConfigService.layer.pipe(Layer.tap((config) => validateConfig(config))),
// Error reporting
ErrorReporter.layer
);
// Configure and run CLI with comprehensive error handling
const cli = Command.run(epCommand, {
name: "EffectPatterns CLI",
version: "0.4.0",
});
cli(process.argv).pipe(
Effect.provide(RuntimeLayers),
Effect.tapError((error) => logErrorDetails(error)),
Effect.catchTags({
// Handle specific error types
ToolError: (error) =>
Effect.gen(function* () {
yield* Effect.logError(`Tool error: ${error.tool} - ${error.reason}`);
return process.exit(1);
}),
ValidationError: (error) =>
Effect.gen(function* () {
yield* Effect.logError(
`Validation error: ${error.field} - ${error.message}`
);
return process.exit(1);
}),
ApiError: (error) =>
Effect.gen(function* () {
yield* Effect.logError(
`API error: ${error.statusCode} - ${error.endpoint}`
);
return process.exit(1);
}),
}),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logError(`Unexpected error: ${error.message}`);
yield* ErrorReporter.report(error);
return process.exit(1);
})
),
NodeRuntime.runMain
);Additional Resources
- See
docs/SERVICE_PATTERNS.mdfor detailed service patterns - See
docs/patterns-guide.mdfor core Effect-TS patterns - See
rules/directory for comprehensive coding rules - See
services/mcp-server/DEPLOYMENT.mdfor detailed deployment guide
Where files are headingNew Copilot instructions files have more rules about branches and pull requests, and fewer about code style.
Of the Copilot instructions files we read, the 2,297 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository3 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- effect-patterns-services-agentsSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

