visit
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:
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/, 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.
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.
Because now you already have all data dependencies as a canonical layer, a single source of truth — GraphQL.
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:
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.
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:
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).
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!
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={`//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.
//localhost:9991/app/main/operations/ [operation_name]
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.
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.
Also published
Photo by on