If your training dashboard starts as one HTTP request and grows into athletes, activities, wellness, workouts, gear, and performance data, a hand-written fetch wrapper becomes expensive to maintain. Every new endpoint adds another URL, another response shape, and another place to get authentication or retry behavior wrong.
This tutorial shows a small, reproducible path with intervals-icu, an open-source TypeScript client for the Intervals.icu API. The goal is not to build a complete training application. It is to establish a typed client, choose the right authentication boundary, call one service, and understand what changes when you move from version 1 to version 2 of the library.
TL;DR
Install the stable npm package, create an IntervalsClient with an API key or OAuth access token, and use service accessors such as client.athletes or client.activities. Version 2 uses typed service methods, retries selected transient failures, and defaults requests to the authenticated athlete.
Prerequisites
You need:
- Node.js 18 or newer.
- npm.
- An Intervals.icu account with an API key, or an OAuth access token for an application acting for other users.
- A TypeScript project that can run ESM modules.
The published package is intervals-icu version 2.2.1, and its package metadata declares Node.js >=18.0.0. The repository is public and licensed under MIT. The examples below target that stable package version, not an unreleased default-branch change.
Install the stable client
Create a small project and pin the package version used in this tutorial:
mkdir intervals-demo
cd intervals-demo
npm init -y
npm install intervals-icu@2.2.1
npm install -D typescript tsx
The package publishes both ESM and CommonJS entry points and exposes TypeScript declarations from its package root. Add a script so a .ts file can run without a separate build step:
{
"type": "module",
"scripts": {
"start": "tsx src/index.ts"
}
}
Create the smallest useful client
Create src/index.ts. Keep the credential outside source control. The placeholder below is intentionally not a real key:
import { IntervalsClient } from 'intervals-icu';
const apiKey = process.env.INTERVALS_API_KEY;
if (!apiKey) {
throw new Error('Set INTERVALS_API_KEY before running this example');
}
const client = new IntervalsClient({
apiKey,
athleteId: '0',
timeout: 30_000,
maxRetries: 3,
retryDelayMs: 1_000,
});
const athlete = await client.athletes.getAthlete();
console.log({
id: athlete.id,
name: athlete.name,
ftp: athlete.ftp,
});
Run it with an environment variable rather than placing the key in the file:
INTERVALS_API_KEY=your-key npm start
On Windows PowerShell, use the equivalent session-scoped variable:
$env:INTERVALS_API_KEY = "your-key"
npm start
The expected result is an object containing the authenticated athlete's fields returned by the API. The exact values depend on the account, so a reproducible verification is to confirm that id is present and that the command exits successfully without printing the credential.
Why service accessors matter
The client groups operations by resource instead of putting every method on one large facade. The same client can read activities, events, wellness records, and workouts through separate accessors:
const recentActivities = await client.activities.listActivities({
oldest: '2026-01-01',
newest: '2026-01-31',
});
const events = await client.events.listEvents({
oldest: '2026-01-01',
newest: '2026-01-31',
});
const wellness = await client.wellness.listWellness({
oldest: '2026-01-01',
newest: '2026-01-31',
});
console.log({
activities: recentActivities.length,
events: events.length,
wellnessRecords: wellness.length,
});
This layout is more than naming preference. It gives each resource group a discoverable boundary and lets the compiler catch common mistakes such as calling a method that belonged to the old facade API. The repository exports more than 100 typed methods across 16 service groups, including routes, gear, weather, custom items, fitness, performance, and search.
Add a typed activity query
Activity IDs are strings in version 2. Use the service accessor and an ID such as i55610271:
const activity = await client.activities.getActivity('i55610271');
console.log({
id: activity.id,
name: activity.name,
type: activity.type,
startDateLocal: activity.start_date_local,
});
Do not copy the old numeric-ID form from a version 1 example. The project's migration guide documents the breaking change and the replacement method names. That guide is worth reading before upgrading because all top-level facade methods were removed in version 2.
API keys, OAuth, and retry behavior
Use an API key for a personal integration. Use accessToken when your application obtains an OAuth bearer token for users:
const client = new IntervalsClient({
accessToken: process.env.INTERVALS_ACCESS_TOKEN,
athleteId: 'i12345',
});
The two credential options represent different trust boundaries. An API key is a personal secret and should stay on a server or local process you control. An OAuth access token is still a secret, even though it represents delegated access. Do not ship either value in browser JavaScript, commit it to Git, or include it in logs.
Version 2.2.0 added support for the server's Retry-After response and jittered exponential backoff. The client retries 429 and 5xx responses by default, and maxRetries and retryDelayMs let you tune that policy. Retries reduce sensitivity to temporary failures, but they do not make a request safe to repeat in every business workflow. Be especially careful with mutating methods and design your own application-level idempotency where the API operation requires it.
Common failure modes
The client constructs but the request fails
Constructing IntervalsClient does not prove that the credential is valid. Verify the environment variable, token scope, athlete ID, and the account's access to the requested resource. A request is the meaningful smoke test.
A version 1 example no longer compiles
Replace client.getAthlete() with client.athletes.getAthlete(), client.getEvents() with client.events.listEvents(), and numeric activity IDs with strings. These are intentional version 2 changes, not TypeScript configuration problems.
A request keeps retrying
Inspect the final IntervalsAPIError, including its HTTP status and retry information. A rate limit response may tell you when to try again, while a persistent 4xx response generally needs a credential, input, or permission change. Setting maxRetries to 0 can make failure timing clearer during local debugging.
The data looks different from your expectation
Treat the API response as an external contract. Keep the package types, the Intervals.icu API documentation, and your own validation close to the integration. Types improve the calling code, but they cannot guarantee that a remote service will never change.
FAQ
Does this client require an API key?
Every authenticated API request needs either an API key or an OAuth access token. The configuration makes apiKey optional because OAuth is supported, not because the API is anonymous.
Can I use it for activities and wellness data?
Yes. Version 2 exposes separate accessors for activities, wellness, events, workouts, and other resource groups. Check the current API permissions before requesting or mutating data.
Does it run in the browser?
The package is a Node.js client with credentials and server-oriented HTTP behavior. Do not put a personal API key in a browser bundle. If you need a browser UI, put a controlled backend between the UI and Intervals.icu and expose only the operations your application needs.
Takeaway
intervals-icu gives a TypeScript application a clear starting point for Intervals.icu integration: install a stable version, keep credentials server-side, use resource-specific services, and let the typed client handle the repetitive HTTP layer. Start with one read-only request, verify the returned shape, and add writes only after you understand the account permissions and retry consequences.
Have you found a better boundary for exposing training data to a frontend: a narrow backend API per feature, or a more general proxy with stricter authorization rules?
AI assistance disclosure: AI assistance was used to organize and edit this tutorial. The project documentation, package metadata, migration guide, changelog, and example API shape were checked against current primary sources before publication.
Top comments (0)