Effect-TS patterns for building APIs. Use when working with APIs in Effect-TS applications.
This skill provides 13 curated Effect-TS patterns for building apis. Use this skill when working on tasks related to:
Rule: Use Http.router.get to associate a URL path with a specific response Effect.
Good Example:
This example defines two separate GET routes, one for the root path (/) and one for /hello. We create an empty router and add each route to it. The resulting app is then served. The router automatically handles sending a 404 Not Found response for any path that doesn't match.
import { Data, Effect } from "effect";
// Define response types
interface RouteResponse {
readonly status: number;
readonly body: string;
}
// Define error types
class RouteNotFoundError extends Data.TaggedError("RouteNotFoundError")<{
readonly path: string;
}> {}
class RouteHandlerError extends Data.TaggedError("RouteHandlerError")<{
readonly path: string;
readonly error: string;
}> {}
// Define route service
class RouteService extends Effect.Service<RouteService>()("RouteService", {
sync: () => {
// Create instance methods
const handleRoute = (
path: string
): Effect.Effect<RouteResponse, RouteNotFoundError | RouteHandlerError> =>
Effect.gen(function* () {
yield* Effect.logInfo(`Processing request for path: ${path}`);
try {
switch (path) {
case "/":
const home = "Welcome to the home page!";
yield* Effect.logInfo(`Serving home page`);
return { status: 200, body: home };
case "/hello":
const hello = "Hello, Effect!";
yield* Effect.logInfo(`Serving hello page`);
return { status: 200, body: hello };
default:
yield* Effect.logWarning(`Route not found: ${path}`);
return yield* Effect.fail(new RouteNotFoundError({ path }));
}
} catch (e) {
const error = e instanceof Error ? e.message : String(e);
yield* Effect.logError(`Error handling route ${path}: ${error}`);
return yield* Effect.fail(new RouteHandlerError({ path, error }));
}
});
// Return service implementation
return {
handleRoute,
// Simulate GET request
simulateGet: (
path: string
): Effect.Effect<RouteResponse, RouteNotFoundError | RouteHandlerError> =>
Effect.gen(function* () {
yield* Effect.logInfo(`GET ${path}`);
const response = yield* handleRoute(path);
yield* Effect.logInfo(`Response: ${JSON.stringify(response)}`);
return response;
}),
};
},
}) {}
// Create program with proper error handling
const program = Effect.gen(function* () {
const router = yield* RouteService;
yield* Effect.logInfo("=== Starting Route Tests ===");
// Test different routes
for (const path of ["/", "/hello", "/other", "/error"]) {
yield* Effect.logInfo(`\n--- Testing ${path} ---`);
const result = yield* router.simulateGet(path).pipe(
Effect.catchTags({
RouteNotFoundError: (error) =>
Effect.gen(function* () {
const response = { status: 404, body: `Not Found: ${error.path}` };
yield* Effect.logWarning(`${response.status} ${response.body}`);
return response;
}),
RouteHandlerError: (error) =>
Effect.gen(function* () {
const response = {
status: 500,
body: `Internal Error: ${error.error}`,
};
yield* Effect.logError(`${response.status} ${response.body}`);
return response;
}),
})
);
yield* Effect.logInfo(`Final Response: ${JSON.stringify(result)}`);
}
yield* Effect.logInfo("\n=== Route Tests Complete ===");
});
// Run the program
Effect.runPromise(Effect.provide(program, RouteService.Default));
Anti-Pattern:
The anti-pattern is to create a single, monolithic handler that uses conditional logic to inspect the request URL. This imperative approach is difficult to maintain and scale.
import { Effect } from "effect";
import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
// A single app that manually checks the URL
const app = Http.request.ServerRequest.pipe(
Effect.flatMap((req) => {
if (req.url === "/") {
return Effect.succeed(Http.response.text("Welcome to the home page!"));
} else if (req.url === "/hello") {
return Effect.succeed(Http.response.text("Hello, Effect!"));
} else {
return Effect.succeed(Http.response.empty({ status: 404 }));
}
})
);
const program = Http.server
.serve(app)
.pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
NodeRuntime.runMain(program);
This manual routing logic is verbose, error-prone (a typo in a string breaks the route), and mixes the "what" (the response) with the "where" (the routing). It doesn't scale to handle different HTTP methods, path parameters, or middleware gracefully. The Http.router is designed to solve all of these problems elegantly.
Rationale:
To handle specific URL paths, create individual routes using Http.router functions (like Http.router.get) and combine them into a single Http.App.
A real application needs to respond differently to different URLs. The Http.router provides a declarative, type-safe, and composable way to manage this routing logic. Instead of a single handler with complex conditional logic, you define many small, focused handlers and assign them to specific paths and HTTP methods.
This approach has several advantages:
userRoutes router and a productRoutes router) and merge them.Effect, meaning it has full access to dependency injection, structured concurrency, and integrated error handling, just like any other part of an Effect application.Rule: Use Http.response.json to automatically serialize data structures into a JSON response.
Good Example:
This example defines a route that fetches a user object and returns it as a JSON response. The Http.response.json function handles all the necessary serialization and header configuration.
import { Effect, Context, Duration, Layer } from "effect";
import { NodeContext, NodeHttpServer } from "@effect/platform-node";
import { createServer } from "node:http";
const PORT = 3459; // Changed port to avoid conflicts
// Define HTTP Server service
class JsonServer extends Effect.Service<JsonServer>()("JsonServer", {
sync: () => ({
handleRequest: () =>
Effect.succeed({
status: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: "Hello, JSON!",
timestamp: new Date().toISOString(),
}),
}),
}),
}) {}
// Create and run the server
const program = Effect.gen(function* () {
const jsonServer = yield* JsonServer;
// Create and start HTTP server
const server = createServer((req, res) => {
const requestHandler = Effect.gen(function* () {
try {
const response = yield* jsonServer.handleRequest();
res.writeHead(response.status, response.headers);
res.end(response.body);
// Log the response for demonstration
yield* Effect.logInfo(`Sent JSON response: ${response.body}`);
} catch (error: any) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Internal Server Error" }));
yield* Effect.logError(`Request error: ${error.message}`);
}
});
Effect.runPromise(requestHandler);
});
// Start server with error handling
yield* Effect.async<void, Error>((resume) => {
server.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EADDRINUSE") {
resume(Effect.fail(new Error(`Port ${PORT} is already in use`)));
} else {
resume(Effect.fail(error));
}
});
server.listen(PORT, () => {
resume(Effect.succeed(void 0));
});
});
yield* Effect.logInfo(`Server running at http://localhost:${PORT}`);
yield* Effect.logInfo("Try: curl http://localhost:3459");
// Run for a short time to demonstrate
yield* Effect.sleep(Duration.seconds(3));
// Shutdown gracefully
yield* Effect.sync(() => server.close());
yield* Effect.logInfo("Server shutdown complete");
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logError(`Server error: ${error.message}`);
return error;
})
),
// Merge layers and provide them in a single call to ensure proper lifecycle management
Effect.provide(Layer.merge(JsonServer.Default, NodeContext.layer))
);
// Run the program
// Use Effect.runFork for server applications that shouldn't resolve the promise
Effect.runPromise(
program.pipe(
// Ensure the Effect has no remaining context requirements for runPromise
Effect.map(() => undefined)
)
);
Anti-Pattern:
The anti-pattern is to manually serialize the data to a string and set the headers yourself. This is verbose and introduces opportunities for error.
import { Effect } from "effect";
import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
const getUserRoute = Http.router.get(
"/users/1",
Effect.succeed({ id: 1, name: "Paul", team: "Effect" }).pipe(
Effect.flatMap((user) => {
// Manually serialize the object to a JSON string.
const jsonString = JSON.stringify(user);
// Create a text response with the string.
const response = Http.response.text(jsonString);
// Manually set the Content-Type header.
return Effect.succeed(
Http.response.setHeader(
response,
"Content-Type",
"application/json; charset=utf-8"
)
);
})
)
);
const app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute));
const program = Http.server
.serve(app)
.pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
NodeRuntime.runMain(program);
This manual approach is unnecessarily complex. It forces you to remember to perform both the serialization and the header configuration. If you forget the setHeader call, many clients will fail to parse the response correctly. The Http.response.json helper eliminates this entire class of potential bugs.
Rationale:
To return a JavaScript object or value as a JSON response, use the Http.response.json(data) constructor.
APIs predominantly communicate using JSON. The Http module provides a dedicated Http.response.json helper to make this as simple and robust as possible. Manually constructing a JSON response involves serializing the data and setting the correct HTTP headers, which is tedious and error-prone.
Using Http.response.json is superior because:
JSON.stringify operation for you, including handling potential circular references or other serialization errors.Content-Type: application/json; charset=utf-8 header. This is critical for clients to correctly interpret the response body. Forgetting this header is a common source of bugs in manually constructed APIs.Http.response object that works seamlessly with all other parts of the Effect Http module.Rule: Define routes with colon-prefixed parameters (e.g., /users/:id) and access their values within the handler.
Good Example:
This example defines a route that captures a userId. The handler for this route accesses the parsed parameters and uses the userId to construct a personalized greeting. The router automatically makes the parameters available to the handler.
import { Data, Effect } from "effect";
// Define tagged error for invalid paths
interface InvalidPathErrorSchema {
readonly _tag: "InvalidPathError";
readonly path: string;
}
const makeInvalidPathError = (path: string): InvalidPathErrorSchema => ({
_tag: "InvalidPathError",
path,
});
// Define service interface
interface PathOps {
readonly extractUserId: (
path: string
) => Effect.Effect<string, InvalidPathErrorSchema>;
readonly greetUser: (userId: string) => Effect.Effect<string>;
}
// Create service
class PathService extends Effect.Service<PathService>()("PathService", {
sync: () => ({
extractUserId: (path: string) =>
Effect.gen(function* () {
yield* Effect.logInfo(
`Attempting to extract user ID from path: ${path}`
);
const match = path.match(/\/users\/([^/]+)/);
if (!match) {
yield* Effect.logInfo(`No user ID found in path: ${path}`);
return yield* Effect.fail(makeInvalidPathError(path));
}
const userId = match[1];
yield* Effect.logInfo(`Successfully extracted user ID: ${userId}`);
return userId;
}),
greetUser: (userId: string) =>
Effect.gen(function* () {
const greeting = `Hello, user ${userId}!`;
yield* Effect.logInfo(greeting);
return greeting;
}),
}),
}) {}
// Compose the functions with proper error handling
const processPath = (
path: string
): Effect.Effect<string, InvalidPathErrorSchema, PathService> =>
Effect.gen(function* () {
const pathService = yield* PathService;
yield* Effect.logInfo(`Processing path: ${path}`);
const userId = yield* pathService.extractUserId(path);
return yield* pathService.greetUser(userId);
});
// Run examples with proper error handling
const program = Effect.gen(function* () {
// Test valid paths
yield* Effect.logInfo("=== Testing valid paths ===");
const result1 = yield* processPath("/users/123");
yield* Effect.logInfo(`Result 1: ${result1}`);
const result2 = yield* processPath("/users/abc");
yield* Effect.logInfo(`Result 2: ${result2}`);
// Test invalid path
yield* Effect.logInfo("\n=== Testing invalid path ===");
const result3 = yield* processPath("/invalid/path").pipe(
Effect.catchTag("InvalidPathError", (error) =>
Effect.succeed(`Error: Invalid path ${error.path}`)
)
);
yield* Effect.logInfo(result3);
});
Effect.runPromise(Effect.provide(program, PathService.Default));
Anti-Pattern:
The anti-pattern is to manually parse the URL string inside the handler. This approach is brittle, imperative, and mixes concerns.
import { Effect } from "effect";
import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
// This route matches any sub-path of /users/, forcing manual parsing.
const app = Http.router.get(
"/users/*", // Using a wildcard
Http.request.ServerRequest.pipe(
Effect.flatMap((req) => {
// Manually split the URL to find the ID.
const parts = req.url.split("/"); // e.g., ['', 'users', '123']
if (parts.length === 3 && parts[2]) {
const userId = parts[2];
return Http.response.text(`Hello, user ${userId}!`);
}
// Manual handling for missing ID.
return Http.response.empty({ status: 404 });
})
)
);
const program = Http.server
.serve(app)
.pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
NodeRuntime.runMain(program);
This manual method is highly discouraged. It's fragile—a change in the base path or an extra slash could break the logic (parts[2]). It's also not declarative; the intent is hidden inside imperative code. The router's built-in parameter handling is safer, clearer, and the correct approach.
Rationale:
To capture dynamic parts of a URL, define your route path with a colon-prefixed placeholder (e.g., /users/:userId) and access the parsed parameters within your handler Effect.
APIs often need to operate on specific resources identified by a unique key in the URL, such as /products/123 or /orders/abc. The Http.router provides a clean, declarative way to handle these dynamic paths without resorting to manual string parsing.
By defining parameters directly in the path string, you gain several benefits:
Http module, allowing you to build complex and well-structured APIs.Rule: Use Http.server.serve with a platform-specific layer to run an HTTP application.
Good Example:
This example creates a minimal server that responds to all requests with "Hello, World!". The application logic is a simple Effect that returns an Http.response. We use NodeRuntime.runMain to execute the server effect, which is the standard way to launch a long-running application.
import { Effect, Duration } from "effect";
import * as http from "http";
// Create HTTP server service
class HttpServer extends Effect.Service<HttpServer>()("HttpServer", {
sync: () => ({
start: () =>
Effect.gen(function* () {
const server = http.createServer(
(req: http.IncomingMessage, res: http.ServerResponse) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello, World!");
}
);
// Add cleanup finalizer
yield* Effect.addFinalizer(() =>
Effect.gen(function* () {
yield* Effect.sync(() => server.close());
yield* Effect.logInfo("Server shut down");
})
);
// Start server with timeout
yield* Effect.async<void, Error>((resume) => {
server.on("error", (error) => resume(Effect.fail(error)));
server.listen(3456, "localhost", () => {
resume(Effect.succeed(void 0));
});
}).pipe(
Effect.timeout(Duration.seconds(5)),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logError(`Failed to start server: ${error}`);
return yield* Effect.fail(error);
})
)
);
yield* Effect.logInfo("Server running at http://localhost:3456/");
// Run for a short duration to demonstrate the server is working
yield* Effect.sleep(Duration.seconds(3));
yield* Effect.logInfo("Server demonstration complete");
}),
}),
}) {}
// Create program with proper error handling
const program = Effect.gen(function* () {
const server = yield* HttpServer;
yield* Effect.logInfo("Starting HTTP server...");
yield* server.start();
}).pipe(
Effect.scoped // Ensure server is cleaned up properly
);
// Run the server with proper error handling
const programWithErrorHandling = Effect.provide(
program,
HttpServer.Default
).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logError(`Program failed: ${error}`);
return yield* Effect.fail(error);
})
)
);
Effect.runPromise(programWithErrorHandling).catch(() => {
process.exit(1);
});
/*
To test:
1. Server will timeout after 5 seconds if it can't start
2. Server runs on port 3456 to avoid conflicts
3. Proper cleanup on shutdown
4. Demonstrates server lifecycle: start -> run -> shutdown
*/
Anti-Pattern:
The common anti-pattern is to use the raw Node.js http module directly, outside of the Effect runtime. This approach creates a disconnect between your application logic and the server's lifecycle.
import * as http from "http";
// Manually create a server using the Node.js built-in module.
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello, World!");
});
// Manually start the server and log the port.
const port = 3000;
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
This imperative approach is discouraged when building an Effect application because it forfeits all the benefits of the ecosystem. It runs outside of Effect's structured concurrency, cannot be managed by its resource-safe Scope, does not integrate with Layer for dependency injection, and requires manual error handling, making it less robust and much harder to compose with other effectful logic.
Rationale:
To create and run a web server, define your application as an Http.App and execute it using Http.server.serve, providing a platform-specific layer like NodeHttpServer.layer.
In Effect, an HTTP server is not just a side effect; it's a managed, effectful process. The @effect/platform package provides a platform-agnostic API for defining HTTP applications, while packages like @effect/platform-node provide the concrete implementation.
The core function Http.server.serve(app) takes your application logic and returns an Effect that, when run, starts the server. This Effect is designed to run indefinitely, only terminating if the server crashes or is gracefully shut down.
This approach provides several key benefits:
Layer, use Config for configuration, and integrate with Logger.Http.App interface, your application logic remains portable across different JavaScript runtimes (Node.js, Bun, Deno) by simply swapping out the platform layer.Rule: Use a rate limiter service to enforce request quotas per client.
Good Example:
import { Effect, Context, Layer, Ref, HashMap, Data, Duration } from "effect"
import { HttpServerRequest, HttpServerResponse } from "@effect/platform"
// ============================================
// 1. Define rate limit types
// ============================================
interface RateLimitConfig {
readonly maxRequests: number
readonly windowMs: number
}
interface RateLimitState {
readonly count: number
readonly resetAt: number
}
class RateLimitExceededError extends Data.TaggedError("RateLimitExceededError")<{
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add PaulJPhilp/effect-patterns-building-apis下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer