
ESI.ts
A TypeScript client for the EVE Online ESI API, built as an engineered product rather than a generated wrapper. It covers every operation in the ESI OpenAPI specification. Every response is validated at runtime, and caching, rate limiting, retry and pagination follow the rules ESI actually enforces. The claims on this page are backed by a test suite that is itself tested.
Documentation site: lgriffin.github.io/ESI.ts, with these guides, a page for every runnable example, and the API reference generated from the TSDoc. The requirements dashboard lists every EARS requirement the client is held to, the scenarios that prove it and its verdict, and how to write a new one.
Release line. Version 12.3.0 is current on npm. 12.0 corrects five list methods and their detail schemas to the shapes ESI actually sends, which changes their return types; nothing else breaks. The 10.x line stays available for Node 18 and 20. What 12.0 changes · What 11.0 changed · Changelog
Install
npm install @lgriffin/esi.tsYou need Node.js 22.12 or later; on Node 18 or 20, stay on 10.x. TypeScript projects need TypeScript 5.4 or later, under node16, nodenext or bundler module resolution. The consumer contract checks every one of those combinations against the published tarball, as ES module and CommonJS.
Quick start
import { EsiClient, createConsoleLogger } from '@lgriffin/esi.ts';
const log = createConsoleLogger('info');
const client = new EsiClient({
userAgent: 'my-app/1.0 (you@example.com)',
logger: createConsoleLogger(), // library diagnostics, at ESI_LOG_LEVEL (default warn)
});
try {
const status = await client.status.getStatus();
log.info(`${status.players} pilots online`);
} finally {
client.shutdown();
}The examples here and in the guides log through an ILogger rather than console: log is the program's own output, and the client gets a logger of its own for library diagnostics. Logging has the details and how to plug in pino or anything else.
Public data needs no token. For character data, pass an EVE SSO access token, or set ESI_ACCESS_TOKEN, and the client attaches it only to the calls that declare a scope:
const character = await client.characters.getCharacterPublicInfo(characterId);
const prices = await client.market.getMarketPrices();
const authed = new EsiClient({ accessToken: token });
const wallet = await authed.wallet.getCharacterWallet(characterId);Next: Using the client for configuration and every domain client, and Authentication for SSO, refresh and many characters.
Multiple characters
An application that acts for many characters has one relationship with ESI, so @lgriffin/esi.ts/client builds one runtime and a view per identity. The views share the rate limiter, the error budget and the ETag cache; each identity's authenticated entries stay apart. esi.public is typed so that an authenticated call does not compile.
import { createEsi } from '@lgriffin/esi.ts/client';
const esi = createEsi({ userAgent: 'my-app/1.0 (you@example.com)' });
const status = await esi.public.status.get();
const wallet = await esi
.as(tokens.identity(characterId))
.character(characterId)
.wallet.get();Many characters has the identities (EsiTokenManager, a raw token, a TokenProvider), what the views share, and the move from tokens.createClient.
Testing your application without ESI
createMockTransport() from @lgriffin/esi.ts/testing answers requests from a table of routes and records what your code sent. Everything between your call and the transport is the real pipeline, so this runs as written:
import { createEsi, identityFromToken } from '@lgriffin/esi.ts/client';
import { createMockTransport } from '@lgriffin/esi.ts/testing';
import { createConsoleLogger } from '@lgriffin/esi.ts';
const log = createConsoleLogger('info');
const transport = createMockTransport().respond({
method: 'GET',
path: '/characters/{character_id}/wallet',
body: 1234567.89,
});
const esi = createEsi({
userAgent: 'my-app/1.0 (you@example.com)',
transport,
});
try {
const view = esi.as(identityFromToken('an-access-token'));
const wallet = await view.character(2114794365).wallet.get();
log.info(`Wallet: ${wallet}`); // Wallet: 1234567.89
log.info(`Sent: ${transport.sent[0]?.headers['authorization']}`); // Sent: Bearer an-access-token
} finally {
esi.shutdown();
}A request no route answers is rejected with an EsiConfigurationError naming the request, without a retry, and appears in transport.unrouted. Testing has the route options and the record.
What you get
| Capability | What ESI.ts does |
|---|---|
| Full coverage | 39 domain clients and 235 routes. They cover all 235 operations in the ESI specification at compatibility date 2026-08-18, plus the specification documents themselves. spec:coverage fails the build if one is missing. |
| Runtime validation | Every GET response is checked against a hand-written Zod schema. Unknown fields pass through, so an additive change from CCP never breaks you. A changed shape throws EsiValidationError instead of corrupting your data quietly. |
| Caching | A GET inside ESI's cache window makes no HTTP call. Older entries are revalidated with ETags, and a 5xx serves the stale copy. A write invalidates the reads it affects. Keys are hashed per token. |
| Rate limiting | One bucket per ESI rate-limit group: 46 of them, generated from the spec. The limiter learns from ESI's headers and honours Retry-After. A 420 or 429 blocks only its own group. |
| Resilience | Exponential backoff with jitter, a single coalesced token refresh on 401, deduplication of identical in-flight GETs, and an opt-in circuit breaker. Each one is an interface you can replace. |
| Pagination | Offset and cursor paging. stream* yields one validated page at a time, and fetchAll* fetches pages concurrently. batch and batchPost handle fan-out. |
| Authentication | EVE SSO with PKCE, and a token manager that handles storage, proactive refresh, rotation, revocation and bulk refresh for many characters. |
| Static data | ./sde answers offline queries over CCP's Static Data Export: 99 typed lookups with no database. It shares no code with the HTTP pipeline, and a lint rule enforces that. It is tested to the same bar as the client (scorecard). |
| Correct wire format | Where the specification is wrong about how ESI reads a request (parameters in the query rather than the body, undocumented length limits), the definitions follow ESI. The specification documents this and live validation proved it. |
The engineering stance
The project is run to a written engineering charter. It has 70 numbered requirements, each in the same EARS form as the test specification, and each with a status that says whether a machine enforces it: 55 are Enforced, 6 Practised, 8 Partial and 1 Gap. A gap is recorded, never hidden. Seven positions explain most of the choices:
- The custom clients are the product. ESI.ts is a library of hand-written, curated domain clients. Method names, argument shapes, wire fixes and validation strictness are product decisions. Generation serves the clients and never replaces them, and a gate fails any change that deprecates one (Client architecture).
- The OpenAPI spec is upstream. Types, cache TTLs, rate-limit groups, scopes and 235 typed operations are generated from it, and CI fails when they go stale. Drift reports keep the hand-written code honest against it.
- Tolerate additive change. New fields and enum members from CCP never break a consumer. A removal is a breaking change.
- Resilience is pluggable. Retry, rate limiting, circuit breaking, deduplication, caching and transport are interfaces, not imports.
- Secure by construction. HTTPS and a host allowlist are checked at construction, tokens are attached only where a scope is declared, URLs are redacted in every error, and cache keys are hashed per token. Each control is a test.
- The specification executes. Behaviour is written as EARS requirements in Gherkin, one per
Rule:, and verified by scenarios that mock only the transport. The audit fails a pull request that weakens the wording. - Verifiable supply chain. Every action is SHA-pinned and every token least-privilege. Releases carry npm provenance, a signed SBOM, cosign signatures and checksums.
By the numbers
Measured on master at 9486fe2c on 2026-09-28 with the commands shown; the mutation and Scorecard rows cite the run that produced them. TESTING.md has the full breakdown.
| Measure | Value | Reproduce |
|---|---|---|
| Tests run offline | 10,045 tests in 319 suites; 9,837 run, 0 failing (208 live tests skip without credentials or an SDE export) | npm test, fuzz, faults, contract:replay, test:integration |
| Unit, BDD and composition | 291 suites, 8,308 tests in about two and a half minutes | npm test |
| Coverage | 97.8% statements, 95.9% branches, 93.6% functions, 97.9% lines | npm run coverage (floors 90 / 80 / 75 / 90) |
| Executable specification | 664 EARS requirements, 863 scenarios, 77 feature files | npm run ears |
| Property and fuzz tests | 1,240 tests; 10,000 runs a property nightly | npm run fuzz |
| Transport fault catalogue | 148 faults through the real pipeline | npm run faults |
| Recorded ESI payloads | 121 replay tests, re-recorded nightly with a drift PR | npm run contract:replay |
| Mutation score | 84.1% of 1,355 mutants in src/core killed by the unit suite (nightly of 2026-09-27); 100% in middleware, pagination and util; floors per directory, ratcheted nightly, changed files on every PR | npm run mutation:ratchet |
| Static data (SDE) | 156 EARS requirements and 223 scenarios in 23 feature files; at least 90% of mutants killed in every SDE directory by the unit suite and by the BDD scenarios alone (2026-10-08, #458); a real-data run against CCP's current export every night | sde/TESTING.md |
| CI | One required check (ci-success) over the full matrix; 29 workflows, 18 of them scheduled | QUALITY-GATES.md |
| OpenSSF Scorecard | 8.3 / 10 on 2026-09-28; the checks still open need settings only the maintainer can change | SECURITY.md |
Every tier has to prove it can fail: a negative fixture, a killed mutant or a caught fault. Every floor is a one-way ratchet.
Packages in the box
| Import | What it holds |
|---|---|
@lgriffin/esi.ts | EsiClient, EsiClientBuilder, EsiApiFactory, domain clients, auth, errors, generated types and scopes |
@lgriffin/esi.ts/schemas | The Zod response schemas |
@lgriffin/esi.ts/errors | Error classes and type guards, including the auth errors |
@lgriffin/esi.ts/testing | createMockTransport and TestDataFactory for your own tests |
@lgriffin/esi.ts/client | createEsi, the spec-shaped low-level surface: esi.public (authenticated calls do not compile), esi.as(identity) |
@lgriffin/esi.ts/sde | SdeDataProvider (YAML and ZIP, through the optional peers js-yaml and adm-zip) and MemorySdeProvider |
@lgriffin/esi.ts/sde/memory | MemorySdeProvider alone, with no file-system or parser code, for browsers and bundles |
import { MarketOrderSchema } from '@lgriffin/esi.ts/schemas';
import { EsiError, isRetryable } from '@lgriffin/esi.ts/errors';
import { SdeDataProvider } from '@lgriffin/esi.ts/sde';
import { createConsoleLogger } from '@lgriffin/esi.ts';
const log = createConsoleLogger('info');
const sdeData = SdeDataProvider.fromDirectory('./sde-data');
log.info(`Type 34: ${sdeData.getType(34)?.name}`); // Type 34: Tritanium
sdeData.close();Guides
The README orients and the guides are canonical. Each guide opens with the charter requirements it implements.
| Using ESI.ts | |
|---|---|
| Using the client | Construction, configuration, every domain client, metadata, batching, examples, 11.0 and 12.0 changes |
| Authentication | Tokens, refresh on 401, SSO with PKCE, the multi-character token manager |
| Many characters | One runtime, esi.public, esi.as(identity), what the views share, moving from createClient |
| Pagination | Offset and cursor paging, stream*, fetchAll*, failure behaviour |
| Errors | Error classes, type guards, retryability, safe mode |
| Runtime validation | Zod response and request validation |
| Logging | ILogger, per-client loggers, pino, ESI_LOG_LEVEL |
| Static data (SDE) | The offline Static Data Export module: its role, isolation, API (module README) |
| How it is built | |
|---|---|
| Engineering charter | The requirements the project holds itself to, with status and gap register |
| Roadmap | Phases, definitions of done, the SDE programme, the release gate |
| Lean decisions | Why it is run this way: every decision since v7, with value stream maps |
| Architecture | Layers, ports, the request path, caching, retry, rate limiting, circuit breaker |
| Client architecture | Custom clients are the product; the generated layer underneath; the plan |
| Design rules | Naming, schemas, adding an endpoint or a client, generated files |
| Testing | Every test tier, what it proves, how to run it |
| Mutation testing | Stryker shards, floors and the ratchet |
| Quality gates | What runs at commit, push, PR, nightly and release; every workflow |
| Security | Runtime defences and supply-chain controls (policy) |
| Semantic versioning | What is public; major, minor or patch; breaking-change commits |
| Release | Cutting a release, changelog, provenance, signatures, support window |
| Audit | The Phase 0 measured baseline for 11.0 |
| OKF bundle | The generated Open Knowledge Format catalogue of ESI |
| Documentation | Documentation surfaces, checked examples, TypeDoc |
| Beads | Issue tracking workflow |
Examples
examples/ has 58 runnable scripts, each with an npm script. The public ones run against live ESI every night, and a failure opens an issue.
npm run example:status # quickest smoke test, no token
npm run example:market # prices and history
npm run example:streaming # stream* over a large region
npm run example # full character profile (ESI_ACCESS_TOKEN)The full list is in USAGE.md, and the examples showcase has a page for each with its source, what it needs and the command that runs it.
Contributing
git clone https://github.com/lgriffin/ESI.ts.git && cd ESI.ts
npm ci
npm run check:local -- --fast # every offline CI tier, minus the buildRead CONTRIBUTING.md first. Every behaviour change starts with an EARS requirement and a failing scenario. Every commit is a conventional commit classified by SEMVER.md. No floor is ever lowered. Work is tracked in GitHub issues labelled by release and phase, mirrored in beads.
License
GPL-3.0-or-later. See LICENSE for the licence text and NOTICE for the copyright line and CCP's trademark notice; EVE Online is the property of CCP hf. and this project is not affiliated with or endorsed by them.
o7
