Global init() or configure() functions that mutate module-level state break every scenario with multiple concurrent consumers: test suites that need different API keys per test, monorepo apps using the same package with different configurations, and multi-tenant servers handling requests with per-user credentials. ISO 25010 testability and modifiability both require that configuration be isolatable per instance. Module-level state also prevents the garbage collector from cleaning up instances and leaks across test runs.
Low because the limitation only surfaces when multiple configurations are required in one process — a constraint many simple consumer apps never hit — but the fix is cheap and the pattern is easily corrected.
Replace global configuration state with a constructor or factory function that captures config in closure.
// Before — global state:
let globalConfig: Config
export function init(config: Config) { globalConfig = config }
export function doSomething() { /* uses globalConfig */ }
// After — per-instance via factory:
export function createClient(config: Config) {
return {
doSomething() { /* uses config from closure */ }
}
}
This pattern allows test suites to create isolated instances (const clientA = createClient({ apiKey: 'test-key-a' })) and monorepo apps to configure multiple instances independently. All configuration options should have TypeScript types and documented defaults.
ID: sdk-package-quality.api-surface.config-pattern
Severity: low
What to look for: Enumerate every configuration option accepted by the SDK. For each option, check how the package is configured by consumers. Look for:
new Client({ apiKey, baseUrl }) — configuration is per-instancecreateClient({ apiKey, baseUrl }) — same, just a different styleSDK.configure({ apiKey }) followed by SDK.doSomething() — mutable shared statesetApiKey('...') that modifies module-scoped variablesprocess.env.API_KEY read at module loadPass criteria: The primary configuration mechanism is per-instance (constructor or factory function). Consumers can create multiple instances with different configurations. Environment variable reading is acceptable if it's done inside the constructor/factory (lazy), not at module load time — 100% of configuration options must have sensible defaults and TypeScript types. Report: "X config options found, all Y have typed defaults."
Fail criteria: The only configuration mechanism is a global configure() or init() function that sets module-level state. Multiple consumers in the same process cannot use different configurations.
Skip (N/A) when: The package has no configuration — it's a pure utility library with no state, or configuration is entirely through function parameters (e.g., format(value, options)).
Detail on fail: "Configuration is via a module-global SDK.init({ apiKey }) that sets module-level state. This means only one configuration can exist per process — breaking in monorepos, test suites, and multi-tenant applications."
Remediation: Per-instance configuration is more flexible and testable than global state.
// Before — global state:
let globalConfig: Config
export function init(config: Config) { globalConfig = config }
export function doSomething() { /* uses globalConfig */ }
// After — per-instance:
export class Client {
constructor(private config: Config) {}
doSomething() { /* uses this.config */ }
}
// Or factory function:
export function createClient(config: Config) {
return {
doSomething() { /* uses config from closure */ }
}
}