AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Write Service Code

skill-kennguyen887-agent-foundation-write-service-code · by kennguyen887

Use when writing code inside a feature — a handler, query, service, event, or integration test. Control flow, async (Promise.all), query performance (N+1, upsert, joins, indexes), events/SQS, logging, decimal/date libs. Language-agnostic with TS/NestJS examples.

No reviews yet
0 installs
14 views
0.0% view→install

Install

$ agentstack add skill-kennguyen887-agent-foundation-write-service-code

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-kennguyen887-agent-foundation-write-service-code)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
24d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Write Service Code? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

When to use

Reach for this when you're writing the body of a handler/service/query/event/test and want the team's implementation conventions — readability, async, nullability, SQL performance, messaging, logging, and test shape. For where files go and what they're named, use the companion skill [structure-a-backend-service](./structure-a-backend-service.md).

Each rule: portable principle▸ Example (TS/NestJS) (neutral listing domain; `/` = rename) → ▸ Other stacks. Some rules restate global policy; the global rule stays authoritative (see Related).

Steps

1. Control flow — return early, no manual loops

  • Return (or throw) early; don't nest. Handle the invalid/empty case first and bail, so the

happy path stays unindented. ``ts if (minPrice == null && maxPrice == null) return; // guard if (!items?.length) return null; // ...happy path, never more than ~2 levels deep ``

  • Transform collections with pipeline functions, not for/while. map/filter/reduce/

find/some/every/flatMap. Keep callbacks pure (no mutating the source). ``ts const coverIds = media.filter((m) => m.type === MediaType.COVER).map((m) => m.id); const labelByCode = categories.reduce((acc, c) => { acc[c.code] = c.label; return acc; }, {} as Record); ``

  • Imperative iteration only for: side effects (forEach / for...of to mutate external state,

dispatch, log) and sequential async that must be ordered (for...of with await, e.g. a retry loop). Never forEach with an async callback. ▸ Other stacks: comprehensions / streams / map equivalents; the principle (declarative transform, imperative only for effects & ordered async) is universal. (Matches global Code Style — Iteration & Collections.)

2. Parallelize independent async with Promise.all

Independent awaits run together; only await in sequence when one result feeds the next.

const [detail, owner] = await Promise.all([getDetail(id), getOwner(id)]);       // independent → parallel
const files = await Promise.all(media.map(async (m) => ({ name: m.name, data: await toBase64(m) })));

A sequential await inside a map/loop for independent work is the anti-pattern — it serializes needlessly. ▸ Other stacks: asyncio.gather, goroutines + errgroup, CompletableFuture.allOf.

3. Prefer null over undefined

Use an explicit nullable type for "absent" values; reserve undefined for "not provided". This keeps DB-nullable columns, DTO fields, and return types consistent.

@Column({ nullable: true }) publishedAt!: Nullable;         // entity field
async getFile(req: FileReq): Promise> { ... }   // return type

Other stacks: Optional (Java), *T/sql.NullString (Go), T | None (Python). The point: one agreed "absent" value, not a mix of null and undefined. (TS note: ==/!= against null intentionally matches both null and undefined.)

API response defaults: an absent array is [] (never null/undefined); an absent scalar/object is null (never undefined). undefined disappears from JSON and breaks clients that expect the key. ▸ Other stacks: same — empty collection for lists, explicit null for the rest.

4. Put private helpers below the public API

A class/file reads top-down: public methods first, then private helpers underneath. Helper names are verb-led + single responsibility (fetchOwnerProfile, shouldTriggerReview, buildPayload).

@Injectable()
export class ListingService {
  async createListing(dto: CreateListingDto) { /* ...delegates to helpers... */ }
  async getListing(id: string) { ... }
  // ── private helpers below ──────────────────────────────
  private async fetchOwnerProfile(listing: Listing) { ... }
  private shouldTriggerReview(listing: Listing): boolean { ... }
}

Keep methods to one screenful; extract a focused private method when one covers 3+ concerns. (Matches global Code Style — Function Size & Density.)

5. Querying & performance

The repository/query builder is where most performance is won or lost. Apply all of these:

  • Select only the columns you need — never implicit SELECT *.

``ts .select(['listing.id', 'listing.status', 'listing.price', 'photo']) // explicit, includes joined alias ``

  • Push every filter into the query (status/type/date/soft-delete) and batch multi-value

