Help us improve
Share bugs, ideas, or general feedback.
From effect-ts
Guides implementation of Effect's request batching, caching, and deduplication using RequestResolvers to optimize API calls and solve N+1 query problems in TypeScript.
npx claudepluginhub andrueandersoncs/claude-skill-effect-ts --plugin effect-tsHow this skill is triggered — by the user, by Claude, or both
Slash command
/effect-ts:batching-cachingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Effect provides automatic optimization for API calls:
Manages Effect concurrency with fibers for forking, joining, interruption, parallel Effect.all execution, and race conditions in Effect apps.
Provides expert guidance on Effect-TS patterns including services, layers, error handling, service composition, and refactoring code with 'effect' imports. Covers Effect + Next.js integration.
Batch and cache data fetches to eliminate N+1 queries in GraphQL resolvers using DataLoader. Covers request-scoped loaders, batch function ordering, context attachment, and cache priming.
Share bugs, ideas, or general feedback.
Effect provides automatic optimization for API calls:
This solves the N+1 query problem automatically.
const program = Effect.gen(function* () {
const todos = yield* getTodos();
const owners = yield* Effect.forEach(todos, (todo) => getUserById(todo.ownerId), { concurrency: "unbounded" });
});
Effect's batching transforms this into optimized batch calls.
import { Request } from "effect";
// Define request shape
interface GetUserById extends Request.Request<User, UserNotFound> {
readonly _tag: "GetUserById";
readonly id: number;
}
// Create tagged constructor
const GetUserById = Request.tagged<GetUserById>("GetUserById");
import { RequestResolver, Effect } from "effect";
// Batched resolver - handles multiple requests at once
const GetUserByIdResolver = RequestResolver.makeBatched((requests: ReadonlyArray<GetUserById>) =>
Effect.gen(function* () {
// Single batch API call
const users = yield* Effect.tryPromise(() =>
fetch("/api/users/batch", {
method: "POST",
body: JSON.stringify({ ids: requests.map((r) => r.id) }),
}).then((res) => res.json()),
);
// Complete each request with its result
yield* Effect.forEach(requests, (request, index) => Request.completeEffect(request, Effect.succeed(users[index])));
}),
);
const getUserById = (id: number) => Effect.request(GetUserById({ id }), GetUserByIdResolver);
const program = Effect.gen(function* () {
const todos = yield* getTodos();
const owners = yield* Effect.forEach(todos, (todo) => getUserById(todo.ownerId), { concurrency: "unbounded" });
});
const SingleUserResolver = RequestResolver.fromEffect((request: GetUserById) =>
Effect.tryPromise(() => fetch(`/api/users/${request.id}`).then((r) => r.json())),
);
const BatchedUserResolver = RequestResolver.makeBatched((requests: ReadonlyArray<GetUserById>) =>
// Handle all requests in one call
batchFetch(requests),
);
const UserResolverWithContext = RequestResolver.makeBatched((requests: ReadonlyArray<GetUserById>) =>
Effect.gen(function* () {
// Access services from context
const httpClient = yield* HttpClient;
const logger = yield* Logger;
yield* logger.info(`Batching ${requests.length} user requests`);
return yield* httpClient.post("/api/users/batch", {
ids: requests.map((r) => r.id),
});
}),
);
// Provide context to resolver
const ContextualResolver = UserResolverWithContext.pipe(RequestResolver.provideContext(context));
import { Effect } from "effect";
const fetchConfig = Effect.promise(() => fetch("/api/config").then((r) => r.json()));
const cachedConfig = yield * Effect.cached(fetchConfig);
const config1 = yield * cachedConfig;
const config2 = yield * cachedConfig;
const cachedUser = yield * Effect.cachedWithTTL(fetchCurrentUser, "5 minutes");
const user1 = yield * cachedUser;
yield * Effect.sleep("6 minutes");
const user2 = yield * cachedUser;
const [cachedUser, invalidate] = yield * Effect.cachedInvalidateWithTTL(fetchCurrentUser, "5 minutes");
const user = yield * cachedUser;
yield * invalidate;
const freshUser = yield * cachedUser;
For more control, use the Cache service:
import { Cache } from "effect";
const program = Effect.gen(function* () {
const cache = yield* Cache.make({
capacity: 100,
timeToLive: "10 minutes",
lookup: (userId: string) => fetchUser(userId),
});
const user1 = yield* cache.get("user-1");
const user2 = yield* cache.get("user-1");
const isCached = yield* cache.contains("user-1");
yield* cache.invalidate("user-1");
const stats = yield* cache.cacheStats;
});
Requests are automatically cached within a query context:
const program = Effect.gen(function* () {
const user1 = yield* getUserById(1);
const user2 = yield* getUserById(1);
const user3 = yield* getUserById(2);
});
const noCaching = getUserById(1).pipe(Effect.withRequestCaching(false));
const customCache =
yield *
Request.makeCache({
capacity: 1000,
timeToLive: "30 minutes",
});
const program = getUserById(1).pipe(Effect.withRequestCache(customCache));
const noBatching = program.pipe(Effect.withRequestBatching(false));
import { Effect, Request, RequestResolver, Schema } from "effect";
// Error types
class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", { id: Schema.Number }) {}
// Request type
interface GetUserById extends Request.Request<User, UserNotFound> {
readonly _tag: "GetUserById";
readonly id: number;
}
const GetUserById = Request.tagged<GetUserById>("GetUserById");
// Batched resolver
const UserResolver = RequestResolver.makeBatched((requests: ReadonlyArray<GetUserById>) =>
Effect.gen(function* () {
const ids = requests.map((r) => r.id);
const response = yield* Effect.tryPromise({
try: () =>
fetch("/api/users/batch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ids }),
}).then((r) => r.json() as Promise<User[]>),
catch: () => new Error("Batch fetch failed"),
});
yield* Effect.forEach(requests, (request, index) => {
const user = response[index];
return user
? Request.completeEffect(request, Effect.succeed(user))
: Request.completeEffect(request, Effect.fail(new UserNotFound({ id: request.id })));
});
}),
);
// Query function
const getUserById = (id: number) => Effect.request(GetUserById({ id }), UserResolver);
// Usage - automatically batched
const program = Effect.gen(function* () {
const todos = yield* getTodos();
const owners = yield* Effect.forEach(todos, (todo) => getUserById(todo.ownerId), { concurrency: "unbounded" });
return owners;
});
For comprehensive batching and caching documentation, consult ${CLAUDE_PLUGIN_ROOT}/references/llms-full.txt.
Search for these sections: