How to Compose and Integrate APIs Together as if You Were Using NPM for APIs

·

13 min read

image.png

stefan_avram_wundergraph_and_fauna.png

Bringing together two APIs for an app that shows the biggest concerts, historically, by country capital.

With React/Next.js, the fundamental problem you’re solving is that of turning some notion of ‘state’ into DOM, with a focus on composability — using smaller things to build bigger things.

Congratulations, you’ve found the final boss of web dev: building reusable ‘LEGO blocks’ of components that can scale infinitely. If you weren’t using React/Next.js for this battle from the very start, at some point you will inevitably end up re-implementing a much worse, ad-hoc ‘React’ of your own out of jQuery anyway — and be responsible for maintaining it.

But building composable UIs is only half the battle. The most scalable UI in the world would be nothing without data to display. Here, then, is the other half: working with APIs, databases, and microservices. If we’re to go all-in on scalable, modular web apps, we cannot forget about composability in this problem space as well.

This is where WunderGraph — an open-source API developer platform — can help. Within the React mental model, you’re already used to listing all your dependencies in the package.json file, and letting package managers do the rest when you npm install && npm start a project. WunderGraph lets you keep that intuition, and do the exact same thing with your data sources, too:

  1. Explicitly name the APIs, microservices, and databases you need, in a configuration-as-code format, and then,
  2. WunderGraph generates client-side code that gives you first-class typesafe access (via Next.js/React hooks) to all these data sources while working on the front-end.

In this tutorial, we’ll get a little adventurous and bring together two disparate, very different APIs in a Next.js app, to show that with WunderGraph (and no other dependencies) running alongside your frontend as an independent server/API Gateway/BFF, you could essentially write front-end code against multiple REST, GraphQL, MySQL, Postgres, DBaaS like Fauna, MongoDB, etc., as if they were a singular monolith.

Before we start, though, let’s quickly TL;DR a concept:

What Does “Composability” Even Mean for APIs?

Right now, your interactions with sources of data are entirely in code. You write code to make calls to an API endpoint or database (with credentials in your .env file) and then you write more code. This time, async boilerplate/glue to manage the returned data.

image.png

What’s wrong with this picture?

The code itself is correct, what’s “wrong” is that now you’re coupling a dependency to your code, and not your config files. Sure, this weather API isn’t an axios or react-dom (library/framework packages), but it is a dependency nonetheless and now, a third-party API that is only ever mirroring temporary data, has been committed to your repo, made part of your core business, and you will now support it for its entire lifetime.

Sticking with the Lego analogy: this is like gluing your sets together. Welcome to bloated, barely readable, barely maintainable codebases with hard constraints.

You’ll be doing the above times ten on modern medium-to-large apps that are split into many microservices, with complex interactions, and separate teams for each who might step on each others’ toes. That’s not even counting all the JOINs you’ll be doing across these multiple services/APIs to get the data you need.

So what could a scalable approach to API composability look like, without sacrificing developer experience?

  1. It needs to support various data without needing a separate client for each type, and we’ll need to be able to explicitly define these data dependencies outside our app code — maybe in a config file.
  2. It should let us add more data sources as we need them, remove obsolete/dead sources from the dependency array, and auto-update the client to reflect these changes.
  3. It should allow us to stitch together data from multiple sources (APIs, databases, Apollo federations, microservices, etc.) so we can avoid having to do in-code JOINs on the frontend at all.

As it turns out, this is exactly what WunderGraph enables, with a combination of API Gateway and BFF architectures.

If you explicitly name your API dependencies in the WunderGraph config like on the right, here:

image.png

image.png

Conceptually, what’s the difference? There are none; both are config files with a list of things your app needs. Sure, you’re writing a little more code on the right, but that’s just stuff React/Next.js does for you under the hood anyway on the left.

WunderGraph introspects and consolidates these data sources (and not merely the endpoints) into a namespaced virtual graph, and builds a schema out of it.

Now, you no longer care about:

  1. How differently your data dependencies work under the hood.
  2. Shipping any third-party clients on the frontend to support those different data sources.
  3. How your teams are supposed to communicate across domains.

Because now you already have all data dependencies as a canonical layer, a single source of truthGraphQL.

As you might have guessed, next, it’s just a matter of writing operations (GraphQL queries/mutations that WunderGraph gives you autocomplete in your IDE) to get the data you want out of this standardized data layer. These are compiled into a native client at build time, persisted, and exposed using JSON-RPC (HTTP).

image.png

All the DevEx wins of using GraphQL without actually having a public GraphQL endpoint, so none of its security/caching/bundle size concerns on the client side.

Finally, in your front-end code, you use this generated client’s typesafe data-fetching hooks.

image.png

Clear, intuitive, and maintainable.

