Simplify Your REST API Logic in React with Connectors for REST APIs and GraphQL

Amanda Martin
If you’ve built a React or Next.js app that talks to multiple REST APIs, you’ve probably got a file like actions.ts set up as a central spot for fetch calls to public services like the USGS Earthquake API or Nominatim’s reverse geocoder. It works, but the code often ends up repetitive, brittle, and difficult to maintain or scale when different endpoints need to talk to each other.
In this post, we’re going to take that same setup and clean it up with a GraphQL layer powered by Apollo Connectors. Instead of orchestrating data in actions.ts, we’ll define a GraphQL schema that does the work for us. Using a declarative configuration, we’ll unify earthquake and location data in a single query with no need for custom resolvers or custom backend logic. The goal: to make your frontend simpler and your data fetching smarter, without giving up the REST services and patterns you already know.
Prerequisites
- Create an Apollo Studio account. This allows you to create and manage your graph, providing the necessary credentials APOLLO_KEY and APOLLO_GRAPH_REF (more on those later…), which the Apollo Router uses to fetch the supergraph schema and run locally.
- Install and authenticate Rover CLI, which we will use for configuring our graph and running Apollo Router locally.
Setup
In the directory you want to host your schema in run:
1rover initFollow the prompts in the CLI to create a new project.
Rover will generate a command for you that contains your APOLLO_KEY and GRAPH_REF. You can start the Router with the command provided, but I would recommend putting these values in an VSCode > settings.json.
1{2 "terminal.integrated.profiles.osx": {3 "graphos": {4 "path": "zsh",5 "args": ["-l"],6 "env": {7 "APOLLO_KEY": "<YOUR_KEY>",8 "APOLLO_GRAPH_REF": "<GRAPH_REF>"9 }10 }11 },12 "terminal.integrated.defaultProfile.osx": "graphos"13}Next create a file at the root called router.yaml and place this code in it. This is to override CORS errors while working in dev. This is not for use in production.
1cors:2 allow_any_origin: trueIf you haven’t already open your project in VSCode or your IDE of choice and from the terminal run:
1rover dev --router-config router.yaml --supergraph-config supergraph.yamlBuilding the Schema
Create a new file called earthquake.graphql and paste the following code.
Note: If you prefer, the completed schema can be found here.
1@link(url: "https://specs.apollo.dev/federation/v2.10", import: ["@key"]) # Enable this schema to use Apollo Federation features2@link( # Enable this schema to use Apollo Connectors3 url: "https://specs.apollo.dev/connect/v0.1"4 import: ["@connect", "@source"]5 )6@source(7 name: "usgs"8 http: { baseURL: "https://earthquake.usgs.gov/fdsnws/event/1/" }9 )101112type EarthquakeProperties {13 mag: Float14 place: String15 time: Float16 updated: Float17 url: String18 title: String19}202122type EarthquakeGeometry {23 #this returns 3 values - lon,lat,depth in km24 coordinates: [Float]25}262728type Earthquake {29 id: ID!30 properties: EarthquakeProperties31 geometry: EarthquakeGeometry32 }333435type Query {36 recentEarthquakes(limit: Int! lat: String! lon: String! maxRadius: Int!): [Earthquake]37 @connect(38 source: "usgs"39 http: {40 GET: "query?format=geojson&latitude={$args.lat}&longitude={$args.lon}&maxradiuskm={$args.maxRadius}&limit={$args.limit}"41 }42 selection: """43 $.features44 {45 properties{46 mag47 place48 time49 updated50 url51 title52 }53 geometry{54 coordinates55 }56 id57 }58 59 """60 )61}The first two directives following @link are required at the top of any file to enable Connectors and federation. The @source directive is used to point to our base URL for USGS earthquakes.
Underneath this there are three types: Earthquake, EarthquakeProperties, and EarthquakeGeometry. Here we are defining what we want to make available in our schema. You can build this however you want and include as much of the REST API as is valuable for your application. Here you will also define all the types and what values are nullable.
Finally you will see the query type. In this schema we only have one query, but you can build out as many as you need. For example, while this calls recent earthquakes and takes in four parameters, you may also want to include a query for retrieving one earthquake by Id. What you design in this schema is dependent upon the needs of your application and frontend team. Inside this query, you will see the @connect directive which is where you declare what populates this query and how it will return in the selection set below.
Adding a second REST API
Next, we want to add extended location details for each earthquake using Nominatim. To do this, add in another source directive below the USGS one.
1@source(2 name: "location"3 http: {4 baseURL: "https://nominatim.openstreetmap.org/"5 headers: [{ name: "User-Agent", value: "testing" }]6 }7 )Nominatim requires user-agent headers but the value passed can be anything you want.
Next, in your Earthquake type add a new field “display_name” and the following code:
1type Earthquake {2 id: ID!3 properties: EarthquakeProperties4 geometry: EarthquakeGeometry5 display_name: String6 @connect(7 source: "location"8 http: {9 GET: "reverse?lat={$this.geometry.coordinates->slice(1,2)->first}&lon={$this.geometry.coordinates->first}&format=json"10 }11 selection: """12 $.display_name13 """14 )15}Here we use a Connector for REST to declare that display_name comes from Nominatum. Later in the React app, we’ll cover how you can alias this to what you are using on your frontend. You can also alias here in the schema if you choose. The $this key references the parent Earthquake object.
Your schema is ready, the last thing we need to do before testing it is to update your supergraph.yaml to point to your file. It should look like this:
1subgraphs:2 earthquake:3 routing_url: http://localhost:40004 schema:5 file: earthquake.graphql6federation_version: =2.10.0To learn about configuring your Router, you can head over to the documentation to see more options especially relevant once you are ready to go to production.
Testing the Query
Head to http://localhost:4000 to create and test the query for your app.

