Reference for writing code and tests in this repo. Rules are grouped by concern; apply every rule in the section that matches your task.
- The codebase is TypeScript, ESM. Each utility lives in
packages/<package-name>withsrcfor source andtestsfor tests, split intotests/unitandtests/e2e. - Not every workspace is a published package:
examples/snippets,layers, andpackages/testingshare dependencies and tooling with the rest of the monorepo but never ship to npm. - Import across packages by package name (
import { myFunction } from '@aws-lambda-powertools/commons'), with the dependency declared in the importing package'spackage.json. Relative paths stay within a package and always carry the.jsextension (from './utils.js'). - Utilities and types shared by two or more packages belong in
@aws-lambda-powertools/commons. - Sibling-package dependencies (including peerDependencies) are exact pins matching the current lockstep version (
"@aws-lambda-powertools/commons": "2.35.0"), no range specifiers.
Conventions inside each package:
- Errors live in a per-package
errors.ts; classes carry theErrorsuffix (IdempotencyKeyError) and extend the package's base error. - Constants live in a per-package
constants.tsasas constobjects, in place of TS enums. - Environment variables are read through the commons helpers —
getStringFromEnv,getNumberFromEnv,getBooleanFromEnvfrom@aws-lambda-powertools/commons/utils/env— rather thanprocess.env. - AWS SDK clients get the Powertools user agent at construction:
addUserAgentMiddleware(client, '<feature>')from commons. - New public entry points (packages build dual ESM/CJS) require entries in both the
exportsmap andtypesVersionsin the package'spackage.json.
Match the existing style of the surrounding code. House conventions:
constby default;letonly for reassignment.async/awaitfor asynchronous code.for...offor array iteration;Object.entries/Object.keys/Object.valuesfor object iteration.- Specific types first,
unknownwhen the type is genuinely unknown,anyonly when unavoidable. - Fix type errors at their source; a cast (
as Type, and especiallyas unknown as Type) is a last resort for genuine boundaries, paired with a comment explaining why it's safe. import type/export typefor type-only imports and exports.nullmeans the absence of a value;undefinedmeans a value not yet set or initialized. Pick the one that matches the meaning.- Descriptive, spelled-out names; readability over cleverness.
- To suppress a type error, use
@ts-expect-errorwith a reason (@ts-ignoreis a lint error). - Biome organizes imports on save/commit; leave import ordering to it.
Document functions, classes, and types with a single JSDoc block per symbol — public APIs always, internal ones too.
- Open with one active-voice sentence ending in a period (
Gets the user by ID.), then longer detail as needed. @param name - the name of the user: active voice, no type annotation. For option objects:@param {Object} optionsthen@param {string} options.name - the name of the user.@exampleto show usage.- Link symbols as
{@link functionName |functionName}— backticks mandatory for docs rendering. @internalmarks symbols outside the public API;@deprecatedalways carries a reason and an alternative.- Skip
@returnsand@throws.
Run from the repo root with -w <workspace>, or from the package directory:
npm run lintto check;npm run lint:fixto auto-fix (review its changes).npm run build:teststo type-check source and tests without emitting — CI compiles them separately from running them.npm run buildadditionally compiles the CommonJS target.
Tests use vitest and live in each package's tests/unit directory. Run with npm run test:unit -w packages/<name> (or npm run test:unit from the package directory). Write unit tests only — end-to-end tests happen when the user asks for them.
Package test scripts use vitest --run tests/unit for unit tests, vitest --run tests/types --typecheck for type tests, and vitest --run tests/e2e for end-to-end tests. Use echo 'Not Implemented' when a package does not provide a suite.
Coverage: CI enforces 100% coverage on src/** (types files excluded) via npm run test:unit:coverage — the plain test run skips coverage, so verify with the :coverage variant before finishing. Every new source line needs a covering test.
Structure:
- One behavior per test case. Use
it.eachto cover input variations of the same behavior. - Nest
describeblocks at most one level deep. - Name tests in active voice, no conditional:
it('throws an error when input is invalid'). - Delimit each test into phases with comments:
// Prepare(setup),// Act(execute),// Assess(verify). - Commit every test enabled:
it.onlyand.skipare lint errors.
Assertions:
- Verify results with
expectassertions on observable behavior. - Each assertion tests something meaningful; test behavior, and reach private methods only by extending the class in the test to expose them.
- Custom matchers from
packages/testing/src/setupEnv.ts:toHaveLogged,toHaveLoggedNth,toHaveEmittedEMFWith,toHaveEmittedNthEMFWith,toHaveEmittedMetricWith,toHaveEmittedNthMetricWith, plustoReceiveCommandWithfor mocked AWS SDK clients.
Environment:
- Keep test cases isolated from each other.
vi.mocksparingly, for external dependencies only.consoleis pre-mocked: use it freely in code under test and in assertions.- Set env vars with
vi.stubEnv()and restore withvi.unstubAllEnvs()inbeforeEach/afterEach; setupEnv pre-sets the standard Lambda env vars.
Invocation-scoped state (tests/unit/concurrency/): when AWS_LAMBDA_MAX_CONCURRENCY is set, Logger, Metrics, and Batch keep per-invocation state in the InvokeStore from @aws/lambda-invoke-store. Otherwise they keep one value shared by all invocations. Two things about that package matter for tests:
- The stores read
globalThis.awslambda.InvokeStore. It only exists after something callsInvokeStore.getInstanceAsync(). The Lambda runtime does that at startup; tests have to do it themselves. Until then, with the env var set, every invocation-scoped read or write throwsInvokeStore is not available. - The instance is created once and cached. Its kind depends on the env at that moment: with
AWS_LAMBDA_MAX_CONCURRENCYset it usesAsyncLocalStorageand isolates invocations; without it,run()gives no isolation. Later calls return the cached instance whatever the env.
So:
-
Call
InvokeStore._testing?.reset()inbeforeEachto drop the cached instance. setupEnv setsAWS_LAMBDA_BENCHMARK_MODE=1to expose_testing. -
Use
sequence()from@aws-lambda-powertools/testing-utilsto interleave two invocations. It callsgetInstanceAsync()for you, so code inside the invocation callbacks needs nothing more. -
Code that runs before
sequence(), such as a constructor or a test of the shared fallback outside any invocation, needsawait InvokeStore.getInstanceAsync()after the env stub:vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); await InvokeStore.getInstanceAsync(); const processor = new BatchProcessor(EventType.SQS);
-
Never call
getInstanceAsync()before the env stub, for example from abeforeEachthat runs ahead of a per-testvi.stubEnv(). It caches the non-isolating store, and tests fail on assertions because one invocation reads the other's state. Nothing throws. -
Tests for the
InvokeStore is not availableerror stub the global away withvi.stubGlobal('awslambda', undefined). Restore it withvi.unstubAllGlobals()inafterEachor later tests lose it too.
When unsure, copy the pattern of an existing test in the same package.
Feature work updates the MkDocs site in docs/features/<utility>.md. Code examples are real TypeScript files in examples/snippets/<utility>/, included via --8<-- snippet syntax — examples/snippets is a workspace that CI lints and type-checks, so every doc example must compile.