The end result? A Docker or NPM-like containerization/package manager paradigm, but for data sources. With all the benefits that come with it:

  • APIs, databases, and microservices turned into modular, composable lego bricks, just like your UI components.
  • No more code bloat for JOINs and filters on the frontend, vastly improved code readability, and no more race conditions when trying to do transactional processing over microservices.
  • With the final “endpoint“ being JSON-RPC over good old-fashioned HTTP, caching, permissions, authentication, and security — all become solved problems regardless of the kind of data source.

But why stick to theory? Let’s dive right in!

An Adventure in Treating APIs Like LEGO

The power to compose and bring together data sources like you’d do libraries can lead you to very interesting places, with ideas that no run-of-the-mill public API could ever give you.

Say, for example, what if you wanted to find out what the biggest musical events — concerts, recitals, festivals, etc. — have historically been at a given country’s capital?

There’s literally no API like this out there. You could build the first one. The world’s your oyster!

Step 1: Deciding on Data

So the two APIs we’ll use here are the Countries API and the MusicBrainz aggregator. Feel free to play around with Insomnia/Postman/Playgrounds and get a feel for what data you can reasonably query with these APIs. You’ll probably find a ton of additional, creative use cases.

Step 2: Quick-starting a WunderGraph + Next.js App

When you’re ready to move on, use the starter template in WunderGraph’s repo for a Next.js app that uses the former as a BFF/API Gateway.

npx -y @wundergraph/wunderctl init — template nextjs-starter -o wg-concerts

This will create a new project directory named wg-concerts (or folder name of your choice), spin up both a WunderGraph (at localhost:9991), and a Next.js server(at localhost:3000), by harnessing the npm-run-all package; specifically using the run-p alias to run both in parallel.

Step 3: Concerts by Capital — Cross API Joins without code.

Here’s the meat and potatoes of this guide. I’ve spoken at length about how cross-source data JOINs are a massive pain point when done in-code, and now, you’ll see firsthand how WunderGraph simplifies them.

You can stitch together 2 API responses — in our case, getting the capital of a country, then using that information to query for concerts that took place there — like so:

