Руководство по составлению и интеграции API вместе
18 декабря 2022 г.
Объединение двух API для приложения, которое показывает самые масштабные исторические концерты по столице страны.
С помощью React/Next.js фундаментальная проблема, которую вы решаете, заключается в превращении некоторого понятия «состояние» в DOM с упором на возможность компоновки — использование меньших вещей для создания больших вещей.
Поздравляем, вы нашли последнего босса веб-разработки: создание многоразовых «блоков LEGO» из компонентов, которые можно масштабировать бесконечно.
Если вы не использовали React/Next.js для этой битвы с самого начала, в какой-то момент вы неизбежно закончите тем, что повторно реализуете гораздо худший, специальный «React» из jQuery — и нести ответственность за его обслуживание.
Но создание составных пользовательских интерфейсов — это только полдела. Самый масштабируемый пользовательский интерфейс в мире был бы ничем без отображаемых данных. Итак, вторая половина: работа с API, базами данных и микросервисами.
Если мы хотим полностью использовать масштабируемые модульные веб-приложения, мы не можем забывать о компонуемости и в этой проблемной области.
Здесь может помочь WunderGraph — платформа для разработки API с открытым исходным кодом. В ментальной модели React вы уже привыкли перечислять все свои зависимости в файле package.json
и позволять менеджерам пакетов делать все остальное, когда вы npm install && npm запустить
проект.
WunderGraph позволяет вам сохранить эту интуицию и делать то же самое с вашими источниками данных:
- Явно назовите нужные вам API, микросервисы и базы данных в формате конфигурации как кода, а затем
2. WunderGraph генерирует код на стороне клиента, который предоставляет вам первоклассный безопасный доступ (через перехватчики Next.js/React) ко всем этим источникам данных во время работы над внешним интерфейсом.
В этом руководстве мы немного рискнем и объединим два совершенно разных API в приложении Next.js, чтобы показать, что WunderGraph (и никакие другие зависимости) работает вместе с вашим интерфейсом в качестве независимого сервера. API Gateway/BFF, вы могли бы, по сути, написать интерфейсный код для несколько REST, GraphQL, MySQL, Postgres, DBaaS, таких как Fauna, MongoDB и т. д., как если бы они были единым монолитом.
Прежде чем мы начнем, давайте быстро TL;DR концепцию:
Что вообще означает «компонуемость» для API?
Сейчас ваше взаимодействие с источниками данных полностью запрограммировано. Вы пишете код для выполнения вызовов конечной точки API или базы данных (с учетными данными в файле .env), а затем пишете дополнительный код. На этот раз асинхронный шаблон/клей для управления возвращаемыми данными.< /p>
Что не так с этой картинкой?
Сам код правильный; «неправильно» то, что теперь вы связываете зависимость со своим кодом, а не с файлами конфигурации.
Конечно, этот API погоды не является axios или react-dom (пакеты библиотеки/фреймворка), но, тем не менее, является зависимостью, и теперь сторонний API, который всегда отражает только временные данные, был передан вашему репозиторию, стал частью вашего основного бизнеса, и теперь вы будете поддерживать его на протяжении всего срока его службы.
Придерживаясь аналогии с Lego: это похоже на склеивание ваших наборов. Добро пожаловать в раздутые, плохо читаемые и плохо поддерживаемые кодовые базы с жесткими ограничениями.
Вы будете выполнять описанное выше раз десять в современных средних и крупных приложениях, которые разделены на множество микросервисов со сложными взаимодействиями и отдельными командами для каждого, которые могут наступать друг другу на пятки.
Это даже не считая всех JOIN, которые вы будете выполнять в этих многочисленных сервисах/API, чтобы получить нужные вам данные.
Итак, как может выглядеть масштабируемый подход к компоновке API без ущерба для удобства разработчиков?
- Он должен поддерживать различные данные, не нуждаясь в отдельном клиенте для каждого типа, и нам нужно будет иметь возможность явно определять эти зависимости данных вне нашего кода приложения — возможно, в файле конфигурации.
2. Это должно позволить нам добавлять дополнительные источники данных по мере необходимости, удалять устаревшие/мертвые источники из массива зависимостей и автоматически обновлять клиент, чтобы отразить эти изменения.
3. Это должно позволять нам объединять данные из нескольких источников (API, базы данных, федерации Apollo, микросервисы и т. д.), чтобы нам вообще не приходилось выполнять JOIN в коде на внешнем интерфейсе.
Как оказалось, это именно то, что позволяет WunderGraph благодаря сочетанию архитектур API Gateway и BFF.
Если вы явно указываете свои зависимости API в конфигурации WunderGraph, как показано справа, здесь:
Концептуально, в чем разница? Их нет; оба являются конфигурационными файлами со списком вещей, которые нужны вашему приложению. Конечно, вы пишете немного больше кода справа, но это просто то, что React/Next.js делает для вас под капотом слева.
WunderGraph анализирует и объединяет эти источники данных (а не только конечные точки) в виртуальный граф с пространством имен и строит на его основе схему.
Теперь вам больше не нужно:
- Насколько по-разному работают ваши зависимости данных.
2. Доставка любых сторонних клиентов на внешний интерфейс для поддержки этих различных источников данных.
3. Как ваши команды должны общаться между доменами.
Потому что теперь у вас уже есть все зависимости данных в виде канонического уровня, единого источника достоверности — GraphQL
Как вы могли догадаться, дальше нужно просто написать операции (запросы/мутации GraphQL, которые WunderGraph предоставляет вам для автозаполнения в вашей IDE), чтобы получить нужные данные из этого стандартизированного слоя данных.
Они компилируются в собственный клиент во время сборки, сохраняются и предоставляются с помощью JSON-RPC (HTTP).
Все DevEx выигрывают от использования GraphQL, фактически не имея общедоступной конечной точки GraphQL, поэтому на стороне клиента не возникает проблем с безопасностью/кэшированием/размером пакета.
Наконец, в своем внешнем коде вы используете сгенерированные клиентские обработчики извлечения данных с безопасным типом.
Четкий, интуитивно понятный и удобный в обслуживании.
Конечный результат? Парадигма контейнеризации/менеджера пакетов, подобная Docker или NPM, но для источников данных. Со всеми вытекающими отсюда преимуществами:
* API, базы данных и микросервисы превратились в модульные сборные кубики Lego, как и ваши компоненты пользовательского интерфейса.
* Больше не нужно раздувать код для JOIN и фильтров во внешнем интерфейсе, значительно улучшена читабельность кода и больше нет условий гонки при попытке выполнить транзакционную обработку в микросервисах.
* Поскольку конечной «конечной точкой» является JSON-RPC поверх старого доброго HTTP, кэширование, разрешения, аутентификация и безопасность — все становится решенными проблемами независимо от типа источника данных.
Но зачем придерживаться теории? Давайте начнем!
Приключение в подходе к API как к LEGO
Возможность создавать и объединять источники данных, как если бы вы это делали в библиотеках, может привести вас к очень интересным вещам и идеям, которые вам никогда не подскажет ни один заурядный общедоступный API.
Скажем, например, что, если вы хотите узнать, какие крупнейшие музыкальные события — концерты, творческие вечера, фестивали и т. д. — исторически проводились в столице данной страны?
Там буквально нет такого API. Вы можете построить первый. Весь мир — твоя устрица!
Шаг 1. Выбор данных
Итак, здесь мы будем использовать два API: API стран и Агрегатор MusicBrainz. Не стесняйтесь поиграть с Insomnia/Postman/Playgrounds и понять, какие данные вы можете обоснованно запрашивать с помощью этих API. Вероятно, вы найдете массу дополнительных творческих вариантов использования.
Шаг 2. Быстрый запуск приложения WunderGraph + Next.js
Когда вы будете готовы двигаться дальше, используйте начальный шаблон в репозитории WunderGraph для приложения Next.js, которое использует первый в качестве шлюза BFF/API.
npx -y @wundergraph/wunderctl init — template nextjs-starter -o wg-concerts
Это создаст новый каталог проекта с именем wg-concerts (или именем папки по вашему выбору), запустит WunderGraph (на локальном хосте:9991) и сервер Next.js (на локальном хосте:3000). ), используя пакет npm-run-all; в частности, используя псевдоним run-p для параллельного запуска обоих.
Шаг 3. Концерты Capital – кросс-присоединение API без кода.
Вот суть этого руководства. Я много говорил о том, что объединение данных из разных источников является серьезной проблемой, когда выполняется в коде, и теперь вы воочию увидите, как WunderGraph упрощает их.
Вы можете объединить 2 ответа API — в нашем случае получить столицу страны, а затем использовать эту информацию для запроса концертов, которые там проходили, — вот так:
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
}
}
}
}
}
}
}
Представьте себе реализацию этого запроса в JavaScript. Действительно, жуткий сезон.
* Директива @internal
для args означает, что, хотя этот аргумент технически является «входом», он будет найден только внутри этого запроса и не должен предоставляться при вызове этой операции.
* Директива @export
работает рука об руку с @internal
и тем, что вы экспортируете (или псевдонимом — это то, что 'as ключевое слово code>', должно иметь то же имя и тип, что и аргумент, который вы пометили как внутренний.
* _join
означает фактическую операцию JOIN:
* Как вы можете заметить, ввод (запрос) этого второго запроса использует тот же аргумент, который мы пометили как внутренний на верхнем уровне этого запроса GraphQL.
* Несмотря на то, что это необязательно, мы используем директиву @transform
(а затем поле 'get
', указывающее на точную структуру данных, которая нам нужна) для создания псевдонима ответа 2-й запрос в «концерты», потому что любой дополнительный запрос, к которому мы присоединяемся, конечно, добавит еще одну сложную, надоедливо вложенную структуру, и мы хотим упростить и сделать ее как можно более читабельной.
* Мы также (необязательно) включаем поле relationships
для каждого концерта, чтобы получить здесь mbid
(внутренний идентификатор MusicBrainz артиста, участвующего в этом концерте), потому что мы по-прежнему хотите запросить объект Artist отдельно позже (для баннеров, эскизов, биографий и т. д. Опять же, необязательно).
Шаг 4. Получение сведений об исполнителе
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
}
}
}
}
Говоря об опыте разработчиков… вот небольшая загвоздка с переменной artistId
, имеющей тип MBID!
, а не String!
. Благодаря WunderGraph вы получите подсказку кода для этого в своей IDE!
Наша вторая и третья операции, соответственно, заключаются в том, чтобы получить изображение баннера артиста размером 1000x185 пикселей (из AudioDB) через его идентификатор MusicBrainz, а затем миниатюру/биографии.
Это просто для того, чтобы украсить наш пользовательский интерфейс, и вы можете пропустить эти запросы, если вам нужна только информация о концерте и ничего больше (возможно, потому, что ваш вариант использования вообще не нуждается в пользовательском интерфейсе).
Шаг 5. Отображение наших данных во внешнем интерфейсе
Мы на финишной прямой! Давайте не будем слишком заморачиваться, просто каждый концерт сопоставлен с компонентами <ConcertCard>
, а <NavBar>
с <Dropdown>
выбрать страну для проведения концертов в ее столице. Ну и, конечно же, TailwindCSS для стилей.
/* 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
Все сделано! Запустите localhost:3000
, и вы увидите свое приложение.
Но прежде чем мы закончим, у нас есть очень важное и важное опасение.
Что делать, если я не использую Next.js/React?
WunderGraph по-прежнему работает как простой шлюз API/BFF без автоматического создания клиентского интерфейса для выборки данных.
В этом сценарии, однако, у вас не будет доступа к хукам React с безопасным типом, которые WunderGraph генерирует для вашей клиентской стороны, поэтому вам придется взять на себя больше забот — реализовать выборку данных самостоятельно, следить за безопасностью типов и создавать внутренние вызовы GET/POST вручную.
Используя конфигурации WunderGraph по умолчанию, каждая ваша операция (файл .graphql) отображается как JSON-RPC (HTTP) по адресу:
http://localhost:9991/app/main/operations/ [имя_операции]
Таким образом, выборка ваших данных будет выглядеть примерно так:
Где Weather.graphql — это имя файла вашей операции.
Разблокировано достижение: компонуемость данных
WunderGraph является частью вашего инструментария для объединения всех ваших API, баз данных и микросервисов — будь то BFF, API Gateway, View-Aggregator, который всегда отражает данные только для чтения, или что-то еще — вы получаете все преимущества компоновки пользовательского интерфейса в области данных.
- Постепенное улучшение: пересматривайте код в любое время, чтобы конкретизировать его, или добавляйте новые части по мере роста потребностей бизнеса.
2. Гибкость: заменяйте детали по мере необходимости, чтобы ваш технический стек не кальцинировался.
-
Улучшенный сквозной опыт для разработчиков:
-
Единый источник достоверности (уровень GraphQL) для всех данных.
* Идеально сформированный клиент с помощью генерации кода означает, что каждая команда точно знает, что они могут или не могут делать с данными (через автозаполнение в вашей IDE) при написании операций в виде запросов или мутаций GraphQL, что позволяет вам создавать именно тот опыт, который вы хотите для своих пользователей, без пробной версии & ошибка.
* В сочетании с Next.js вы можете иметь готовые запросы к работе в границах <Suspense>
, чтобы вы точно знали, что отображается в каждом из них, и какие именно запросы выполняются под капотом. Это знание позволяет лучше устанавливать исправления и оптимизировать, потому что вы будете точно знать, где могут быть какие-либо проблемы или узкие места.
Что касается современной бессерверной веб-разработки, WunderGraph может работать на всем, что может работать с Docker, поэтому интеграция в ваш технический стек будет бесшовной.
Это мощная игра WunderGraph. Компонуемость для всех зависимостей, позволяющая создавать модульные приложения с интенсивным использованием данных для современного Интернета без ущерба для удобства разработчиков.
Также опубликовано здесь
Фото Брента Нинабера на Unsplash
Оригинал