Once you have confirmed that your graph is working as expected, it’s time to move to the React app to see what we need to modify. Leave your local Router running as you will need it to be able to test your application.
Modifying the React App
Clone the repo and install the dependencies, run the project.
Open actions.ts so we can investigate the API calls.
There are two functions here, one to call USGS and a second to then call Nominatim on each returned value. There is also some code here to create the necessary object for our frontend.
1export async function searchEarthquakes(2 latitude: number,3 longitude: number,4 maxRadius: number,5 limit = 206): Promise<Earthquake[]> {7 try {8 // Build USGS Earthquake API URL9 const url = `https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&latitude=${latitude}&longitude=${longitude}&maxradius=${maxRadius}&limit=${limit}`;101112 // Fetch earthquake data13 const response = await fetch(url);141516// rest of code omitted here. . . . .171819 return earthquakesWithLocation;20 } catch (error) {21 console.error("Error fetching earthquake data:", error);22 return [];23 }24}Integrating your GraphQL query doesn’t have to be complicated. If you’re comfortable writing a query and calling fetch, you already know 90% of what you need. Head back to the GraphQL sandbox and click the 3 dots to the right of your query. Then select copy to cURL.
Note: If you would rather follow along with the final version, take a look at this branch.

In actions.ts inside the try/catch block at the top, paste your curl. This is a little difficult to read so if you are in VSCode, this is a good place to use copilot to make this more readable. The end result should look like this.
1const query = `2 query RecentEarthquakes($limit: Int!, $lat: String!, $lon: String!, $maxRadius: Int!) {3 recentEarthquakes(limit: $limit, lat: $lat, lon: $lon, maxRadius: $maxRadius) {4 id5 locationDetails: display_name6 properties {7 mag8 place9 time10 updated11 url12 title13 }14 geometry {15 coordinates16 }17 }18 }19 `;202122 const variables = {23 limit,24 maxRadius,25 lat: latitude.toString(),26 lon: longitude.toString(),27 };282930 const response = await fetch("http://localhost:4000/", {31 method: "POST",32 headers: {33 "Content-Type": "application/json",34 },35 body: JSON.stringify({36 query,37 variables,38 }),39 });40if (!response.ok) {41 throw new Error(`GraphQL API error: ${response.statusText}`);42 }434445 const {data} = await response.json();464748 const earthquakes = data.recentEarthquakes ?? [];49 console.log(earthquakes)50 return earthquakes;Here you have your query, the parameters needed from the frontend, and a fetch call to your graph. Notice we are providing an alias to display_name to locationDetails to match our frontends shape.
Let’s clean up the old code.
In the try/catch block you can remove the rest of the code.
1// Build USGS Earthquake API URL2 // const url = `https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&latitude=${latitude}&longitude=${longitude}&maxradius=${maxRadius}&limit=${limit}`;345 // // Fetch earthquake data6 // const response = await fetch(url);789 // if (!response.ok) {10 // throw new Error(`USGS API error: ${response.statusText}`);11 // }121314 // const data = await response.json();15 // const earthquakes = data.features as Earthquake[];161718 // Get location details for each earthquake19 // const earthquakesWithLocation = await Promise.all(20 // earthquakes.map(async (quake) => {21 // try {22 // // Get location details from Nominatim23 // const locationDetails = await getLocationDetails(24 // quake.geometry.coordinates[1], // latitude25 // quake.geometry.coordinates[0] // longitude26 // );272829 // // Add location details to earthquake properties30 // return {31 // ...quake,32 // locationDetails: locationDetails ?? undefined,33 // };34 // } catch (error) {35 // console.error(36 // `Error getting location details for earthquake ${quake.id}:`,37 // error38 // );39 // return quake;40 // }41 // })42 // );434445 // Log the first earthquake with location details for debugging46 // if (earthquakesWithLocation.length > 0) {47 // console.log(48 // "First earthquake with location details:",49 // JSON.stringify(50 // {51 // id: earthquakesWithLocation[0].id,52 // hasLocationDetails:53 // !!earthquakesWithLocation[0].locationDetails,54 // locationDetails:55 // earthquakesWithLocation[0].locationDetails,56 // },57 // null,58 // 259 // )60 // );61 // }626364 // return earthquakesWithLocation;You can also remove the entire function call for getLocationDetails. We are now calling exactly what we need in one GraphQL query so we do not need this second function or API call here.
1async function getLocationDetails(2// latitude: number,3// longitude: number4// ): Promise<string | null> {5// try {6// // Add a small delay to avoid rate limiting7// await new Promise((resolve) => setTimeout(resolve, 100));8910// const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}`;111213// const response = await fetch(url, {14// headers: {15// "User-Agent": "EarthquakeSearchApp/1.0",16// },17// // Ensure we don't cache the response18// cache: "no-store",19// });202122// if (!response.ok) {23// console.error(24// `Nominatim API error: ${response.status} ${response.statusText}`25// );26// return null;27// }282930// const data = await response.json();313233// // Log the response for debugging34// console.log(35// `Location details for ${latitude},${longitude}:`,36// JSON.stringify({ display_name: data.display_name }, null, 2)37// );383940// return data.display_name ?? null;41// } catch (error) {42// console.error("Error getting location details:", error);43// return null;44// }45//}By modifying this file to use our new GraphQL query, we eliminated over 50 lines of redundant code without changing any UI logic. It’s a reminder that simplicity scales and a practical example of how to keep your GraphQL integration dead simple.
Another interesting thing to notice here is that searchByPlace is still intact and using REST. Using Graphql is not all or nothing, you can adopt it in your applications when it makes sense and use it alongside your other REST API calls. This allows you to adopt Graphql slowly without breaking other parts of your application which is especially important if you work with multiple teams.
Save your files and navigate to http://localhost:3000 to see your updates in action.

Wrapping up
While this example uses Apollo Router and Connectors for REST APIs to keep things simple and local, in a production app you’d typically pair this setup with Apollo Client on the frontend. Apollo Client handles caching, state management, and reactive updates, all the things you’d expect in a mature GraphQL application.
But the key point here is GraphQL doesn’t have to be all-or-nothing or hard to adopt. By adding a lightweight GraphQL layer over your existing REST services using Connectors, you can reduce boilerplate, simplify frontend data fetching, and set yourself up for more scalable, maintainable code. And when you’re ready, tools like Apollo Client make it easy to fully integrate this into your application architecture.
To get started building your first Apollo Connector for REST APIs today, check out the documentation or get started with pre-built connectors.