query ConcertsByCapital($countryCode: ID!, $capital: String! @internal) {
  country: countries\_country(code: $countryCode) {
    name
    capital @export(as: "capital")
    concerts: \_join @transform(get: "music\_search.areas.nodes.events.nodes") {
      music\_search {
        areas(query: $capital, first: 1) {
          nodes {
            events {
              nodes {
                name
                relationships {
                  artists(first: 1) {
                    nodes {
                      target {
                        mbid
                      }
                    }
                  }
                }
                lifeSpan {
                  begin
                }
                setlist
              }
            }
          }
        }
      }
    }
  }

Imagine implementing this query in JavaScript. Truly, spooky season.

  • The @internal directive for args signifies that while this argument is technically an ‘input’, it will only be found internally within this query and need not be provided when we call this operation.
  • The @export directive works hand-in-hand with @internal, and whatever you’re exporting (or the alias — that’s what the ‘as’ keyword is for) must have the same name and type as the arg you’ve marked as internal.
  • _join signifies the actual JOIN operation:
  • As you can tell, the input (query) of this 2nd query uses the same arg we marked as internal at the top level of this GraphQL query.
  • While optional, we’re using the @transform directive (and then the ‘get’ field that points to the exact data structure we need) to alias the response of the 2nd query into ‘concerts’ because any additional query we join will of course add another complex, annoyingly nested structure and we want to simplify and make it as readable as possible.
  • We’re also (optionally) including the relationships field for each concert — to get mbid (the MusicBrainz internal ID of the artist involved in that concert) here because we still want to query the Artist entity individually later (for banners, thumbnails, bios, and such. Again, optional).

Step 4: Getting Artist Details

query ArtistBanner($artistId: music\_MBID!) {
  music\_lookup {
    artist(mbid: $artistId) {
      name
      theAudioDB {
        banner
      }
    }
  }
}

query ArtistDetail($mbid: music\_MBID!) {
  music\_lookup {
    artist(mbid: $mbid) {
      name
      theAudioDB {
        banner
        thumbnail
        biography
      }
    }
  }
}

Speaking of developer experience…here’s a little gotcha with the artistId variable being of type MBID! and not String!. Thanks to WunderGraph, you get code hinting for that in your IDE!

Our second and third operations respectively, are to get a 1000x185 pixel banner image of the artist (from theAudioDB) via their MusicBrainz ID, and then thumbnail/biographies. This is just to prettify our UI, and you can skip these queries if you want just the concert details and nothing else (perhaps because your use-case doesn’t need a UI at all).

Step 5: Displaying Our Data On The Frontend

We’re in the home stretch! Let’s not get too wild here, just each concert mapped to <ConcertCard> components, and a <NavBar> with a <Dropdown> to select a country to fetch concerts in its capital. Oh, and TailwindCSS for styling, of course.

/\* NextJS stuff \*/
import { NextPage } from "next";
/\* WunderGraph stuff \*/
import { useQuery, withWunderGraph } from "../components/generated/nextjs";
/\* my components \*/
import ConcertCard from "../components/ConcertCard";
import NavBar from "../components/NavBar";
/\* my types \*/
import { AllConcertsResult } from "../types/AllConcerts";

const Home: NextPage = () => {
  // WunderGraph-generated typesafe hook for data fetching
  const { result, refetch } = useQuery.ConcertsByCapital({
    input: { countryCode: "BG" },
  });

  // we can just use the provided refetch here (with a callback to our NavBar component) to redo the query when the country is changed.  Neat!
  const switchCountry = (code: string) => {
    refetch({
      input: { countryCode: code },
    });
  };
  const concertsByCapital = result as AllConcertsResult;

  const data = concertsByCapital.data;
  const country = data?.country?.name;
  const capital = data?.country?.capital;
  const concerts = data?.country?.concerts;

  return (
    <div>
      <NavBar
        country={country}
        capital={capital}
        switchCountry={switchCountry}
      />
      <div className="font-mono m-10 text-zinc-50">
        {data ? (
          <div>
            {concerts?.map((concert) => {
              let name = concert?.name;
              let date = concert?.lifeSpan.begin as string;
              let setList = concert?.setlist;
              let artistId =
                concert?.relationships?.artists?.nodes\[0\]?.target.mbid;
              return (
                <ConcertCard
                  name={name}
                  date={date}
                  setList={setList}
                  artistId={artistId}
                />
              );
            })}
          </div>
        ) : (
          <div className="grid h-screen place-items-center"> Loading...</div>
        )}
      </div>
      <hr />
    </div>
  );
};


export default withWunderGraph(Home); // to make sure SSR works

Index.tsx

import Link from "next/link";
import React from "react";

type Props = {
  country?: string;
  capital?: string;
  switchCountry?(code: string): void;
};

const NavBar = (props: Props) => {
  const \[selectedOption, setSelectedOption\] = React.useState("BG");

  // Dropdown subcomponent that's just a styled, state-aware <select>
  function Dropdown() {
    return (
      <select
        onChange={handleChange}
        className="cursor-pointer"
        name="country"
        id="countries"
        value={selectedOption}
      >
        <option value="BG">Bulgaria</option>
        <option value="ES">Spain</option>
        <option value="JP">Japan</option>
      </select>
    );
  }

  // handle a country change
  function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
    event.preventDefault();
    setSelectedOption(event.target.value); // to reflect changed country in UI
    props.switchCountry(event.target.value); // callback
  }

  return (
    <nav className="sticky top-0 z-50 h-12 shadow-2xl  w-full bg-red-600">
      <ul className="list-none m-0 overflow-hidden p-0 fixed top-0 w-full flex justify-center">
        <li className="cursor-pointer">
          <div className="block py-3 text-center text-white hover:underline text-lg text-slate-50 ">
            <Link href="/">Home</Link>
          </div>
        </li>
        {props.country && (
          <li className="cursor-pointer">
            <div className="block py-3 px-4 text-center text-white no-underline text-lg text-black ">
              <Dropdown />
            </div>
          </li>
        )}

        {props.capital && (
          <li>
            <div className="block py-3 text-center text-white no-underline text-lg text-slate-50 ">
              @ {props.capital}
            </div>
          </li>
        )}
      </ul>
    </nav>
  );
};

export default NavBar;

Navbar.tsx

/\* WunderGraph stuff \*/
import { useRouter } from "next/router";
import { useQuery } from "../components/generated/nextjs";
/\* my types \*/
import { ArtistResult } from "../types/Artist";
/\* utility functions \*/
import parseVenue from "../utils/parse-venue";

type Props = {
  name: string;
  date: string;
  setList: string;
  artistId: string;
};

