Как архитектурно передавать на фронт сложную структуру с вложенными полями и массивами
Что проверяют
Разделяете ли вы модель хранения и контракт API и знаете ли подводные камни encoding/json. Вопрос архитектурный, поэтому одного json.NewEncoder(w).Encode(user) мало. Следом спрашивают, как менять такую структуру, не ломая фронтенд, и когда стоит взять GraphQL или gRPC.
Как отвечать
Каркас ответа:
- отдельные DTO: структуры ответа описываются отдельно от моделей базы и доменных сущностей. Так наружу не уйдут служебные поля, а схему таблицы можно менять независимо. Сборка DTO лежит в хендлере или отдельном маппере;
- явный контракт: схема в OpenAPI и генерация кода (
oapi-codegen) для Go и типов для TypeScript. Тогда расхождение ловится на генерации, а не в проде; - теги и нули:
json:"items",omitemptyдля необязательных полей, с Go 1.24 естьomitzero, который понимает нулевые структуры иtime.Time. Дляint64больше 2^53 нуженjson:",string", иначе JavaScript потеряет точность. Деньги передают в минимальных единицах или строкой, но неfloat64; - форма данных: глубоко вложенные массивы пагинируются отдельно, повторяющиеся сущности можно передать справочником по
id, а не копировать в каждый элемент; - сборка без N+1: вложенные коллекции из базы подтягиваются одним запросом с
INили агрегацией черезjson_agg, а не циклом по родителям; - эволюция: поля только добавляются, удаление и переименование идут через новую версию или период совместимости.
Если разным экранам нужны разные срезы одной большой структуры, уместно упомянуть GraphQL (gqlgen) или BFF, а для обмена между сервисами gRPC с protobuf.
Где спотыкаются
Не знают, что nil слайс сериализуется в null, а не в []. Фронтенд падает на data.items.map, поэтому пустые коллекции инициализируют явно: make([]Item, 0).
Отдают модель базы напрямую. Наружу уезжает хэш пароля, а sql.NullString превращается в объект {"String": "...", "Valid": true}.
Отвечают только про сериализацию и молчат про контракт, версионирование и размер ответа. Для архитектурного вопроса это половина ответа.