Skill: Backend Service
SkillDev toolsAdding or modifying business logic services in src/services/ with dependency injection. Every change must ship with colocated `*.spec.ts` tests, keep patch coverage ≥ 90% on changed lines, and pass `make fix`, `make test-unit`, `make test-integration`, and `make test-e2e` before submission.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Skill: Backend Service skill
What this skill tells your AI
The instructions your AI receives, as published by exelearning/exelearning in .agents/skills/backend-service/SKILL.md and read by ahel’s review.
Parent: AGENTS.md | Related: backend-route, database-migration
When to Use
Adding or modifying business logic services in src/services/.
Key Files
src/services/*.ts— service implementationssrc/services/*.spec.ts— colocated testssrc/services/file-helper.ts— file path resolution (getFilesDir,getProjectAssetsDir, etc.)src/exceptions/— custom error types (HttpException,TranslatableException)src/db/queries/*.ts— database query functions (consumed by services)
DI Pattern (Required)
// Service file
interface MyServiceDeps {
queries: { findById: typeof findById };
}
const defaultDeps: MyServiceDeps = { queries: { findById } };
let deps = defaultDeps;
export function configure(newDeps: Partial<MyServiceDeps>): void {
deps = { ...defaultDeps, ...newDeps };
}
export function resetDependencies(): void { deps = defaultDeps; }
Test Pattern
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
afterEach(() => {
resetDependencies();
// Also clean up any side effects (caches, singletons)
});
it('returns data when found', async () => {
configure({ queries: { findById: async () => ({ id: 1, name: 'test' }) } });
const result = await myService.findById({} as any, 1);
expect(result).toEqual({ id: 1, name: 'test' });
});
Use {} as any for the db parameter when behavior is fully injected via DI — no real DB needed.
Caching Pattern
When adding caching, include TTL and an invalidate export for tests:
interface CacheEntry { value: boolean; expiresAt: number; }
let cache: CacheEntry | null = null;
const CACHE_TTL_MS = 5000;
export function invalidateCache(): void { cache = null; }
Test that the cache works by asserting call counts on the injected dependency.
Commands
bun test src/services/my-service.spec.ts # Run service tests
make fix # Lint
Gotchas
mock.module()is forbidden — causes test pollution in Bun. Always use the DI pattern above.isPathSafe()for all user-derived paths — path traversal is a critical security vulnerability.- Lazy directory creation — never create directories eagerly. Create on-demand when writing files.
- Cleanup ordering on shutdown — kill processes before deleting their files. On Windows, open file handles cause EBUSY errors. Example: PR #1578.
- Dual source of truth — if server and static builder need the same config, extract a shared function. Do not copy-paste. Example: PR #1564.
- Extract complex inline logic — when fixing a calculation bug, extract it into a named pure function with edge-case handling, then test it directly. Example: PR #1546.
Done When
- Service exports
configure()andresetDependencies() -
.spec.tscolocated and passing at 90%+ coverage -
afterEachcallsresetDependencies()plus any cache invalidation - No
mock.module()usage - File paths validated with
isPathSafe() -
make fixpasses clean
Signals
- GitHub stars
- 142
- Forks
- 32
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
backend-service- Source
- github.com/exelearning/exelearning