const ConcertCard = (props: Props) => {

  const router = useRouter();

  const artist = useQuery.ArtistBanner({
    input: { artistId: props.artistId },
  }) as ArtistResult;

  const banner = artist.result.data?.music\_lookup.artist?.theAudioDB?.banner;
  const artistName = artist.result.data?.music\_lookup.artist?.name;
  const venue = parseVenue(props.name);

  return (
    <div className="concert grid place-items-center mb-5 ">
      {banner ? (
        <>
          <img
            className="hover:shadow-\[20px\_5px\_0px\_5px\_rgb(220,38,38)\] hover:ring-1 ring-red-600 hover:scale-105 cursor-pointer"
            onClick={() => router.push({
              pathname: \`/concert/${props.artistId}\`
            })}
            src={banner}
            width="1000"
            height="185"
          />
        </>
      ) : (
        <>
          <img
            src={\`https://via.placeholder.com/1000x185.png\`}
            width="1000"
            height="185"
          />
        </>
      )}

      <p className="text-3xl mt-5"> {artistName}</p>
      <p className="text-xl mt-5"> {venue}</p>
      <p className=" font-medium  mb-5"> {props.date}</p>
      <hr />
    </div>
  );
};

export default ConcertCard;

ConcertCard.tsx

/\* NextJS stuff \*/
import { NextPage } from "next";
import { useRouter } from "next/router";
/\* WunderGraph stuff \*/
import { useQuery, withWunderGraph } from "../../components/generated/nextjs";
import NavBar from "../../components/NavBar";

/\* my types \*/
import { ArtistDetailResult } from "../../types/ArtistDetail";

type Props = {};

const Concert: NextPage = (props: Props) => {
  const router = useRouter();
  const { id } = router.query;
  const artistId: string = id as string;

  const result = useQuery.ArtistDetail({
    input: {
      mbid: artistId,
    },
  }).result as ArtistDetailResult;

  const data = result.data;

  const artistName = data?.music\_lookup?.artist?.name;
  const bannerImg = data?.music\_lookup?.artist?.theAudioDB?.banner;
  const thumbnailImg = data?.music\_lookup?.artist?.theAudioDB?.thumbnail;
  const bio = data?.music\_lookup?.artist?.theAudioDB?.biography;


return (
    <div className="flex grid h-full place-items-center bg-black text-zinc-100">
      <NavBar />
      {data ? (
        <div className="mt-1">
          <div className="banner mx-1 object-fill">
            <img className="" src={bannerImg} />
          </div>
          <div className="grid grid-cols-2 mt-2 mx-5">
            <div className="w-full mt-2">
              <img
                className="rounded-lg ring-2 shadow-\[10px\_10px\_0px\_5px\_rgb(220,38,38)\] hover:ring-1 ring-red-600 thumbnail"
                src={thumbnailImg}
                width="500px"
                height="500px"
              />
            </div>

            <div className="flex flex-col ml-8">
              <div className="mb-10 font-black text-7xl ">{artistName}</div>
              <div className="w-5/6 mx-2 font-mono break-normal line-clamp-4">
                {bio}
              </div>
            </div>
          </div>
        </div>
      ) : (
        <div className="grid h-screen place-items-center"> Loading...</div>
      )}
    </div>
  );
};


export default withWunderGraph(Concert);

[id].tsx

All done! Fire up localhost:3000 and you’ll see your app.

But before we sign off, here’s a very valid — and important — concern.

What if I’m Not Using Next.js/React?

WunderGraph still works as a straightforward API Gateway/BFF without the auto-generation of a frontend client for data fetching.

In this scenario, though, you won’t have access to the typesafe React hooks WunderGraph generates for you clientside, so you’re going to have to take on more of the concerns — implementing data fetching yourself, watching for type-safety, and making the internal GET/POST calls manually.

Using default WunderGraph configs, each operation (.graphql file) you have, is exposed as JSON-RPC (HTTP) at:

http://localhost:9991/app/main/operations/[operation\_name\]

So your data fetching is going to look something like this:

image.png

Where Weather.graphql is the filename of your operation.

Achivement Unlocked: Composability for Data

With WunderGraph being part of your tooling to bring together all your APIs, databases, and microservices — whether that’s as a BFF, an API Gateway, a View-Aggregator that only ever mirrors read-only data, or whatever — you get all the benefits of UI composability, in the realm of data.

  1. Progressive enhancement: revisit code anytime to flesh things out, or add new parts as business needs grow.

2. Flexibility: Swap out parts as needed — so your tech stack doesn’t calcify.

  1. Improved end-to-end developer experience:

  2. Single source of truth (GraphQL layer) for all data.

  3. A perfectly-molded client via code generation means every team knows exactly what they can or cannot do with the data (via autocompletion in your IDE) when writing operations as GraphQL queries or mutations, allowing you to craft the exact experience you want for your users without trial & error.
  4. Paired with Next.js, you can have queries ready to go in your <Suspense> boundaries so you know exactly what is rendered within each, and exactly which queries it runs under the hood. That knowledge leads to better patching and optimization because you’d know exactly where any gotchas or bottlenecks would be.

In terms of modern, serverless web development, WunderGraph can run on anything that can run Docker, so integration into your tech stack is seamless.

That’s WunderGraph’s powerplay. Composability for all dependencies, allowing you to build modular, data-intensive experiences for the modern web without compromising on developer experience.