From ts-backend-dev
Optimize TypeScript code that interacts with databases. Use this skill when the user wants to fix N+1 queries, add caching, improve transaction safety, prevent race conditions, simplify async flows, or generally speed up a TypeScript backend. Triggers on phrases like "optimize", "slow query", "N+1", "race condition", "caching", "performance", "bottleneck", or when the user points at TypeScript code that reads or writes to a database and asks for improvements.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ts-backend-dev:ts-db-perfThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Optimize: $ARGUMENTS
Optimize: $ARGUMENTS
The classic trap: fetching a list, then querying per item in a loop.
// N+1: one query for the list, one per item
const orders = await db.order.findMany();
for (const order of orders) {
order.customer = await db.customer.findUnique({ where: { id: order.customerId } });
}
// Resolved: single query with join/include
const orders = await db.order.findMany({
include: { customer: true },
});
If the ORM doesn't support include, use a WHERE id IN (...) or a JOIN.
// Over-fetching
const users = await db.user.findMany();
// Tight select
const users = await db.user.findMany({
select: { id: true, email: true },
});
Applies to raw SQL too -- avoid SELECT * when you only need a few columns.
Always paginate list endpoints. Cursor-based for large/real-time datasets, offset-based for simple cases.
// Offset-based
const [data, total] = await Promise.all([
db.user.findMany({ skip: (page - 1) * limit, take: limit }),
db.user.count(),
]);
// Cursor-based (no count query, stable under writes)
const items = await db.message.findMany({
take: limit,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: "desc" },
});
Cache where data is read-heavy and stale reads are tolerable.
async function getExchangeRate(from: string, to: string): Promise<number> {
const key = `rate:${from}:${to}`;
const cached = await cache.get(key);
if (cached !== null) return Number(cached);
const rate = await fetchRateFromAPI(from, to);
await cache.set(key, String(rate), { ttl: 60 }); // 60s TTL
return rate;
}
For repository-level caching, wrap the lookup:
async findById(id: string): Promise<User | null> {
const cached = await cache.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await db.user.findUnique({ where: { id } });
if (user) await cache.set(`user:${id}`, JSON.stringify(user), { ttl: 300 });
return user;
}
Invalidate on writes. Prefer short TTLs over complex invalidation logic.
// Sequential writes outside a transaction -- slow and non-atomic
for (const item of items) {
await db.item.update({ where: { id: item.id }, data: item });
}
// Batched in a transaction -- fast and atomic
await db.$transaction(
items.map((item) =>
db.item.update({ where: { id: item.id }, data: item })
)
);
For large batches, chunk to avoid exceeding connection limits or statement size:
const CHUNK = 100;
for (let i = 0; i < items.length; i += CHUNK) {
await db.$transaction(
items.slice(i, i + CHUNK).map((item) =>
db.item.update({ where: { id: item.id }, data: item })
)
);
}
Critical for balance updates, inventory, counters -- anything that reads-then-writes.
Optimistic locking (version column):
const result = await db.account.update({
where: { id: accountId, version: currentVersion },
data: { balance: newBalance, version: { increment: 1 } },
});
if (!result) throw new ConflictError("Concurrent modification");
Atomic updates (no read needed):
await db.account.update({
where: { id: accountId },
data: { balance: { increment: amount } },
});
Distributed lock (Redis/Valkey, for cross-service coordination):
const lock = await acquireLock(`balance:${accountId}`, 30_000);
try {
await db.$transaction(async (tx) => {
await tx.$executeRaw`
UPDATE account SET balance = balance + ${amount} WHERE id = ${accountId}
`;
});
} finally {
await releaseLock(`balance:${accountId}`, lock);
}
Idempotency (webhooks, retries):
const key = `processed:${eventId}`;
if (await cache.get(key)) return { status: "already_processed" };
await cache.set(key, "processing", { ttl: 3600 });
// ... do work ...
await cache.set(key, "done", { ttl: 86_400 });
Independent lookups should run concurrently.
// Sequential -- 2 round-trips
const user = await db.user.findUnique({ where: { id } });
const settings = await db.settings.findUnique({ where: { userId: id } });
// Parallel -- 1 round-trip wall time
const [user, settings] = await Promise.all([
db.user.findUnique({ where: { id } }),
db.settings.findUnique({ where: { userId: id } }),
]);
Use Promise.allSettled when partial failures are acceptable.
Early returns over nesting:
// Nested
async function process(data: Input | null) {
if (data) {
if (data.valid) {
// actual logic
}
}
}
// Flat
async function process(data: Input | null) {
if (!data || !data.valid) return;
// actual logic
}
Extract helpers for repeated patterns:
function ensureFound<T>(value: T | null | undefined, label: string): T {
if (value == null) throw new NotFoundError(`${label} not found`);
return value;
}
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub widnyana/eyay-toolkits --plugin ts-backend-dev