Skill: Backend Service

SkillDev tools

Adding 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.

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 implementations
  • src/services/*.spec.ts — colocated tests
  • src/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() and resetDependencies()
  • .spec.ts colocated and passing at 90%+ coverage
  • afterEach calls resetDependencies() plus any cache invalidation
  • No mock.module() usage
  • File paths validated with isPathSafe()
  • make fix passes clean

Signals

GitHub stars
142
Forks
32
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
backend-service
Source
github.com/exelearning/exelearning