Изоморфные API-интерфейсы TypeScript меняют правила игры, стирая грань между клиентом и сервером
27 января 2023 г.Разработка полного стека только что вышла на совершенно новый уровень производительности. Изоморфные API-интерфейсы TypeScript, как я их называю, стирают границы между клиентом и сервером. Без какого-либо этапа компиляции разработчик получает немедленную обратную связь, когда вносит изменения в API. Вы можете легко переключаться между клиентским и серверным кодом, потому что это один и тот же код.
Вот короткое видео, иллюстрирующее этот опыт:
https://www.youtube.com/watch?v=CrCdhWti -Fw&ab_channel=WunderGraph&embedable=true
В видео показано определение мутации API на левой вкладке и реализация клиента для вызова мутации на правой вкладке. Когда мы меняем имя поля ввода или тип мутации, клиентский код немедленно отражает изменения и показывает нам ошибки в IDE. Этот немедленный цикл обратной связи полностью меняет правила разработки.
Давайте поговорим об истории API-интерфейсов Isomorphic TypeScript, о том, как они работают, о преимуществах и недостатках, а также о том, как вы можете начать с ними работать.
Изоморфные API-интерфейсы TypeScript: что именно означает этот термин и откуда он взялся?
Существует несколько фреймворков, которые позволяют вам определять API на TypeScript и совместно использовать код между клиентом и сервером, наиболее популярным из которых является trpc а> .
Существуют разные подходы к реализации такого рода цикла обратной связи, генерации кода и вывода типа. Мы поговорим о различиях, плюсах и минусах каждого подхода. Вы увидите, что вывод типов намного лучше во время разработки, но имеет недостатки, когда дело доходит до совместного использования типов в репозиториях.
Термин «изоморфные API-интерфейсы TypeScript» вдохновлен сообществом JavaScript и React.js. В сообществе React термин «изоморфный» или «универсальный» используется для описания приложения React, которое использует один и тот же код для рендеринга на сервере и на клиенте, что, я думаю, очень похоже на то, что мы делаем здесь с API. .
Изоморфные API-интерфейсы TypeScript позволяют вам определить контракт API на сервере и вывести из него клиентский код посредством вывода типа. Следовательно, нам не нужно проходить этап генерации кода, чтобы получить типобезопасный клиент; вместо этого мы используем компилятор TypeScript для вывода клиентского кода непосредственно во время разработки.
Магия API-интерфейсов Isomorphic TypeScript
Давайте посмотрим, как работают API-интерфейсы Isomorphic TypeScript. Как можно использовать один и тот же код между клиентом и сервером? Разве это не означает, что клиенту придется импортировать серверный код?
Магия API-интерфейсов Isomorphic TypeScript заключается в операторе import type
TypeScript. TypeScript 3.8 представил импорт только для текста и Экспорт .
Импорт и экспорт только типа — это новая форма импорта и экспорта. Их можно использовать для импорта или экспорта типов из модуля без импорта или экспорта каких-либо значений.
Это означает, что мы можем импортировать типы из модуля; и, следовательно, обмениваться типами между клиентом и сервером без импорта самого кода сервера. Это важнейшая часть API-интерфейсов Isomorphic TypeScript.
Теперь давайте углубимся в то, как мы можем применить эти знания на практике.
1. Определите контракт API на сервере
// .wundergraph/operations/users/get.ts
export default createOperation.query({
input: z.object({
id: z.string(),
}),
handler: async ({input}) => {
return {
id: input.id,
name: 'Jens',
bio: 'Founder of WunderGraph',
};
},
});
WunderGraph использует маршрутизацию на основе файлов, аналогичную Next.js. Создавая файл get.ts
в папке .wundergraph/operations/users
, мы регистрируем эту операцию в папке /users/get
. маршрут.
Теперь мы могли бы вызвать эту операцию с помощью curl:
curl http://localhost:9991/operations/users/get?id=123
Это здорово, если мы не используем TypeScript, но весь смысл этого поста в том, чтобы использовать TypeScript. Итак, давайте посмотрим, как определяется createOperation.query
.
2. Предоставление типов из определения API
//
const createQuery = <IC extends InternalClient, UserRole extends string>() => <I extends z.AnyZodObject, R>(
{
input,
handler,
live,
requireAuthentication = false,
internal = false,
rbac,
}: {
input?: I;
handler: (ctx: HandlerContext<I, IC, UserRole>) => Promise<R>;
live?: LiveQueryConfig;
} & BaseOperationConfiguration<UserRole>): NodeJSOperation<z.infer<I>, R, 'query', IC, UserRole> => {
return {
type: 'query',
inputSchema: input,
queryHandler: handler,
internal: internal || false,
requireAuthentication: requireAuthentication,
rbac: {
denyMatchAll: rbac?.denyMatchAll || [],
denyMatchAny: rbac?.denyMatchAny || [],
requireMatchAll: rbac?.requireMatchAll || [],
requireMatchAny: rbac?.requireMatchAny || [],
},
liveQuery: {
enable: live?.enable || true,
pollingIntervalSeconds: live?.pollingIntervalSeconds || 5,
},
};
};
export type HandlerContext<I, IC extends InternalClient, Role extends string> = I extends z.AnyZodObject
? _HandlerContext<z.infer<I>, IC, Role>
: Omit<_HandlerContext<never, IC, Role>, 'input'>;
export type NodeJSOperation<Input, Response, OperationType extends OperationTypes, IC extends InternalClient, UserRole extends string> = {
type: OperationType;
inputSchema?: z.ZodObject<any>;
queryHandler?: (ctx: HandlerContext<Input, IC, UserRole>) => Promise<Response>;
mutationHandler?: (ctx: HandlerContext<Input, IC, UserRole>) => Promise<Response>;
subscriptionHandler?: SubscriptionHandler<Input, Response, IC, UserRole>;
requireAuthentication?: boolean;
internal: boolean;
liveQuery: {
enable: boolean;
pollingIntervalSeconds: number;
};
rbac: {
requireMatchAll: string[];
requireMatchAny: string[];
denyMatchAll: string[];
denyMatchAny: string[];
};
};
Нужно распаковать много кода, поэтому давайте рассмотрим его шаг за шагом.
Функция createQuery
— это фабрика, которая возвращает функцию createOperation.query
. Помещая фактическую функцию в фабрику, мы можем передавать в функцию универсальные типы, такие как InternalClient (IC) и UserRole. Это позволяет нам внедрять сгенерированные типы, не усложняя API для пользователя.
Важно отметить два общих аргумента функции createQuery
: I extends z.AnyZodObject, R
. I
— тип ввода, а R
— тип ответа.
Пользователь может передать входное определение в функцию createOperation.query
, как показано на шаге 1. Как только это значение будет передано в функцию createQuery
, I code> общий тип выводится из входного определения. Это позволяет следующее:
- Мы можем использовать
z.infer<I>
, чтобы вывести тип ввода из определения ввода
2. Этот предполагаемый тип используется, чтобы сделать функцию handler
безопасной для типов
3. Кроме того, мы устанавливаем предполагаемый тип как общий тип Input
типа NodeJSOperation
Позже мы можем использовать import type
для импорта типа Input
из NodeJSOperation
.
Чего не хватает, так это типа Response
R
, который на самом деле менее сложен, чем тип Input
. Вторым общим аргументом функции createQuery
является R
(тип ответа).
Если вы внимательно посмотрите на определение аргумента handler
, вы увидите, что это функция, которая возвращает Promise<R>
. Таким образом, все, что мы возвращаем из функции handler
, относится к типу Response
. Мы просто передаем R
в качестве второго универсального аргумента типу NodeJSOperation
, и все готово.
Теперь у нас есть тип NodeJSOperation
с двумя общими аргументами: Input
и Response
. Остальная часть кода гарантирует, что внутренний клиент и пользовательский объект являются типобезопасными, но эргономичными; например, пропуская свойство input
, если пользователь не передал определение ввода.
3. Предоставление контракта API на клиенте
Наконец, нам нужен способ импорта контракта API на клиенте. Мы немного используем генерацию кода при создании моделей для клиента, чтобы сделать это приятным для разработчиков.
Имейте в виду, что тип NodeJSOperation
является универсальным с типами Input
и Response
в качестве универсальных аргументов, поэтому нам нужен способ извлеките их, чтобы сделать наши клиентские модели типобезопасными.
Вот вспомогательная функция для достижения этого с использованием ключевого слова infer
:
export type ExtractInput<B> = B extends NodeJSOperation<infer T, any, any, any, any> ? T : never;
export type ExtractResponse<B> = B extends NodeJSOperation<any, infer T, any, any, any> ? T : never;
Ключевое слово infer
позволяет нам извлечь общий аргумент из общего в определенной позиции. В данном случае мы извлекаем типы Input
и Response
из типа NodeJSOperation
.
Вот выдержка из файла моделей клиентов, в котором используется эта вспомогательная функция:
import type function_UsersGet from "../operations/users/get";
import type { ExtractInput, ExtractResponse } from "@wundergraph/sdk/operations";
export type UsersGetInput = ExtractInput<typeof function_UsersGet>;
export type UsersGetResponseData = ExtractResponse<typeof function_UsersGet>;
export interface UsersGetResponse {
data?: UsersGetResponseData;
errors?: ReadonlyArray<GraphQLError>;
}
Обратите внимание, что мы импортируем только тип function_UsersGet
из файла операций, а не фактическую реализацию. Во время компиляции все импортируемые типы удаляются.
Здесь есть еще один самородок, который вы легко можете пропустить: сгенерированные клиентские модели экспортируют тип UsersGetInput
, который выводится из типа function_UsersGet
, который является экспортом типа < code>NodeJSOperation тип, который выводит свой тип Input
из функции createOperation.query
.
Это означает, что здесь происходит цепочка вывода типов. Это не только делает клиентские модели типобезопасными, но также включает еще одну очень мощную функцию, которую я считаю очень важным выделить.
<цитата>Вывод клиентов из определения контракта API сервера позволяет провести рефакторинг контракта API сервера без нарушения работы клиента.
Давайте добавим клиентский код, чтобы проиллюстрировать это:
import { useQuery, withWunderGraph } from '../../components/generated/nextjs';
const Users = () => {
const { data } = useQuery({
operationName: 'users/get',
input: {
id: '1',
},
});
return (
<div style={{ color: 'white' }}>
<div>{data?.id}</div>
<div>{data?.name}</div>
<div>{data?.bio}</div>
</div>
);
};
export default withWunderGraph(Users);
Это сгенерированный клиентский код для запроса users/get
. Если мы устанавливаем для operationName
значение users/get
(что, кстати, является строкой с безопасным типом), мы вынуждены передать input
объект, соответствующий типу UsersGetInput
.
Если теперь мы рефакторим свойство id
в userId
в контракте API сервера, клиентский код также будет преобразован в userId
, потому что < code>UsersGetInput выводится из контракта API сервера. Если вместо этого мы изменим тип свойства id
с string
на number
, IDE немедленно покажет ошибку, поскольку предполагаемый тип поля id
(число) больше не будет соответствовать строке.
Такая немедленная петля обратной связи делает этот подход таким мощным. Если вы ранее работали с API REST или GraphQL, вы знаете, что рефакторинг контракта API потребует гораздо больше шагов.
Доступны различные типы операций
WunderGraph поддерживает три разных типа операций TypeScript: запросы, изменения и подписки. Давайте посмотрим, как вы можете их определить.
Изоморфные API TypeScript: запросы
Выше мы видели операцию запроса, но я все же хочу перечислить здесь все три типа операций для полноты картины.
export default createOperation.query({
input: z.object({
id: z.string(),
}),
handler: async ({ input }) => {
return {
id: input.id,
name: 'Jens',
bio: 'Founder of WunderGraph',
};
},
});
Операция запроса будет зарегистрирована как обработчик запроса GET
на сервере. Задав определение input
, аргумент input
функции handler
будет типобезопасным. Кроме того, мы также создаем промежуточное ПО для проверки JSON-Schema для конечной точки.
Другие параметры, которые мы могли бы настроить, это rbac
для управления доступом на основе ролей; requireAuthentication
, чтобы требовать аутентификацию для конечной точки; live
для настройки оперативных запросов (включено по умолчанию); и internal
, чтобы сделать эту конечную точку доступной только для других операций, но не для клиента.
После включения аутентификации вы также сможете использовать свойство user
аргумента функции handler
:
export default createOperation.query({
requireAuthentication: true,
handler: async ({ input, user }) => {
return db.findUser(user.email);
},
});
Эта операция вернет объект пользователя из базы данных, используя в качестве идентификатора утверждение электронной почты из токена JWT или заголовка аутентификации файла cookie.
Изоморфные API TypeScript: мутации
Далее рассмотрим операцию мутации:
export default createOperation.mutation({
input: z.object({
id: z.number(),
name: z.string(),
bio: z.string(),
}),
handler: async ({ input }) => {
return {
...input,
};
},
});
Операция мутации будет зарегистрирована как обработчик запроса POST
на сервере. Мы принимаем три свойства и возвращаем их как есть. В обработчике мы обычно делаем здесь некоторые операции с базой данных.
Изоморфные API TypeScript: подписки
Наконец, давайте определим операцию подписки:
export default createOperation.subscription({
input: z.object({
id: z.string(),
}),
handler: async function* ({ input }) {
try {
// setup your subscription here, e.g. connect to a queue / stream
for (let i = 0; i < 10; i++) {
yield {
id: input.id,
name: 'Jens',
bio: 'Founder of WunderGraph',
time: new Date().toISOString(),
};
// let's fake some delay
await new Promise((resolve) => setTimeout(resolve, 1000));
}
} finally {
// finally gets called, when the client disconnects
// you can use it to clean up the queue / stream connection
console.log('client disconnected');
}
},
});
Операция подписки будет зарегистрирована как обработчик запроса GET
на сервере, который вы можете свернуть из командной строки или использовать через SSE (отправленные сервером события) от клиента, если вы добавляете параметр запроса ?wg_sse
.
Функция handler
немного отличается от двух других операций, потому что это функция асинхронного генератора.
Вместо того, чтобы возвращать одно значение, мы используем ключевое слово yield
для возврата потока значений. Асинхронные генераторы позволяют нам создавать потоки без обратных вызовов или обещаний.
Вы, возможно, задавались вопросом, как справиться с отключением клиента. Асинхронные генераторы позволяют создавать блоки try
/ finally
.
Как только клиент отключается от подписки, мы внутренне вызываем функцию return
генератора, которая вызовет блок finally
. Следовательно, вы можете запустить свою подписку и очистить ее в той же функции без использования обратных вызовов или промисов. Я думаю, что синтаксис асинхронного генератора — невероятно эргономичный способ создания асинхронных потоков данных.
Сокращение разрыва между операциями GraphQL, REST и TypeScript
Если вы знакомы с GraphQL, вы могли заметить, что в терминологии GraphQL и Isomorphic TypeScript API есть некоторое совпадение. Это не случайно.
Прежде всего, мы называем все операциями, что является общепринятым термином в GraphQL. Во-вторых, мы называем операции чтения запросами, операции записи — мутациями, а операции потоковой передачи — подписками.
Все это сделано намеренно, поскольку WunderGraph предлагает взаимодействие между API-интерфейсами GraphQL, REST и Isomorphic TypeScript. Вместо создания файла .wundergraph/operations/users/get.ts
мы могли бы также создать файл .wundergraph/operations/users/get.graphql
. р>
query UsersGet($id: String!) {
users_user(id: $id) {
id
name
bio
}
}
Учитывая, что мы добавили users
GraphQL API в наш Virtual Graph , этот запрос GraphQL можно было бы вызывать из клиента, как если бы это была операция TypeScript. Операции GraphQL и TypeScript отображаются для клиента одинаковым образом. Для клиента не имеет значения, написана ли реализация операции на TypeScript или GraphQL.
Вы можете смешивать и сочетать операции GraphQL и TypeScript по своему усмотрению. Если для вашего случая достаточно простого запроса GraphQL, вы можете использовать его. Если вам нужна более сложная логика, например сопоставление ответа или вызов нескольких API, вы можете использовать операцию TypeScript.
Кроме того, мы не просто регистрируем операции GraphQL и TypeScript в качестве конечных точек RPC, мы также позволяем вам использовать файловую систему, чтобы придать вашим операциям структуру. Поскольку мы также создаем коллекцию Postman для вашего API, вы можете легко поделиться этим API со своей командой или другой компанией.
Вызов других операций из операции
Важно отметить, что вы получаете безопасный для типов доступ к другим операциям из обработчиков операций TypeScript через объект контекста:
export default createOperation.query({
input: z.object({
code: z.string(),
}),
handler: async (ctx) => {
const country = await ctx.internalClient.queries.Country({
input: {
code: ctx.input.code,
},
});
const weather = await ctx.internalClient.queries.Weather({
input: {
city: country.data?.countries_country?.capital || '',
},
});
return {
country: country.data?.countries_country,
weather: weather.data?.weather_getCityByName?.weather,
};
},
});
В этом примере мы используем internalClient
для вызова операций Country
и Weather
и объединения результатов. Возможно, вы помните, как мы передавали IC extends InternalClient
в фабрику createOperation
в начале этой статьи. Вот как мы делаем internalClient
типобезопасным.
Уроки прошлого: резюме предыдущей работы в этой области
Мы не первые, кто использует эти методы, поэтому я думаю, что важно отдать должное там, где это необходимо, и объяснить, где и почему мы используем другой подход.
tRPC: платформа, с которой началась новая волна API-интерфейсов TypeScript
На данный момент tRPC, вероятно, является самой разрекламированной платформой в пространстве API TypeScript, поскольку она сделала использование подхода import type
популярным API с безопасным типом.
На днях я разговаривал с Alex/KATT, создателем tRPC, и он спросил меня, почему мы не используем tRPC напрямую в WunderGraph, поскольку мы могли бы использовать всю экосистему фреймворка. Это отличный вопрос, на который я хотел бы ответить здесь.
Во-первых, я считаю, что tRPC — отличная платформа, и я впечатлен работой, проделанной Алексом и сообществом. При этом было несколько вещей, которые не совсем соответствовали нашему варианту использования.
Одной из основных функций WunderGraph было и остается создание и интеграция API через виртуальный слой GraphQL . Я обсуждал это ранее, но для нас важно разрешить пользователям определять операции в папке .wundergraph/operations
путем создания файлов .GraphQL
. Именно так работает WunderGraph, и это отличный способ соединить разные API вместе.
Мы представили возможность создавать операции TypeScript, чтобы предоставить нашим пользователям больше гибкости. Операции на чистом TypeScript позволяют напрямую взаимодействовать с базой данных или объединять несколько других API таким образом, который невозможен с GraphQL. Например, возможности TypeScript по манипулированию данными и преобразованию намного мощнее, чем то, что вы можете делать с GraphQL, даже если вы вводите пользовательские директивы.
Для нас операции TypeScript являются расширением существующей функциональности WunderGraph. Для нас было важно убедиться, что нам не приходится иметь дело с двумя разными способами использования API. Таким образом, наследуя структуру, форму и параметры конфигурации слоя GraphQL, мы можем использовать операции TypeScript точно так же, как операции GraphQL. Единственное отличие состоит в том, что вместо вызова одного или нескольких API-интерфейсов GraphQL мы вызываем операцию TypeScript.
Кроме того, WunderGraph уже имеет множество существующих функций и промежуточных программ, таких как проверка JSON-Schema, аутентификация, авторизация и т. д., которые мы можем повторно использовать для операций TypeScript. Все это уже реализовано в Golang, выбранном нами языке для создания API-шлюза WunderGraph. Как вы, возможно, знаете, WunderGraph разделен на две части: шлюз API, написанный на Golang; и сервер WunderGraph, написанный на TypeScript, основанный на fastify. Таким образом, мы решили использовать существующий шлюз API и внедрить на его основе упрощенный сервер API TypeScript.
При этом я хотел бы выделить несколько моментов, в которых мы используем другой подход к tRPC.
tRPC не зависит от фреймворка, а WunderGraph придерживается мнения
Одна из замечательных особенностей tRPC заключается в том, что он не зависит ни от фреймворка, ни от транспортного уровня. Однако это может быть обоюдоострый меч: хотя и здорово, что вы можете использовать tRPC с любой инфраструктурой, которую вы хотите, есть недостаток, заключающийся в том, что пользователь вынужден принимать множество решений.
Например, руководство по использованию tRPC с подписками объясняет, как использовать tRPC с подписками WebSocket:
import { applyWSSHandler } from '@trpc/server/adapters/ws';
import ws from 'ws';
import { appRouter } from './routers/app';
import { createContext } from './trpc';
const wss = new ws.Server({
port: 3001,
});
const handler = applyWSSHandler({ wss, router: appRouter, createContext });
wss.on('connection', (ws) => {
console.log(`➕➕ Connection (${wss.clients.size})`);
ws.once('close', () => {
console.log(`➖➖ Connection (${wss.clients.size})`);
});
});
console.log('✅ WebSocket Server listening on ws://localhost:3001');
process.on('SIGTERM', () => {
console.log('SIGTERM');
handler.broadcastReconnectNotification();
wss.close();
});
В WunderGraph нет такого руководства, где вы должны самостоятельно обрабатывать соединения WebSocket. Наша цель с WunderGraph состоит в том, чтобы разработчик мог сосредоточиться на бизнес-логике своего API, что приводит нас к следующему пункту.
tRPC и WunderGraph: наблюдаемые и асинхронные генераторы
В то время как tRPC использует Observables для обработки подписок, WunderGraph использует асинхронные генераторы.
Вот пример API tRPC для подписок:
const ee = new EventEmitter();
const t = initTRPC.create();
export const appRouter = t.router({
onAdd: t.procedure.subscription(() => {
// `resolve()` is triggered for each client when they start subscribing `onAdd`
// return an `observable` with a callback which is triggered immediately
return observable<Post>((emit) => {
const onAdd = (data: Post) => {
// emit data to client
emit.next(data);
};
// trigger `onAdd()` when `add` is triggered in our event emitter
ee.on('add', onAdd);
// unsubscribe function when client disconnects or stops subscribing
return () => {
ee.off('add', onAdd);
};
});
}),
});
А вот аналог в WunderGraph:
// .wundergraph/operations/users/subscribe.ts
import {createOperation, z} from '../../generated/wundergraph.factory'
const ee = new EventEmitter();
export default createOperation.subscription({
handler: async function* () {
let resolve: (data: any) => void;
const listener = (data: any) => resolve(data);
try {
let promise = new Promise(res => resolve = res);
ee.on('event', listener);
while (true) {
yield await promise;
promise = new Promise(res => resolve = res);
}
} finally {
ee.off('event', listener);
}
}
})
Какая разница? Это может быть личным предпочтением, поскольку я в основном разрабатываю на Golang, но я думаю, что асинхронные генераторы легче читать, потому что поток более линейный. Вы можете более или менее читать код сверху вниз — точно так же, как он выполняется.
Наблюдаемые, с другой стороны, используют обратные вызовы и не так просты для чтения. Я предпочитаю регистрировать прослушиватель событий, а затем выдавать события вместо того, чтобы генерировать события и затем регистрировать обратный вызов.
tRPC и WunderGraph: код в качестве маршрутизатора и файловая система в качестве маршрутизатора
tRPC использует маршрутизатор на основе кода, а WunderGraph использует маршрутизатор на основе файловой системы. Использование файловой системы в качестве маршрутизатора имеет много преимуществ. Легче понять контекст и аргументацию кода, поскольку вы можете видеть структуру вашего API в файловой системе. Также проще ориентироваться, так как вы можете использовать свою IDE для перехода непосредственно к файлу, который хотите отредактировать. И последнее, но не менее важное: проще делиться кодом и повторно использовать его.
И наоборот, маршрутизатор на основе кода гораздо более гибок, поскольку вы не ограничены файловой системой.
tRPC по сравнению с WunderGraph: когда вы масштабируетесь не только на TypeScript
Удивительно, когда вы можете построить весь свой стек на TypeScript, но у этого подхода есть определенные ограничения. В конечном итоге вы столкнетесь с ситуацией, когда вы захотите написать службу на языке, отличном от языка TypeScript, или захотите интегрироваться со сторонними службами.
В этом случае вам придется вручную управлять зависимостями API, используя только TypeScript.
Вот где я считаю, что WunderGraph сияет. Вы можете начать с чистого подхода TypeScript, а затем постепенно переходить к более сложной настройке, интегрируя все больше и больше внутренних и внешних сервисов. Мы не просто думаем о первом дне, но также предлагаем решение, которое масштабируется за пределы небольшой команды, работающей над одной кодовой базой.
Будущее изоморфных API TypeScript
Тем не менее, я считаю, что у Isomorphic TypeScript API большое будущее, поскольку они предоставляют потрясающие возможности для разработчиков. В конце концов, именно поэтому мы добавили их в WunderGraph в первую очередь.
Я также рад поделиться некоторыми идеями, которые у нас есть для будущего Isomorphic TypeScript API. Текущий подход заключается в определении отдельных процедур/операций, которые не зависят друг от друга.
Что, если бы мы могли использовать шаблон, аналогичный GraphQL, где мы могли бы определять отношения между процедурами и разрешать их компоновку? Например, мы можем определить процедуру User
в корне, а затем вложить в нее процедуру Posts
.
Вот пример того, как это может выглядеть:
// .wundergraph/operations/users.ts
import {createOperation, z} from '../../generated/wundergraph.factory'
export default createOperation.query({
handler: async function (args) {
return {
id: 1,
name: 'John Doe',
}
}
})
// .wundergraph/operations/users/posts.ts
import {createOperation, z} from '../../../generated/wundergraph.factory'
import User from '../users'
export default createOperation.query({
input: User,
handler: async function (args) {
return fetchPosts(args.id);
}
})
Теперь мы можем запросить процедуру User
и получить процедуру Posts
как вложенное поле, указав поле posts
в операции.
import { useQuery, withWunderGraph } from '../../components/generated/nextjs';
const Users = () => {
const { data } = useQuery({
operationName: 'users/get',
input: {
id: '1',
},
include: {
posts: true,
},
});
return (
<div style={{ color: 'white' }}>
<div>{data?.id}</div>
<div>{data?.name}</div>
<div>{data?.bio}</div>
</div>
);
};
export default withWunderGraph(Users);
Я еще не совсем уверен в эргономике и деталях реализации этого подхода, но это позволило бы нам получить опыт, более похожий на GraphQL, и в то же время иметь возможность пользоваться преимуществами вывода типов.
Нужны ли нам наборы выбора на уровне поля? Или может быть достаточно вложенных процедур/преобразователей?
С другой стороны, отсутствие такой функциональности в конечном итоге приведет к большому дублированию. При масштабировании API RPC вы получите множество процедур, очень похожих друг на друга, но с небольшими отличиями, поскольку они решают несколько иной вариант использования.
Заключение
Надеюсь, вам понравилась эта статья, и вы узнали что-то новое о TypeScript и создании API. Мне не терпится увидеть, какое будущее ждет изоморфные API-интерфейсы TypeScript и как они будут развиваться. Я думаю, что этот новый стиль создания API сильно повлияет на то, как мы думаем о разработке полного стека в будущем.
Если вам интересно поиграть с самим собой, вы можете клонировать этот пример и попробуй. Я также подготовил GitPod, так что вы можете легко опробовать его в браузере .
Однако следует иметь в виду, что универсального решения не существует. API TypeScript RPC отлично подходят, когда и внешний, и внутренний интерфейс написаны на TypeScript. По мере расширения ваших команд и организаций вы можете перерасти этот подход и вам понадобится что-то более гибкое.
WunderGraph позволяет очень быстро работать над проектом в первые дни благодаря подходу, основанному исключительно на TypeScript. Как только вы найдете определенный продукт, подходящий для рынка, вы сможете постепенно переходить от подхода, основанного на чистом TypeScript, к более сложной настройке, интегрируя все больше и больше внутренних и внешних сервисов. Это то, что мы называем «от идеи до IPO». Фреймворк должен наилучшим образом поддерживать вас на разных этапах вашего проекта.
Подобно тому, как самолеты используют системы закрылков для адаптации к различным условиям полета, WunderGraph позволяет вам адаптироваться к различным этапам вашего проекта. Во время взлета вы можете использовать чистый подход TypeScript, чтобы быстро оторваться от земли. Как только вы окажетесь в воздухе, полные закрылки создадут слишком большое сопротивление и замедлят вас. Именно тогда вы сможете постепенно перейти к использованию виртуального Graph и разделить свои API на более мелкие службы.< /p>
В какой-то момент вы даже можете разрешить другим разработчикам и компаниям интегрироваться с вашей системой через API. Вот когда пригодится сгенерированная коллекция почтальонов для всех ваших операций. Ваши API не могут создавать ценности, если о них никто не знает.
Я хотел бы услышать ваши мысли по этой теме, поэтому не стесняйтесь обращаться ко мне в Twitter или присоединяйтесь к нашему Discord сервер, чтобы поговорить об этом.
Также опубликовано здесь
Оригинал