From 18eea7e743315762aeda8121fcd58600d5d3a235 Mon Sep 17 00:00:00 2001
From: mheavey2 <85061867+mheavey2@users.noreply.github.com>
Date: Wed, 22 Oct 2025 15:14:53 +0100
Subject: [PATCH] fix: migrate Foursquare API to new Places API endpoints
(#1474)
* fix: migrate Foursquare API to new Places API endpoints
* docs: update readme
* feat: add fetch error handling to the Foursquare API example
---------
Co-authored-by: Yashar Fakhari <6448697+YasharF@users.noreply.github.com>
---
.env.example | 3 +-
README.md | 16 ++++----
controllers/api.js | 44 +++++++++++++--------
views/api/foursquare.pug | 84 ++++++++++++++++++----------------------
4 files changed, 76 insertions(+), 71 deletions(-)
diff --git a/.env.example b/.env.example
index ddb75f3f4a..27030223c0 100644
--- a/.env.example
+++ b/.env.example
@@ -37,8 +37,7 @@ FACEBOOK_SECRET=41860e58c256a3d7ad8267d3c1939a4a
# FB Pixel ID is optional if you are trying to do customer rtracking
FACEBOOK_PIXEL_ID=
-FOURSQUARE_ID=2STROLSFBMZLAHG3IBA141EM2HGRF0IRIBB4KXMOGA2EH3JG
-FOURSQUARE_SECRET=UAABFAWTIHIUFBL0PDC3TDMSXJF2GTGWLD3BES1QHXKAIYQB
+FOURSQUARE_APIKEY=foursquare-service-key
GITHUB_ID=cb448b1d4f0c743a1e36
GITHUB_SECRET=815aa4606f476444691c5f1c16b9c70da6714dc6
diff --git a/README.md b/README.md
index f9e0065631..a3df4e5b73 100644
--- a/README.md
+++ b/README.md
@@ -220,13 +220,15 @@ Obtain SMTP credentials from a provider for transactional emails. Set the SMTP_U
-- Go to Foursquare for Developers and log in
-- Click on **My Apps** in the top menu
-- Click the **Create A New App** button
-- Enter _App Name_, _Welcome page url_,
-- For **Redirect URI**: your BASE_URL value followed by /auth/foursquare/callback (i.e. `http://localhost:8080/auth/foursquare/callback` )
-- Click **Save Changes**
-- Copy and paste _Client ID_ and _Client Secret_ keys into `.env` file
+- Go to Foursquare for Developers and log in
+
+- Click on **Create a new project** button
+- Enter your _Organization_ and _Project Name_
+- Click **Create**
+- Navigate to your project
+- Click **Settings** in the left-hand-side menu
+- Generate a Service API Key
+- Copy and paste the Service API Key as `FOURSQUARE_APIKEY` in your `.env` file
diff --git a/controllers/api.js b/controllers/api.js
index 60c51d84e9..f7f3d2559c 100644
--- a/controllers/api.js
+++ b/controllers/api.js
@@ -32,29 +32,41 @@ exports.getApi = (req, res) => {
*/
exports.getFoursquare = async (req, res, next) => {
try {
- const headers = {
- Authorization: `${process.env.FOURSQUARE_APIKEY}`,
+ const options = {
+ method: 'GET',
+ headers: {
+ accept: 'application/json',
+ 'X-Places-Api-Version': '2025-06-17',
+ authorization: `Bearer ${process.env.FOURSQUARE_APIKEY}`,
+ },
+ };
+
+ const fetchJson = async (url, fetchOptions, label) => {
+ const res = await fetch(url, fetchOptions);
+ if (!res.ok) {
+ const text = await res.text().catch(() => '');
+ throw new Error(`${label} failed: ${res.status} ${res.statusText} - ${text}`);
+ }
+ return res.json();
};
- const [trendingVenuesRes, venueDetailRes, venuePhotosRes] = await Promise.all([
- fetch('https://api.foursquare.com/v3/places/search?ll=47.609657,-122.342148&limit=10', {
- headers,
- }).then((res) => res.json()),
- fetch('https://api.foursquare.com/v3/places/427ea800f964a520b1211fe3', {
- headers,
- }).then((res) => res.json()),
- fetch('https://api.foursquare.com/v3/places/427ea800f964a520b1211fe3/photos', {
- headers,
- }).then((res) => res.json()),
+ const [trendingVenuesRes, venueDetailRes] = await Promise.all([
+ fetchJson('https://places-api.foursquare.com/places/search?ll=47.609657,-122.342148&limit=10', options, 'Foursquare search'),
+ fetchJson('https://places-api.foursquare.com/places/427ea800f964a520b1211fe3', options, 'Foursquare venue detail'),
]);
res.render('api/foursquare', {
- title: 'Foursquare API (v3)',
- trendingVenues: trendingVenuesRes.results,
+ title: 'Foursquare Places API',
+ trendingVenues: trendingVenuesRes.results || [],
venueDetail: venueDetailRes,
- venuePhotos: venuePhotosRes.slice(0, 9), // Limit the photos to 9
});
} catch (error) {
- next(error);
+ console.error('Foursquare API Error:', error);
+ return res.status(500).render('api/foursquare', {
+ title: 'Foursquare Places API',
+ trendingVenues: [],
+ venueDetail: null,
+ error: 'Failed to fetch Foursquare data',
+ });
}
};
diff --git a/views/api/foursquare.pug b/views/api/foursquare.pug
index ee5bc088e9..989369393a 100644
--- a/views/api/foursquare.pug
+++ b/views/api/foursquare.pug
@@ -7,56 +7,48 @@ block content
| Foursquare API
.btn-group.mb-4.d-flex(role='group')
- a.btn.btn-primary.w-100(href='https://location.foursquare.com/developer', target='_blank')
+ a.btn.btn-primary.w-100(href='https://foursquare.com/developer', target='_blank')
i.far.fa-check-square.fa-sm.me-2
| Developer Info
a.btn.btn-primary.w-100(href='https://docs.foursquare.com/', target='_blank')
i.fas.fa-code-branch.fa-sm.me-2
| API Docs
- h3.text-primary Trending Venues
- p Near longitude: -122.342148, latitude: 47.609657
- table.table.table-striped.table-bordered
- thead
- tr
- th.d-xs
- th Name
- th.d-xs.d-sm Category
- th.d-xs Address
- th.d-xs Distance (meters)
- th.d-xs Open Now ?
- tbody
- each venue in trendingVenues
+ if error
+ .alert.alert-danger.mt-3 #{ error }
+ else
+ h3.text-primary Trending Venues
+ p Near longitude: -122.342148, latitude: 47.609657
+ table.table.table-striped.table-bordered
+ thead
tr
- td.d-xs
- if venue.categories && venue.categories.length > 0
- img(src=venue.categories[0].icon.prefix + '32' + venue.categories[0].icon.suffix, alt=venue.categories[0].name, width='32', height='32')
- else
- | N/A
- td= venue.name
- td.d-xs.d-sm= venue.categories && venue.categories.length > 0 ? venue.categories[0].name : 'N/A'
- td.d-xs= venue.location.formatted_address || 'N/A'
- td.d-xs= venue.distance
- td.d-xs= venue.closed_bucket || 'Unknown'
- br
- h3.text-primary Venue Details
- p
- i
- u #{ venueDetail.name }
- if venueDetail.categories && venueDetail.categories.length > 0
+ th.d-xs
+ th Name
+ th.d-xs.d-sm Category
+ th.d-xs Address
+ th.d-xs Distance (meters)
+ tbody
+ each venue in trendingVenues
+ tr
+ td.d-xs
+ if venue.categories && venue.categories.length > 0
+ img(src=venue.categories[0].icon.prefix + '32' + venue.categories[0].icon.suffix, alt=venue.categories[0].name, width='32', height='32')
+ else
+ | N/A
+ td= venue.name
+ td.d-xs.d-sm= venue.categories && venue.categories.length > 0 ? venue.categories[0].name : 'N/A'
+ td.d-xs= venue.location.formatted_address || 'N/A'
+ td.d-xs= venue.distance
+ br
+ h3.text-primary Venue Details
+ p
+ i
+ u #{ venueDetail.name }
+ if venueDetail.categories && venueDetail.categories.length > 0
+ |
+ | is a #{ venueDetail.categories[0].name }
|
- | is a #{ venueDetail.categories[0].name }
- |
- | located at #{ venueDetail.location.address || 'N/A' }, #{ venueDetail.location.locality || 'N/A' }, #{ venueDetail.location.region || 'N/A' }. (longitude: #{ venueDetail.geocodes.main.longitude }, latitude: #{ venueDetail.geocodes.main.latitude })
- if venueDetail.related_places
- if venueDetail.related_places.children && venueDetail.related_places.children.length > 0
- p Related venues or businesses to #{ venueDetail.name }, which are mostly in the same building or the immediate area are:
- p(style='margin-left: 20px; white-space: pre-wrap') #{ venueDetail.related_places.children.map(place => place.name).join(', ') }
- if venuePhotos && venuePhotos.length > 0
- h4.text-secondary Photo Gallery
- | #{ venueDetail.name }
- .row
- each photo in venuePhotos
- .col-md-4
- img.img-thumbnail(src=photo.prefix + '400x400' + photo.suffix, alt=venueDetail.name, width='100%')
- else
- p No photos available.
+ | located at #{ venueDetail.location.address || 'N/A' }, #{ venueDetail.location.locality || 'N/A' }, #{ venueDetail.location.region || 'N/A' }. (longitude: #{ venueDetail.longitude }, latitude: #{ venueDetail.latitude })
+ if venueDetail.related_places
+ if venueDetail.related_places.children && venueDetail.related_places.children.length > 0
+ p Related venues or businesses to #{ venueDetail.name }, which are mostly in the same building or the immediate area are:
+ p(style='margin-left: 20px; white-space: pre-wrap') #{ venueDetail.related_places.children.map(place => place.name).join(', ') }