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:
- Explicitly name the APIs, microservices, and databases you need, in a configuration-as-code format, and then,
- 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.
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?
- 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.
- 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.
- 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:
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:
- How differently your data dependencies work under the hood.
- Shipping any third-party clients on the frontend to support those different data sources.
- 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 truth â GraphQL.
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).
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.
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 getmbid
(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:
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.
- 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.
- Improved end-to-end developer experience:
- Single source of truth (GraphQL layer) for all data.
- 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.
- 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.
Top comments (0)