Представьте: вы переименовали поле title в heading — в базе и на сервере. Сервер работает, тесты бэкенда зелёные, а у пользователей в приложении пропали заголовки заметок: фронтенд по-прежнему читает note.title. Ошибку нашёл не компилятор, а пользователь. В этом уроке разберём, как tRPC превращает такие ошибки из «нашёл пользователь» в «красная волна в редакторе» — и при этом не требует ни файла со схемой, ни генерации кода.

Проблема: граница между фронтендом и бэкендом

Фронтенд и бэкенд общаются по сети, а по сети ходит просто текст — JSON. TypeScript про этот JSON ничего не знает: на этапе компиляции он не видит сервера, а когда сервер работает, никаких типов уже нет. Поэтому в обычном REST-приложении тип ответа мы просто обещаем сами себе:

ts
// REST: the response type is only a promise
type Note = { id: string; title: string };

const res = await fetch('/api/notes');
const notes = (await res.json()) as Note[]; // nobody checks this

console.log(notes[0].title); // undefined, if the server renamed the field

Запись as Note[] — не проверка, а клятва: «я знаю, что сервер пришлёт именно такое». Если сервер изменится, TypeScript этого не заметит, а notes[0].title во время работы окажется undefined.

Чтобы закрыть этот разрыв, команды придумали два популярных решения:

  • OpenAPI и кодогенерация (REST). API описывают в файле openapi.yaml, а специальная программа — генератор — создаёт по нему типы для клиента.
  • Схема GraphQL и кодогенерация. Схема — тот самый контракт из модуля про GraphQL; типы клиента снова генерируются из неё.

У обоих подходов есть цена: кроме кода сервера нужно писать и поддерживать ещё один артефакт (спецификацию или схему) и не забывать запускать генератор. А что, если контрактом станет сам код сервера?

Идея RPC: вызвать функцию на другом компьютере

RPC (Remote Procedure Call, «удалённый вызов процедуры») — старая идея: вызвать функцию, которая живёт на другом компьютере, так, будто она лежит рядом, в вашем коде.

Аналогия: в чайхане вы не пишете повару письмо с адресом и описанием, как заварить чай. Вы говорите официанту «один чай, пожалуйста» — и получаете стакан чая. Официант — это RPC-библиотека: она берёт ваш «вызов функции», доносит его до кухни (сервера) и приносит результат обратно.

REST устроен иначе: он строится вокруг ресурсов и HTTP-методов (GET /notes/n2, POST /notes), то есть вокруг существительных. RPC строится вокруг действий, то есть глаголов: note.byId, note.create. tRPC — это TypeScript RPC: вы вызываете процедуры сервера как обычные функции, а TypeScript проверяет эти вызовы.

Вызов выглядит как обычная функция, а по сети едет обычный HTTP-запрос
Вызов выглядит как обычная функция, а по сети едет обычный HTTP-запрос

Никакой магии тут нет: под капотом работают привычные HTTP и JSON. Вот что реально уходит по сети, когда клиент вызывает trpc.note.byId.query({ id: 'n2' }):

http
GET /api/trpc/note.byId?batch=1&input={"0":{"id":"n2"}} HTTP/1.1
Host: localhost:3000

И ответ сервера:

http
HTTP/1.1 200 OK
Content-Type: application/json