@tomkp/strava
v1.0.1
Published
A complete, type-safe TypeScript client for the Strava API v3
Maintainers
Readme
@tomkp/strava
A complete, type-safe TypeScript client for the Strava API v3.
Features
- Full TypeScript support with comprehensive type definitions
- Automatic token refresh when tokens expire
- Rate limit tracking
- Specific error types for different failure scenarios
- Zero runtime dependencies - uses native fetch
- Database agnostic - works with any storage solution
- Framework agnostic - use with Express, Fastify, Next.js, or any Node.js app
Installation
npm install @tomkp/strava
# or
yarn add @tomkp/strava
# or
pnpm add @tomkp/stravaNote: Requires Node.js 18+ (uses native fetch)
Quick Start
1. Initialize the Client
import { StravaClient } from "@tomkp/strava";
const client = new StravaClient({
clientId: process.env.STRAVA_CLIENT_ID!,
clientSecret: process.env.STRAVA_CLIENT_SECRET!,
redirectUri: "http://localhost:3000/auth/callback",
});2. OAuth Flow
// Redirect user to Strava authorization
const authUrl = client.getAuthorizationUrl("activity:read_all");
// Redirect to: authUrl
// After user authorizes, exchange code for tokens
const tokenResponse = await client.exchangeAuthorizationCode(code);
// Tokens are automatically stored in the client
// You should also save them to your database
await saveTokensToDatabase(tokenResponse);3. Make API Calls
// Get athlete info
const athlete = await client.getAthlete();
console.log(`Welcome ${athlete.firstname} ${athlete.lastname}!`);
// Get activities
const activities = await client.getActivities({ per_page: 50 });
console.log(`You have ${activities.length} activities`);
// Get detailed activity with streams
const activity = await client.getActivity(activityId);
const streams = await client.getActivityStreams(activityId);Complete Examples
Express.js Integration
import express from "express";
import { StravaClient, StravaTokens } from "@tomkp/strava";
const app = express();
// Initialize client
const stravaClient = new StravaClient({
clientId: process.env.STRAVA_CLIENT_ID!,
clientSecret: process.env.STRAVA_CLIENT_SECRET!,
redirectUri: "http://localhost:3000/auth/callback",
onTokenRefresh: async (tokens) => {
// Save refreshed tokens to database
await db.updateTokens(userId, tokens);
},
});
// OAuth routes
app.get("/auth/strava", (req, res) => {
const authUrl = stravaClient.getAuthorizationUrl("activity:read_all");
res.redirect(authUrl);
});
app.get("/auth/callback", async (req, res) => {
const { code } = req.query;
try {
const tokenResponse = await stravaClient.exchangeAuthorizationCode(code as string);
// Save tokens to database
await db.saveUser({
stravaId: tokenResponse.athlete.id,
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token,
expiresAt: tokenResponse.expires_at,
});
res.redirect("/dashboard");
} catch (error) {
console.error("OAuth error:", error);
res.redirect("/error");
}
});
// API routes
app.get("/api/activities", async (req, res) => {
try {
// Load tokens from database
const tokens = await db.getUserTokens(userId);
stravaClient.setTokens(tokens);
// Fetch activities
const activities = await stravaClient.getActivities();
res.json(activities);
} catch (error) {
console.error("Error fetching activities:", error);
res.status(500).json({ error: "Failed to fetch activities" });
}
});With Automatic Token Refresh
import { StravaClient } from "@tomkp/strava";
const client = new StravaClient({
clientId: process.env.STRAVA_CLIENT_ID!,
clientSecret: process.env.STRAVA_CLIENT_SECRET!,
autoRefresh: true, // Enable automatic refresh (default: true)
refreshBuffer: 600, // Refresh 10 minutes before expiry (default: 600)
onTokenRefresh: async (tokens) => {
// This callback is called whenever tokens are refreshed
console.log("Tokens refreshed!");
await database.updateTokens(userId, {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.expiresAt,
});
},
});
// Load tokens from database
const storedTokens = await database.getTokens(userId);
client.setTokens(storedTokens);
// The client will automatically refresh tokens if needed
// No need to manually check expiration!
const athlete = await client.getAthlete();Fetching All Activities with Pagination
// Fetch all activities (handles pagination automatically)
const allActivities = await client.getAllActivities();
console.log(`Total activities: ${allActivities.length}`);
// Fetch activities after a specific date
const after = new Date("2024-01-01").getTime() / 1000;
const recentActivities = await client.getAllActivities({ after });
// Fetch with manual pagination control
let page = 1;
let hasMore = true;
const activities = [];
while (hasMore) {
const pageActivities = await client.getActivities({
page,
per_page: 200, // Max allowed by Strava
});
if (pageActivities.length === 0) {
hasMore = false;
} else {
activities.push(...pageActivities);
page++;
}
}Error Handling
import {
StravaClient,
StravaRateLimitError,
StravaAuthenticationError,
StravaNotFoundError,
isStravaErrorType,
} from "@tomkp/strava";
try {
const activities = await client.getActivities();
} catch (error) {
// Handle specific error types
if (isStravaErrorType(error, StravaRateLimitError)) {
console.error("Rate limit exceeded!");
console.error(`Retry after: ${error.retryAfter} seconds`);
console.error(`Current usage: ${error.usage}`);
} else if (isStravaErrorType(error, StravaAuthenticationError)) {
console.error("Authentication failed - tokens may be invalid");
// Redirect user to re-authenticate
} else if (isStravaErrorType(error, StravaNotFoundError)) {
console.error("Resource not found");
} else {
console.error("Unexpected error:", error);
}
}Monitoring Rate Limits
// Make an API call
const activities = await client.getActivities();
// Check rate limit status
const rateLimits = client.getRateLimitInfo();
if (rateLimits) {
console.log("15-minute limit:", rateLimits.shortTerm);
console.log("Daily limit:", rateLimits.longTerm);
// Check if approaching limits
const shortTermPercent = (rateLimits.shortTerm.usage / rateLimits.shortTerm.limit) * 100;
if (shortTermPercent > 80) {
console.warn("Approaching short-term rate limit!");
}
}Getting Activity Details and Streams
// Get detailed activity information
const activity = await client.getActivity(activityId, true); // includeAllEfforts = true
console.log(`Activity: ${activity.name}`);
console.log(`Distance: ${activity.distance}m`);
console.log(`Moving Time: ${activity.moving_time}s`);
// Get time-series data (streams)
const streams = await client.getActivityStreams(activityId, {
keys: ["time", "distance", "altitude", "heartrate"],
key_by_type: true,
});
if (streams.heartrate) {
console.log("Heart rate data:", streams.heartrate.data);
}
if (streams.altitude) {
console.log("Elevation data:", streams.altitude.data);
}API Reference
StravaClient
Constructor
new StravaClient(config: StravaClientConfig)Config Options:
clientId(required): Your Strava OAuth client IDclientSecret(required): Your Strava OAuth client secretredirectUri(optional): OAuth callback URLautoRefresh(optional): Auto-refresh tokens when expired (default: true)refreshBuffer(optional): Seconds before expiry to trigger refresh (default: 600)onTokenRefresh(optional): Callback when tokens are refreshed
Methods
Token Management:
setTokens(tokens)- Set authentication tokensgetTokens()- Get current tokensclearTokens()- Clear tokens (logout)hasValidTokens()- Check if tokens are validgetAuthorizationUrl(scope, state?)- Get OAuth authorization URLexchangeAuthorizationCode(code)- Exchange auth code for tokensrefreshAccessToken(refreshToken?)- Manually refresh tokensdeauthorize()- Revoke application access
Athlete:
getAthlete()- Get authenticated athletegetAthleteStats(athleteId)- Get athlete statistics
Activities:
getActivities(options?)- Get activities (paginated)getAllActivities(options?)- Get all activities (auto-pagination)getActivity(activityId, includeAllEfforts?)- Get activity detailsgetActivityStreams(activityId, options?)- Get time-series datagetActivityZones(activityId)- Get heart rate/power zonesgetActivityLaps(activityId)- Get activity laps
Utilities:
testConnection()- Test API connectiongetClientInfo()- Get client state summarygetRateLimitInfo()- Get current rate limit info
Type Definitions
All Strava API types are fully typed. Key types include:
StravaAthlete- Athlete profileStravaActivity- Activity dataStravaStreams- Time-series dataStravaTokens- OAuth tokensStravaRateLimitInfo- Rate limit information
Error Types
The client provides specific error classes for different scenarios:
StravaAuthenticationError- Authentication failed (401)StravaAuthorizationError- Insufficient permissions (403)StravaNotFoundError- Resource not found (404)StravaRateLimitError- Rate limit exceeded (429)StravaValidationError- Invalid parameters (400)StravaNetworkError- Network/connection errorStravaApiError- Server error (5xx)StravaError- Base error class
Strava Rate Limits
Strava has two rate limits:
- Short-term: 200 requests per 15 minutes
- Long-term: 2,000 requests per day
Use client.getRateLimitInfo() to monitor your usage.
OAuth Scopes
Common OAuth scopes:
read- Read public dataread_all- Read private dataactivity:read- Read activitiesactivity:read_all- Read all activities (including private)activity:write- Create/update activitiesprofile:read_all- Read full profileprofile:write- Update profile
Multiple scopes can be combined: activity:read_all,profile:read_all
Environment Variables
Create a .env file with your Strava credentials:
STRAVA_CLIENT_ID=your_client_id
STRAVA_CLIENT_SECRET=your_client_secret
STRAVA_REDIRECT_URI=http://localhost:3000/auth/callbackGet your credentials at: https://www.strava.com/settings/api
Best Practices
- Store tokens securely - Never expose tokens in client-side code
- Use environment variables - Keep credentials out of your codebase
- Handle rate limits - Monitor usage and implement backoff strategies
- Implement token refresh - Use the
onTokenRefreshcallback to update your database - Error handling - Always wrap API calls in try-catch blocks
- Test connection - Use
testConnection()to verify authentication
License
MIT
