Strava is one of my most-used apps and my primary form of social media; I take great interest in the the data I can get from Strava's API on my health and fitness. This is a guide on how to make your own API application with Next.js to get your fitness data.
Create a Strava application
After you are logged in to your Strava account, head to https://www.strava.com/settings/api and create an application. Add your application's client ID and client secret to your .env:
STRAVA_CLIENT_ID=123456
STRAVA_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxx
Strava API Authentication
Every API request needs an access token, but access tokens are short-lived so you'll need a long-lived refresh token to generate new access tokens without the need to re-authenticate each time.
Authorize your app
Go to http://www.strava.com/oauth/authorize?client_id=[YOUR_CLIENT_ID]&response_type=code&redirect_uri=http://localhost/exchange_token&approval_prompt=force&scope=read and click "Authorize".
Optional: Scope change
The URL above is for only the most basic use case. I prefer to grant all permissions because the data I want requires a scope of read_all or activity:read_all, and I also want the ability to edit activities. The URL I use for authentication is http://www.strava.com/oauth/authorize?client_id=[YOUR_CLIENT_ID]&response_type=code&redirect_uri=http://localhost/exchange_token&approval_prompt=force&scope=read,read_all,profile:read_all,profile:write,activity:read,activity:read_all,activity:write. But if you're building an app for real users, you'll need to be more selective about which permissions to request.
Get the refresh token
You'll be redirected to a broken page. Look at the URL and copy the code parameter. This is your authorization code. To get your refresh token, make a curl request with your authorization code, client_id, and client_secret:
curl -X POST https://www.strava.com/oauth/token \
-F client_id=YOURCLIENTID \
-F client_secret=YOURCLIENTSECRET \
-F code=AUTHORIZATIONCODE \
-F grant_type=authorization_code
The response will look something like this:
{
"token_type": "Bearer",
"expires_at": 1562908002,
"expires_in": 21600,
"refresh_token": "REFRESHTOKEN",
"access_token": "ACCESSTOKEN",
"athlete": {
"id": 123456,
"username": "MeowTheCat",
"resource_state": 2,
"firstname": "Meow",
"lastname": "TheCat",
"city": "",
"state": "",
"country": null,
...
}
}
Add the refresh_token value to your .env:
STRAVA_CLIENT_ID=123456
STRAVA_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxx
STRAVA_REFRESH_TOKEN=xxxxxxxxxxxxxxxxxxxxx
Get your Strava data into React
In your Next.js app, create a new file called lib/strava.ts to handle all API requests. It will use the environment variables to generate the access token needed for each request, and hold helper functions such as getActivities, getActivityById, etc.
Here's a sample getActivites function that returns a list of recent public activities for the requested user:
// app/lib/strava.ts
const clientId = process.env.STRAVA_CLIENT_ID;
const clientSecret = process.env.STRAVA_CLIENT_SECRET;
const refreshToken = process.env.STRAVA_REFRESH_TOKEN;
const USER_ID = 64884851; // <- Replace with your Strava user ID
const TOKEN_ENDPOINT = "https://www.strava.com/oauth/token";
const ATHLETE_ENDPOINT = `https://www.strava.com/api/v3/athletes/${USER_ID}`;
const getAccessToken = async () => {
const body = JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken,
grant_type: "refresh_token",
});
const response = await fetch(TOKEN_ENDPOINT, {
method: "POST",
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
},
body,
});
return response.json();
};
export const getActivities = async () => {
const { access_token: accessToken } = await getAccessToken();
const response = await fetch(
`${ATHLETE_ENDPOINT}/activities?access_token=${accessToken}`
);
const json = await response.json();
const publicActivities = json.filter(
(activity) => activity.visibility === "everyone"
);
return publicActivities;
};
Rate limits and pagination
By default, the API's read rate limits are 100 requests every 15 minutes, and 1,000 requests daily (200 and 2,000, respectively, for overall rate limits). This means you should try to get more data per request.
Requests that return multiple items are paginated to 30 items by default. You can get up to 200 items per request by increasing the page size. You can also use page parameter to specify offsets and further pages. page and page_size are hard-coded here but should be computed based on your needs:
const page = 1;
const pageSize = 200;
export const getActivities = async () => {
const { access_token: accessToken } = await getAccessToken();
const response = await fetch(
`${ENDPOINT}/${USER_ID}/activities?access_token=${accessToken}&page=${page}&per_page=${pageSize}`,
);
const json = await response.json();
return json;
};
Next.js specifics
If you're using the Next.js App Router, you can call functions like getActivities directly inside Server Components, since they run on the server. If you're using the Pages Router, you can fetch data on the server before rendering by calling getActivities inside getStaticProps or getServerSideProps.
Either way, a good way to minimize requests to the Strava API is to take advantage of Next.js caching and revalidating. In the App Router, you can use the built-in Next.js fetch options:
export const getActivities = async () => {
const { access_token: accessToken } = await getAccessToken();
const response = await fetch(
`${ENDPOINT}/${USER_ID}/activities?access_token=${accessToken}&page=1&per_page=200`,
{
next: {
revalidate: 3600, // cache for 1 hour
tags: ["strava-activities"], // tags to use for on-demand revalidation if needed
}
}
);
const json = await response.json();
return json;
};
This approach ensures your data stays reasonably fresh while significantly reducing the number of requests made.
Results
If all goes right, you should get an array of activities that looks something like this:
{
"activities": [
{
"resource_state": 2,
"athlete": {
"id": 64884851,
"resource_state": 1
},
"name": "Morning Run",
"distance": 19450.5,
"moving_time": 5108,
"elapsed_time": 5289,
"total_elevation_gain": 31,
"type": "Run",
"sport_type": "Run",
"workout_type": 3,
"device_name": "Garmin fēnix 7 Pro (47mm)",
"id": 18069600826,
"start_date": "2026-04-11T15:44:44Z",
"start_date_local": "2026-04-11T08:44:44Z",
"timezone": "(GMT-08:00) America/Los_Angeles",
"utc_offset": -25200,
"location_city": null,
"location_state": null,
"location_country": null,
"achievement_count": 18,
"kudos_count": 75,
"comment_count": 7,
"athlete_count": 2,
"photo_count": 0,
"map": {
"id": "a18069600826",
"summary_polyline": "sylnExdarUvAy@|@u@\\a@rFeE`C{BjBuAxDeDdAq@RNnAnBdBvDx@|AjAjB`AhCIPgD`CoAbAuChCcClBgLvJgAdAeGzEkDxCi@ZkC~BcElDa@TWZaBfAqDdDi@\\}BzBoDhCcFfE_B`BoCrB}DrD_An@mCbCeApAKPKx@WZm@Ry@GmAx@WXId@Nb@d@t@Fl@MZs@~@K?g@b@eGrFYd@c@Ns@HmAf@u@CSVk@jBsAvBmD~C{A|@aB|Aq@`@qFrFm@`@gBjBQFOEs@Dq@JcAl@kB`BuAhBiAdAm@v@uBhBk@x@o@vAy@hAuBdAe@^yBpCgB`Bg@p@{AzAk@v@aBbBiNtOW\\_@~@gAfAg@t@iC|Ci@hAQf@Gn@FbBEnA[~@uBbCuEjE]t@a@zA}@~AqBfBkAzAiAl@{@z@yHnQs@bAgB|@e@l@_@z@aB~Fs@dB{AtCcArAsAbCmBvCuBjEKJcADc@L}ClCI~@k@hBmAzB}@nAqBfEw@rBi@bA]`AoB|DKXAXJb@?Z{@xEcArC{@x@KVc@dDa@xAaD`KIHKG?MTUb@}@`DsKN]VsBT]j@g@hAeD~@mE?a@Qc@AOT{@xAoCz@uBd@{@~BcFt@yA|AaC`AiCF{@HUtCmC`BKVUlBaE~BsDf@i@jBoC`B{Cj@_BrAeFj@}A`@c@~Ay@r@y@xF{MlAcCt@}@jAm@vAeB~AsA|@yAhAoDR[hEyDfB{BVo@Da@?w@K}@@c@Hw@Po@pGaIl@kAhG_H`@o@bCqCtGeHd@a@`GcH|@w@`AU|@q@d@o@t@mBrCeD`CgB`B{BlCyBp@QvAEn@[xAyAp@g@xI_J~BwApDcD`BiCZqAHEl@@bAi@l@EZOnCmCbBsAdAiA|AoAR[BQK_@k@y@IY?[Ra@nAiAVCt@Hd@OZc@Lo@N_@fAwAQNbAs@x@{@vAu@vAsAVCn@}@fAcApA_Ax@{@xBaBpE_ExA_AvAwAj@_AlHmFnBmBbAu@xA{AlBqAp@o@~@k@p@k@L?\\g@`CmBd@k@bDcCr@{@bFkEnA{@tHsGf@YpBoBhE_DUo@Ek@IQcAoB_@]s@kAq@aBo@}@Ys@QQu@aB_DfBcEhDs@t@mCvBi@j@wA~@sAfAiAfAmAx@",
"resource_state": 2
},
"trainer": false,
"commute": false,
"manual": false,
"private": false,
"visibility": "everyone",
"flagged": false,
"gear_id": "g29695258",
"start_latlng": [
33.985703,
-118.46749
],
"end_latlng": [
33.985664,
-118.467479
],
"average_speed": 3.808,
"max_speed": 5.1,
"average_cadence": 81.9,
"average_watts": 410.1,
"max_watts": 538,
"weighted_average_watts": 412,
"device_watts": true,
"kilojoules": 2094.7,
"has_heartrate": true,
"average_heartrate": 163,
"max_heartrate": 184,
"heartrate_opt_out": false,
"display_hide_heartrate_option": true,
"elev_high": 8.8,
"elev_low": 2,
"upload_id": 19171079592,
"upload_id_str": "19171079592",
"external_id": "garmin_ping_557352014877",
"from_accepted_tag": false,
"pr_count": 12,
"total_photo_count": 2,
"has_kudoed": false
},
{
"resource_state": 2,
"athlete": {
"id": 64884851,
"resource_state": 1
},
"name": "Afternoon Elliptical",
"distance": 0,
"moving_time": 3633,
"elapsed_time": 3633,
"total_elevation_gain": 0,
"type": "Elliptical",
"sport_type": "Elliptical",
"workout_type": 27,
"device_name": "Garmin fēnix 7 Pro (47mm)",
"id": 18059561999,
"start_date": "2026-04-10T23:03:59Z",
"start_date_local": "2026-04-10T16:03:59Z",
"timezone": "(GMT-07:00) America/Creston",
"utc_offset": -25200,
"location_city": null,
"location_state": null,
"location_country": null,
"achievement_count": 0,
"kudos_count": 11,
"comment_count": 0,
"athlete_count": 1,
"photo_count": 0,
"map": {
"id": "a18059561999",
"summary_polyline": "",
"resource_state": 2
},
"trainer": true,
"commute": false,
"manual": false,
"private": false,
"visibility": "everyone",
"flagged": false,
"gear_id": null,
"start_latlng": [],
"end_latlng": [],
"average_speed": 0,
"max_speed": 0,
"average_cadence": 66.7,
"has_heartrate": true,
"average_heartrate": 141.2,
"max_heartrate": 150,
"heartrate_opt_out": false,
"display_hide_heartrate_option": true,
"elev_high": 0,
"elev_low": 0,
"upload_id": 19160919308,
"upload_id_str": "19160919308",
"external_id": "garmin_ping_557055123872",
"from_accepted_tag": false,
"pr_count": 0,
"total_photo_count": 0,
"has_kudoed": false
}
],
...
}
That's it! The sky's the limit in terms of what you do with this data. I suggest using /athletes/{id}/stats, /activities/{id}/zones, and /activities/{id}/laps to get more meaningful data.
You can explore more endpoints and definitions at the official Strava API docs or the Swagger UI playground.