filters with IN (:...ids) — never fetch broadly then .filter() in code, and never query per-item in a loop (that's the N+1). (Matches global Database & Migration Rules.) ``ts .where('listing.isDeleted = false') .andWhere('listing.status IN (:...statuses)', { statuses }) // one query, not one-per-status ``

  • Load relations in the query, not lazily per row, to avoid N+1: leftJoinAndSelect for

optional relations you need in the result, innerJoin for a required relation you only filter on (no select), Brackets for grouped OR. ``ts .leftJoinAndSelect('listing.photos', 'photo', 'photo.isDeleted = false') // eager, optional .innerJoin('listing.owner', 'owner') // required, filter-only ``

  • One round-trip when you can — fetch rows + total together (getManyAndCount) instead of two

queries; combine related work rather than looping queries. ``ts const [rows, total] = await qb.take(take).skip(skip).orderBy(column, direction).getManyAndCount(); ``

  • Write-or-update in one statement with upsert instead of select-then-insert/update; it also

lets you batch insert + update + soft-delete together. ``ts await this.photoRepo.upsert( [...newPhotos, ...unused.map((p) => ({ ...p, isDeleted: true }))], ['id'], // conflict key ); ``

  • Index for your filters + sort, and order deterministically. Add a composite @Index matching

the common WHERE + ORDER BY; drive orderBy from a typed sort-map constant (not free-text), so only indexed columns are sortable. ``ts @Index('listing_status_createdAt_idx', ['status', 'createdAt']) // sort map: enum → { column, direction } ; query: const { column, direction } = listingSortMap[sortBy]; ``

  • Money & dates go through standard libraries, never ad-hoc math. Precise/decimal numeric work

uses a decimal library (floats drift — 0.1 + 0.2 !== 0.3); date/time uses a date library (parsing, timezones, formatting). ▸ Example: a Decimal type + a decimal(p,s) column transformer for money; a day/date lib for every date operation. ▸ Other stacks: your ecosystem's decimal + datetime libs — never hand-roll currency math or timezone arithmetic.

Other stacks: the same SQL discipline applies through any ORM/query layer (Django select_related/prefetch_related, JPA fetch joins, sqlc/sqlx); upsert = INSERT ... ON CONFLICT / MERGE.

6. Events & async messaging (SQS/pub-sub)

  • One event per file under the feature's events/, holding the event class (extends a shared

BaseEvent) and its handler; a barrel exports the handler list for batch registration.

  • Emit from the use-case via the bus; never publish externally inline. The command/query handler

publishes a domain event; an event handler does the outbound I/O. ```ts // in a command handler this.eventBus.publish(new ListingStatusChangedEvent(listings));

// events/listing-status-changed.event.ts @EventsHandler(ListingStatusChangedEvent) export class ListingStatusChangedEventHandler implements IEventHandler { async handle(event: ListingStatusChangedEvent): Promise { await this.publisher.dispatch({ event: TOPIC.LISTINGSTATUSCHANGED, payload: event.data.map((l) => ({ listingId: l.id, status: l.status })), // mapped subset, NOT the raw entity }); } } ```

  • Outbound payloads are an explicit mapped subset (id + the few fields subscribers need), never

the whole entity — that's a contract you don't want leaking internal columns.

  • Inbound consumers are wired from config + named constants, not hard-coded URLs/strings. Queue

URLs are built from a centralized config block; topic/queue/pattern names are constants. ``ts SqsModule.registerAsync({ useFactory: (config: ConfigService) => { const { region, accountNumber, sqsEndpoint } = config.get(configEvents); const consumers = [getQueueName(EVENT_PATTERNS.listing.published)].map((name) => ({ name, queueUrl: ${sqsEndpoint}/${accountNumber}/${name}, region, })); return { consumers }; }, inject: [ConfigService], }); // @SqsMessageHandler(queueName, false) async handleMessage(message: Message) { ... } ``

  • In a queue (SQS) consumer, never throw — log and return. An unhandled throw returns the

message to the queue, where it's re-consumed until it expires — a poison message that can loop forever. ▸ Example: the handler extends a shared AbstractEventHandler; on failure it calls logger.error(...) then return, so the message is acked/deleted while the error still reaches alarms. Handle partial-batch failures explicitly (report which records failed). ▸ Other stacks: ack/commit the message and route the failure to a dead-letter queue — don't let an exception trigger blind redelivery.

Other stacks: an in-process event bus or outbox emits; a separate consumer/worker handles the queue. Principle: use-case → event → handler → broker, payloads are explicit, names are config.

7. Logging

  • Structured metadata, not string concatenation. A short message string + a context object.

``ts this.logger.info(Response from for "${path}", { data: maskPii(data), payload: maskPii(payload) }); this.logger.error("Can't publish listing", { listingId, resError: (error as Error).message }); ``

  • Mask PII/secrets before logging (identity numbers, tokens) — pass values through a masker.
  • Levels: info = normal milestones (external call made, status transition), warn = recovered/

fallback paths, error = exceptions & failed external calls (include the error message + ids), debug = detailed tracing. Log every critical branch, fallback, and missing-config path (global Feature Flags & Observability). Request logging + a global exception filter are wired once centrally, not per handler. ▸ Other stacks: any structured logger (zap, structlog, SLF4J + MDC) — key-value context, not interpolated strings.

8. Writing a test (integration layer)

These are integration tests — they boot the app and hit the real transport/DB boundary. For the isolated unit-test layer (mocked deps, no DB — fast tests for handlers/services/DTOs) see [write-unit-tests](./write-unit-tests.md); a repo may run one layer or both (two jest projects). See [structure-a-backend-service](./structure-a-backend-service.md) step 8 for the harness. Per spec:

describe('ListingCmdController', () => {
  let client: ClientProxy, app: INestApplication, dataSource: DataSource;
  let factoryCtx: FactoryContext, listingFactory: ListingFactory, listingRepo: Repository;

  beforeAll(() => {
    ({ client, app, dataSource } = global.testContext);   // app booted once in the shared setup
    listingRepo = dataSource.getRepository(Listing);
  });
  beforeEach(() => {
    factoryCtx = new FactoryContext(dataSource);
    listingFactory = new ListingFactory(factoryCtx);       // fresh factory per test
  });
  afterEach(async () => {
    jest.restoreAllMocks();
    await listingRepo.delete({});
    await factoryCtx.destroy();                            // clean state between tests
  });

  it('publishes the listing and persists it', async () => {
    // Arrange — seed via factory, mock only externals
    const listing = await listingFactory.build({ status: ListingStatus.DRAFT });
    jest.spyOn(SearchIndexService.prototype, 'index').mockResolvedValue({ ok: true });

    // Act — exactly one action under test, blank line before & after
    const { success } = await lastValueFrom(client.send(PATTERN.listing.publish, msg({ id: listing.id })));

    // Assert — response AND persisted state, with specific expected values
    expect(success).toBe(true);
    const updated = await listingRepo.findOneOrFail({ where: { id: listing.id } });
    expect(updated.status).toBe(ListingStatus.PUBLISHED);   // not toBeTruthy()
  });

  it('fails for an unknown listing', async () => {
    const { success } = await lastValueFrom(client.send(PATTERN.listing.publish, msg({ id: v4() })));
    expect(success).toBe(false);                            // cover the error path too
  });
});
  • Structure every test Arrange → Act → Assert, blank line around the Act, exactly one Act

(one action under test) — the comments above show the shape.

  • Seed with factories + a faker lib for inputs (realistic random names/emails/numbers/ids),

never hand-rolled literals; build only the fields the test asserts on. Clean up the rows created in this test afterward (and only those) so parallel specs don't collide.

  • Mock only what crosses the process boundary (external services via spyOn(...prototype),

outbound HTTP via an intercept lib). Use the real DB; assert the persisted row, not just the reply.

  • Assert specific expected values, not just truthinessexpect(id).toBe(realId), not

toBeTruthy(). Common matchers: toBe (string/number), toMatchObject (part of an object), toEqual (whole object/array), arrayContaining (array members).

  • Group branch variations with it.each instead of copy-pasting near-identical tests.
  • Cover the success path + the error/edge path; assert outbound side effects with spy-called-with.
  • Keep specs small and run coverage. One spec per API when it has many cases (≤ ~300 lines);

group several small APIs into one spec otherwise. Run the coverage report over what you wrote.

  • Don't write isolated unit tests that bypass the boundary — if it can't be reached through the

boundary, skip it (global HTTP-layer testing rule).

9. Robustness — transactions, event-handler safety, external clients

  • Multi-repo writes go in one transaction; broadcast a compensating event before re-throwing. Wrap

related mutations in dataSource.transaction(em => …) and pass em to each repo op for atomicity. In an event-driven flow, if the transaction fails after side effects were signalled, emit a compensating event (e.g. OrderCanceled releasing held inventory) before throwing, so downstream can undo: ``ts await dataSource.transaction(async (em) => { const order = await saveOrder(em, dto); try { await reserveInventory(em, order); await debitWallet(em, order); return order; } catch (e) { await events.publish(new OrderCanceledEvent(order)); throw e; } // compensate, then roll back }); ``

  • In-process event handlers must not crash the bus. A CQRS @EventsHandler is fire-and-forget;

one failing handler (missing template, third-party down) shouldn't break sibling handlers. Wrap handle so it logs + swallows instead of throwing — e.g. a shared @CatchException() decorator (log + return). (This is the in-process twin of the SQS "don't throw" rule in §6.)

  • External/microservice clients own their lifecycle. A ClientProxy (TCP/microservice client)

closes on OnApplicationShutdown (no zombie connections on deploy); retry config (attempts + delay) is env-driven (exponential backoff + jitter, capped); route every call through one base send() wrapper that centralizes retries + error mapping (don't scatter clientProxy.send across services). ```ts @Injectable() export class WalletClient implements OnApplicationShutdown { constructor(@Inject(walletMs) private readonly proxy: ClientProxy) {} onApplicationShutdown() { this.proxy.close(); } send(pattern: string, data: I) { return this.base.sendAsync(this.proxy, pattern, data); } }

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.