100+ API-ендпоінтів і нуль інтеграційних багів. Ось мій фреймворк
Як і більшість розробників, я досить довго страждав від інтеграційних помилок, які неминуче виникають на будь-якому більш-менш серйозному проєкті: бек змінив поле, додав нове, поміняв формат, а фронт дізнався про це вже після того, як щось зламалося.
Тож, деякий час тому я вирішив, що досить це терпіти, і розробив для свого проєкту власний фреймворк для контрактного тестування API. На практиці результат виявився навіть кращим, ніж я очікував: після його повноцінного впровадження кількість інтеграційних помилок між фронтом та беком скоротилася практично до нуля.
У цій статті я хочу поділитися цим фреймворком: як я його дизайнив, які задачі намагався вирішити, як організував схеми та контракти і чому в результаті він перетворився на дещо більше, ніж просто набір API тестів.
Стек у моєму випадку досить стандартний для сучасного frontend-проєкту: JavaScript, TypeScript, React та Zod. Але сама ідея не прив’язана до конкретної бібліотеки — Zod тут просто дуже зручний інструмент для її реалізації.
Перед тим як щось будувати, я визначив головну задачу фреймворку: автоматично перевіряти реальні API responses і гарантувати, що бек повертає саме те, що очікує фронт.
Це означає ловити зміни типів і форматів полів, зникнення обов’язкових полів, появу нових неочікуваних полів, порушення правил валідації та зміни сталих відповідей.
І саме з цієї задачі починається вся архітектура фреймворку.
QA Engineer vs. Software Developer
Перш ніж почати, хочу проговорити досить поширене, на мою думку, хибне уявлення про API contract testing: цим мають займатися розробники, а не QA.
Цей фреймворк може реалізувати та успішно використовувати і QA, і розробник. Для його базової імплементації не потрібне глибоке знання архітектури застосунку: QA може описати API контракти, побудувати схеми валідації та використовувати їх для автоматичної перевірки відповідей API. У такому варіанті фреймворк уже повністю виконує свою основну задачу — ловить порушення контракту між беком та його споживачем.
Але якщо цей фреймворк інтегрує розробник безпосередньо в архітектуру застосунку, з тих самих ресурсів можна отримати значно більше користі. Правильно побудовані схеми фактично стають єдиним джерелом істини одразу для кількох частин застосунку:
- TypeScript-типів;
- контрактних тестів;
- runtime валідації;
- валідації форм;
- правил та обмежень окремих полів;
- фронтенд-елементів, що залежать від цих обмежень, наприклад помилок, чи підрахунку символів;
- API-документації.
Тобто замість того, щоб незалежно описувати одну й ту саму структуру в типах, тестах, формах, UI та документації, ми описуємо її один раз і далі перевикористовуємо всюди, де вона потрібна.
Саме тому для QA цей фреймворк може бути потужним інструментом API контрактного тестування, а для розробника — ще й частиною архітектури самого застосунку.
Організація фреймворку
src/api/ ├── endpoints # Ресурси для реалізації взаємодії з API | └── product # Папка бізнес-сутності для групування едпоінтів | └── getProductById/ # Іменна папка ендпоінта | ├── getProductById.api.ts # Гола універсальна API function | ├── getProductById.hook.ts # UI-ready хук | └── getProductById.schemas.ts # Схеми валідації та типи └── schemas # Папка з описами бізнес-ентіті проекту ├── common.schemas.ts # Схеми та типи, що зустрічаються у всіх бізнес-ентіті └── product.schemas.ts # Схеми та типи окремої бізнес ентіті
Зверніть увагу на те, як організовані файли всередині кожного endpoint-а. На це є дві причини.
Перша — я будую цей framework як AI-native architecture і намагаюся зробити його якомога більш передбачуваним та зручним для AI-агентів. Чим чіткіше розділені обовʼязки і чим стабільніший неймінг паттерн, тим легше агенту зрозуміти структуру, знайти потрібний файл і коректно розширити існуючу реалізацію.
Друга причина — separation of concerns. Я навмисно розділяю реалізацію на декілька suffix-файлів, кожен із яких має одну чітко визначену відповідальність:
*.api.ts— максимально проста функція взаємодії з API. Вона не залежить від React і містить лише логіку, необхідну для виконання запиту та отримання відповіді. Завдяки цьому її можна використовувати швидко та незалежно: у React хуках, контрактних тестах, server-side коді або будь-якому іншому споживачі.*.hook.ts— React шар поверх API-функції. Тут уже можуть з’являтися loading та error стани, кеш, управління станами, трансформації та інша логіка, необхідна безпосередньо UI.*.schemas.ts— контракт конкретного endpoint-а: Zod-схеми для рантайм валідацій та пов’язані з ними TypeScript-типи.
Таке розділення тут особливо важливе для контрактних тестів. Тесту не потрібно піднімати React або тягнути за собою UI-логіку лише для того, щоб перевірити API. Він може напряму викликати функцію з *.api.ts і провалідувати отриманий response схемою з *.schemas.ts.
Naming conventions
Особисто я вважаю семантику однією з найбільш недооцінених частин розробки ПЗ і ставлюся до неї дуже серйозно. Правильна назва повинна одразу пояснювати, що за сутність перед вами, яку роль вона виконує та де використовується, ще до того, як ви відкрили файл. Це особливо важливо в AI-native репозиторіях: послідовний неймінг дозволяє AI за самими назвами точніше будувати граф залежностей, передбачати призначення сутностей і знаходити потрібний контекст. Тому неймінг конвенції у цьому фреймворку — це не косметика, а частина його архітектури.
Endpoints
Для endpoint-ів я використовую просте правило:
<operation><entity><qualifier?>
Спочатку вказується операція, потім business entity, а за необхідності — уточнення конкретного запиту.
Наприклад:
- getProductById
- createProduct
- modifyProduct
- deleteProduct
- getProducts
Цей самий неймінг використовується для папки ендпоінта та пов’язаних із ним файлів:
getProductById/ ├── getProductById.api.ts ├── getProductById.hook.ts └── getProductById.schemas.ts
Таким чином усі частини реалізації конкретного API запиту легко знайти за одним і тим самим іменем.
Схеми & Типи
Для схем і типів я використовую окремий набір стандартних суфіксів, які описують роль даних у конкретному request:
- QueryParams
- RequestPayload
- ResponsePayload
Якщо це Zod-схема, до назви просто додається Schema:
Наприклад:
- GetProductById_QueryParams
- CreateProduct_RequestPayload
- GetProductById_ResponsePayload
- GetProductById_ResponsePayloadSchema
У результаті імʼя саме описує структуру:
<endpoint>_<data role><Schema?>
Тобто, побачивши GetProductById_ResponsePayloadSchema, одразу зрозуміло три речі:
- до якого ендпоінтаа вона належить
- яку частину API контракту описує
- і що перед нами runtime схема, а не TypeScript тип
Починаємо роботу
Common schemas
Почнемо з найпростішого — файлу common.schemas.ts.
Його задача — зберігати невеликі, але багаторазово використовувані схеми, які зустрічаються практично в будь-якій бізнес-сутності. Це не окремі моделі застосунку, а будівельні блоки: ID, timestamp-и, tracker-поля, UUID, email та інші стандартизовані примітиви.
Наприклад, мінімальний common.schemas.ts може виглядати так:
import { z } from 'zod'
// Уніфікований тип ID для всього застосунку
export const Id_Schema = z.string().uuid()
// Уніфікований набір полів, що відстежують створення та модифікацію сутності
export const Tracker_Schema = z.object({
createdOn: z.string().datetime(),
modifiedOn: z.string().datetime(),
})
Надалі ці схеми не дублюються в кожній бізнес-сутності, а просто перевикористовуються через extend() або як окремі поля. Це дозволяє змінити правило в одному місці й автоматично поширити його на весь застосунок.
Business Entity
Тепер перейдемо до product.schema.ts — файлу, в якому описана бізнес-сутність Product. Для таких схем я використовую просте правило неймінгу: назва сутності + суфікс Schema, тобто в нашому випадку — Product_Schema.
Тут є принципово важливий момент: ми описуємо сутність так, як її бачить фронтенд, а не так, як вона представлена на бекенді. Одна й та сама сутність цілком може мати різні репрезентації на двох сторонах API.
Наприклад, бекенд-модель продукту може містити поле stock із точною кількістю одиниць товару на складі. Власники магазину навряд чи захочуть передавати цю інформацію назовні, тому фронт замість stock: 12345 може отримувати лише inStock: true. Саме цю репрезентацію і повинен описувати наш контракт.
import { z } from 'zod'
import { Id_Schema, Tracker_Schema } from './common.schemas'
// Схема бізнес-сутності Product для рантайм-валідації.
// Наслідує поля уніфікованої tracker сутності.
export const Product_Schema = Tracker_Schema.extend({
id: Id_Schema, name: z.string(),
description: z.string().min(5).max(500),
inStock: z.boolean(),
})
// Тип Product, автоматично виведений зі схеми рантайм-валідації.
export type Product = z.infer
// Generic Response Schema для endpoint-ів, які повертають обʼєкти однакової форми.
export const Product_GenericResponseSchema = Product_Schema
Важлива деталь — тип Product ми не описуємо окремо. Він автоматично генерується з тієї самої Zod-схеми через z.infer. Таким чином Product_Schema стає одночасно runtime-представленням контракту та джерелом compile-time типізації. Якщо ми змінимо структуру або правила валідації Product_Schema, TypeScript-тип Product зміниться разом із нею. Рантайм валідація та типізація залишаються синхронізованими за дизайном.
Зверніть увагу на productGenericResponseSchema. У реальних API дуже часто кілька ендпоінтів повертають одну й ту саму форму сутності. Замість того щоб повторно описувати її у кожному ендпоінт файлі, я пропоную винести його на верхній рівень — у файл відповідної сутності — і перевикористовувати звідти.
Важливе уточнення щодо самої Product_Schema: business-level схема повинна містити всі поля, які можуть зустрічатися в цій сутності в будь-якому ендпоінті. Це не endpoint-specific контракт, а entity-specific контракт — фактично single source of truth для всіх полів сутності та правил їх валідації. Уже конкретні ендпоінти повинні будувати свої контракти поверх цієї базової схеми через pick, omit, extend або перевикористовуємі підсхеми.
API Contract
Після того як у нас уже є опис канонічної бізнес-сутності, саме час описати контракт конкретного ендпоінта. Для прикладу візьмемо createProduct.schemas.ts.
Припустимо, що get, create та update endpoint-и повертають Product в однаковому вигляді. У такому випадку немає сенсу щоразу заново описувати схему— достатньо імпортувати вже підготовлену Product_GenericResponseSchema з product.schemas.ts і використати її як основу для CreateProduct_ResponsePayloadSchema.
import { Product_GenericResponseSchema, type Product } from '@/api/schemas/'
// Схема рантайм-валідації даних відповіді для Create Product.
export const CreateProduct_ResponsePayloadSchema = Product_GenericResponseSchema
export type CreateProduct_ResponsePayload = z.infer
// Тип даних запиту для Create Product.
// Zod-схема навмисно не створюється, оскільки вона не використовується.
export type CreateProduct_RequestPayload = Pick< Product, 'name' | 'description' >
Тут важливий ще один принцип: не треба створювати Zod-схему просто тому, що ви можете її створити. Якщо певні дані не проходять runtime validation і схема ніде не використовується, вона лише створює зайвий код та ще одну сутність, яку доведеться підтримувати.
Тому для ResponsePayload, який ми далі будемо перевіряти в контрактних тестах, Zod-схема має сенс. А якщо для RequestPayload нам достатньо compile-time типізації, ми просто формуємо потрібний тип із уже існуючого Product через Pick.
Таким чином навіть ендпоінт контракт продовжує будуватися від одного й того самого джерела істини, а не створює паралельний опис тієї ж структури.
Contract tests
Після того як бізнес-ентіті та контракти вже описані, самі тести стають майже нудними. Вам потрібно лише викликати API, отримати відповідь і перевірити її через відповідну Zod-схему.
import { getProductById_API } from '@/api/endpoints'
import { GetProductById_ResponsePayloadSchema } from '@/api/schemas'
describe('API contracts', () => {
describe('Products', () => {
it('GET product by ID, async () => {
const data = await getProductById_API(12345)
expect(() => { GetProductById_ResponsePayloadSchema.parse(data) }).not.toThrow()
})
})
})
Уся складність уже знаходиться не в самому тесті, а в правильно побудованих схемах тому додавання нової перевірки зазвичай займає буквально кілька рядків коду.
Важливе уточнення: це не юніт тести
Важливо правильно розуміти задачу контрактного тестування. Ми не намагаємося протестувати сам API або перевірити всю його бізнес-логіку. Це не місце для едж-кейсів, спроб «зламати» ендпоінт або перевірок того, як бек поводиться з неправильними даними.
Наша задача значно простіша: зробити валідний запит, отримати успішну відповідь і переконатися, що він відповідає контракту, на який розраховує наш застосунок.
Тому тримайте контрактні тести максимально простими:
- викликати API;
- переконатися, що запит успішний;
- перевірити відповідь відповідною схемою.
Не перетворюйте контрактні тести на дублювання юніт тестів беку (вони-ж у вас є, правда?). Тут нас цікавить лише одне: чи повертає API те, що очікує його споживач?
Pro Tips
#1. Валідація масивів
Зверніть увагу, що Zod успішно валідує порожній масив навіть тоді, коли для його елементів задана конкретна схема: z.array(Product_Schema). Для [] така валідація пройде успішно, тому що в масиві просто немає жодного елемента, до якого можна було б застосувати Product_Schema.
Тому якщо ваш тест повинен перевіряти не лише сам факт наявності масиву, а й форму його елементів, обов’язково переконайтеся, що тест дата містить хоча б один елемент. Наприклад:
expect(data.length).toBeGreaterThanOrEqual(1)
Інакше ви ризикуєте отримати false positive: тест зелений, але структура об’єктів усередині масиву фактично взагалі не була перевірена.
#2. Ловимо нові поля
В контрактних тестах важливо перевіряти не лише відсутні або невалідні поля, а й появу нових неочікуваних полів у відповіді. Тому схеми варто валідувати в strict-режимі. Інакше бек може додати нове поле, а ваш тест цього не помітить.
Вирішити це можна двома способами.
Перший — явно вмикати strict валідацію у кожній Zod-схемі:
const Product_Schema = z.object({ id: idSchema, name: z.string(), }).strict()
Другий варіант (який мені подобається більше) — написати окрему утиліту для контрактних тестів. Вона рекурсивно проходить по всій переданій Zod-схемі та застосовує strict до всіх object схем, включно зі схемами, вкладеними в arrays/objects/unions тощо.
У такому випадку схеми залишаються універсальними, а strict-поведінка централізовано додається саме там, де вона потрібна — у контрактних тестах.
#3. Показуємо отримане значення
Ще одна дрібниця, яка дуже сильно впливає на зручність дебагу: у помилці повинно бути видно конкретне значення, яке не пройшло перевірку.
Самого повідомлення на кшталт «поле не відповідає очікуваному формату» часто недостатньо. Особливо якщо тести працюють із великими відповідями або складно вкладеними структурами.
Тому я рекомендую окремо налаштувати репортінг так, щоб разом із шляхом до поля та причиною помилки виводилося і значення, що трігернуло помилку.
Замість абстрактного:
Invalid format at products[3].id
значно корисніше отримати щось на кшталт:
Invalid format at products[3].id Received: "abc-123"
Це здається невеликою деталлю, але на практиці вона суттєво скорочує час на розбір падінь тестів.
#4. Сталі дані
Іноді API має ендпоінти, які повертають сталі дані — тобто значення, що не змінюються регулярно. Наприклад — список статусів бізнес-ентіті. У такому випадку може бути корисно перевіряти не лише загальну форму відповіді, а й зафіксувати конкретний набір очікуваних значень через літерали.
На перший погляд це може виглядати як зайвий оверхед, але на практиці така перевірка іноді дуже корисна. Особливо якщо поведінка фронта напряму залежить від конкретних статусів: наприклад, для кожного з них відображається інший UI, доступні різні дії або працює окрема бізнес-логіка.
У такому випадку поява нового статусу або перейменування існуючого — це вже не просто зміна даних, а зміна контракту, яка потенційно впливає на фронт. Якщо немає повної впевненості, що бек-команда завжди вчасно повідомить про такі зміни, контрактні тести можуть виступити додатковою страховкою.
Бонуси
На початку статті я згадував, що розробник може отримати від цього фреймворку значно більше, ніж лише контрактні тести. Оскільки Zod-схеми вже стали нашим single source of truth, їх можна повторно використовувати безпосередньо в коді.
Ось декілька практичних сценаріїв.
#1. Типізація API
Оскільки TypeScript-типи автоматично генеруються з наших Zod-схем, ті самі типи можна використовувати безпосередньо в API layer:
export const getProductByIdApi = async ( id: string, ): Promise => api.get(/products/${id})
Таким чином типізація та валідація API базуються на одному джерелі істини. Змінилася схема — разом із нею змінюється і тип, який використовує код.
#2. Runtime-валідація даних із Local Storage
Припустимо, користувач створює продукт, а ми зберігаємо чернетку форми в localStorage. Після перезавантаження сторінки хочемо відновити його, але перед передачею даних у форму перевірити, що вони досі відповідають нашому контракту.
const rawDraft = localStorage.getItem('productDraft')
if (rawDraft) {
try {
const parsedDraft = JSON.parse(rawDraft)
const result = ProductDraft_Schema.safeParse(parsedDraft)
if (result.success) form.reset(result.data)
} catch {
// Invalid JSON — ignore the stored draft
}
}
}
Таким чином дані з localStorage проходять дві перевірки: спочатку ми гарантуємо, що це валідний JSON, а потім Zod перевіряє їхню структуру та значення перед тим, як вони потраплять назад у форму.
#3. Валідація форм
Ті самі Zod-схеми можна напряму інтегрувати з популярними бібліотеками форм. Наприклад, для React Hook Form існує Zod resolver:
const form = useForm({ resolver: zodResolver(Product_Schema), })
У результаті правила на кшталт:description: z.string().min(5).max(500) не потрібно повторно описувати у формі. Схема, яка визначає API контракт і TypeScript тип, одночасно визначає правила валідації форми.
#4. Використання параметрів схеми в UI
Zod-схема містить не лише інформацію про тип поля, а й його правила: description: z.string().min(5).max(500)
Якщо UI показує character counter: 127 / 500 — немає сенсу окремо hardcode-ити 500 у React-компоненті.
Беремо const maxLength = Product_Schema.shape.description.maxLength
І використовуємо значення безпосередньо в UI: <CharacterCounter current={description.length} max={maxLength} />
Тобто 500 існує лише в одному місці — у Product_Schema. Валідація форм та рантайму використовує одне й те саме правило.
#5. Інтеграція зі сторонніми сервісами
Але що це ми весь час говоримо про класичну інтеграцію frontend ↔ backend? Давайте подивимося на цей фреймворк з іншого боку.
Припустимо, ваш застосунок інтегрується з third-party API. Ваш код напряму залежить від відповідей, який повертає сторонній сервіс, але при цьому у вас немає ані прямого каналу комунікації з його командою, ані гарантії оперативного повідомлення про зміни API.
У такому сценарії контрактне тестування може заграти абсолютно новими барвами. Ваші тести фактично стають системою раннього попередження: якщо сторонній сервіс змінить структуру відповіді, прибере поле, додасть нове або змінить його формат, ви дізнаєтеся про це автоматично — ще до того, як зміна перетвориться на проблему для користувачів.
Тобто той самий фронтенд, який у внутрішній frontend ↔ backend інтеграції допомагає скоротити кількість комунікаційних та інтеграційних помилок, у випадку third-party API частково компенсує відсутність цієї комунікації взагалі.
#6. AI Compatibility
У цьому фреймворку я приділив багато уваги стандартизації, уніфікації структури та семантиці. І саме тут настав час пожинати бонуси: чим більш передбачуваною є архітектура репозиторія, тим простіше AI зрозуміти її закономірності та коректно їх продовжувати.
Якщо цей фреймворк імплементований послідовно, AI вже не потрібно щоразу вирішувати, де створювати файли, як називати схеми, як групувати ендпоінти або як будувати контрактні тести. У репозиторії вже існує паттерн, який достатньо розпізнати та відтворити.
На практиці це означає, що при появі нового API-модуля мені достатньо передати AI посилання на Swagger. Далі він може самостійно розібрати API специфікацію, визначити бізнес-сутності та endpoint-и й згенерувати модуль відповідно до вже існуючої структури: схем, типів, абстракцій та контрактного тестування.
І так, це перевірено на практиці. Реалізація нового API-модуля на 10 endpoint-ів разом із тестуванням займає близько 16 хвилин.
Тобто стандартизація тут працює не лише на developer experience. Вона робить репозиторій AI-native: замість того щоб щоразу пояснювати агенту, як потрібно реалізувати черговий модуль, ми даємо йому архітектуру, в якій правильний спосіб реалізації вже закодований у структурі та семантиці коду.
Висновок
Трохи цифр. Працюючи з великою API, яка має понад 100 ендпоінтів, за допомогою цього фреймворку мені вдалося звести кількість інтеграційних багів практично до нуля, скоротити час реакції на зміни API до одного дня та без додаткової комунікації з бек-командою перехопити близько 100 інтеграційних проблем лише за три місяці.
Я вважаю це достатньо сильним результатом. На цьому етапі фреймворк досить добре вирішує одну з класичних проблем розробки — постійну необхідність синхронізуватися зі змінами API та ловити ситуації, коли фактичний контракт починає відрізнятися від того, що очікує застосунок.
Загалом мені подобається результат. Фреймворк вирішує всі основні задачі, заради яких я його створював, при цьому вимагає відносно мало додаткового коду і дає значно більше користі, ніж просто валідація API.
Буду радий почути вашу думку.
Чи використовуєте ви щось подібне у своїх проєктах?
Де, на вашу думку, цей підхід працює добре, а де може почати ламатися?
8 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівВ принципі більшість описаних підходів імплементовуємо схожим чином, але з деякими речами не погоджуюсь. Наприклад який сенс перевіряти, чи не додалось нове поле в респонсі? Поява нового поля ніяк не впливає на фронт (так же, як і лишні поля в реквестах до беку), тому правильніше просто дату парсить через схему, все лишнє відпаде. Також не бачу з боку фронта сенсу в тестах реальних апі ендпойнтів, в іншому дякую, що поділились, є цікаві моменти.
А в юніт-тестах/описі інтерфейсів бекенда цього нема?
Коли я намагався валідувати респонси з бекенда, то бекендери покрутили пальцем біля скроні (хіба що для кривих 3rd party сервісів таке допускалось). Та і в теорії TDD і т.п. ніби говориться, що не потрібно тестувати те, за що ви не несете відповідальність і на що не можете повпливати.
Але ж будь-яка перевірка запиту на етапі білда валідна лише в момент білда і лише для тестових даних. Як правило проблеми з API вилазять в рантаймі (щось десь недорозрахувалось, щось десь недокешувалось...)
На заборі он що написано, а там будова виявляється(...
Ну, бекендери-послідовники Данінга-Крюгера для мене не новина. Оце вони б здивувались якби взнали, що Consumer-Driven Contract Testing виникло ще у 2006 році.
У висновку чіткі метрики — більше ста інтеграційних багів за три місяці.
Я не тестую дані — я тестую модель даних. Це трохи різні речі.
Ну це вже не проблема фронта і тим паче не проблема контрактних тестів. Якщо бек пливе під навантаженням це 100% проблема беку.
В мене частіше бувало, що апка ламалась при відсутності даних. Наприклад, коли API рандомно вертало або пустий масив або null )) Якщо модель передбачає nullable, то це ж і тестові дані під таке треба генерувати.
Або коли треба було в одному респонсі вертати дані з різних джерел і на тестових даних все ОК, а на реальних — частини атрибутів просто немає. Ну протестували ми, що API _може_ вертати «правильну» модель. Що далі? Як дізнатись, чи _може_ API вертати і неправильну?
Сама концепція перевірки схеми даних чудова. Але організаційно я її бачу на іншому рівні. Типу «наша апка працює от з таким інтерфейсом: %посилання_на_конфіг_OpenAPI%» і «наш бекенд віддає дані от в такому вигляді: %посилання_на_конфіг_OpenAPI%». Якщо формально конфіги сумісні (або це один і той же файл) — то і ОК, а неправда вилізе в рантаймі ))
PS Як це все працює з GraphQL?
Закладайте цю поведінку в модель даних.
Знову-ж таки: я не тестую дані
Не буває такої
Як посилання вам допоможе вирішити задачі, які я поставив перед цим фреймворком?
Там є вбудована система контрактів, але вона не така універсальна як моя.
Чи треба перевіряти схему на опціональні атрибути (коли відсутність полів валідна)?
Виглядає, як healthcheck тільки для схеми )) Тобто показує, що схема даних на момент запуску перевірки і для конкретних реквестів така, як і очікується
По хорошому ж апка має працювати з будь-яким бекендом, який реалізує заданий інтерфейс
Але якщо цей оверхед якось працює і приносить позитивний результат, то і добре
Саме це я і намагаюся перевірити фактажем
Підтримка фреймворку потребує ресурсів, але оверхедом його я б не називав.
До речі, дозвольте подякувати вам за предметну дискуссію.