GraphQL — потужний інструмент для побудови API, але, як відомо, з великою потужністю приходить велика відповідальність. Навіть найдосвідченіші розробники припускаються помилок при проєктуванні GraphQL-схем, обробників даних і запитів. У цій лекції розберемо найпоширеніші помилки, дізнаємося, як їх уникнути, і подивимось кращі практики для написання якісного та продуктивного GraphQL API.
Типові помилки при проєктуванні схеми
Неповна або надмірна схема
Проєктування схеми — це певне мистецтво, і тут легко або перестаратися, або навпаки створити схему, яка не покриває потрібний функціонал.
Приклад помилки:
type Query {
userProfile: UserProfile
}
type UserProfile {
id: ID
firstName: String
lastName: String
age: Int
hobbies: [String]
contactInfo: String
}
Здається нормально, але що якщо contactInfo містить чутливі дані? Така схема може легко призвести до витоку.
Як уникнути:
- Проєктуйте мінімалістичні та ізольовані схеми.
- Використовуйте підхід
need-to-knowпри наданні даних і уникайте «перегодовування» клієнтів інформацією.
Приклад виправленої схеми:
type Query {
userProfile: UserProfile
}
type UserProfile {
id: ID
firstName: String
lastName: String
age: Int
hobbies: [String]
}
type PrivateUserInfo {
contactInfo: String
}
Тепер конфіденційна інформація винесена в окремий тип, який можна надавати лише авторизованим користувачам.
Глибокі вкладені структури (проблема N+1)
Помилка:
Якщо ваша схема містить занадто глибокі вкладення, клієнт може запитувати величезні обсяги даних одним потужним запитом.
Приклад:
query {
users {
id
posts {
id
comments {
id
text
}
}
}
}
Якщо є тисячі користувачів, у кожного десятки постів, а у кожного поста сотні коментарів, цей запит може «розвалити» ваш сервер.
Як уникнути:
- Обмежуйте глибину запитів через плагіни або middleware.
- Використовуйте
Batch Loadingдля усунення дублюючих запитів.
Помилки в обробці даних
Проблеми з продуктивністю (помилка N+1)
Проблема: Поширена помилка при використанні GraphQL пов'язана з тим, що при витяганні пов'язаних даних запити до бази даних починають «вистрілювати» як з кулемета.
Приклад:
@QueryMapping
public List<User> users() {
return userService.getAllUsers();
}
@QueryMapping
public List<Post> posts(@Argument("userId") String userId) {
return postService.getPostsByUser(userId);
}
Кожен користувач може ініціювати окремий запит для отримання пов'язаних постів. Якщо користувачів 100, то буде виконано 100 SQL-запитів до бази даних.
Виправлення: Використовуємо DataLoader:
@Bean
public DataLoaderRegistry dataLoaderRegistry() {
DataLoaderRegistry registry = new DataLoaderRegistry();
DataLoader<String, List<Post>> postLoader = DataLoader.newMappedDataLoader(userService::getPostsForUsers);
registry.register("posts", postLoader);
return registry;
}
Тепер для 100 користувачів буде виконано всього 1 батч-запит.
Необроблені виключення
Помилка: ваш обробник даних кидає необроблені виключення, які потрапляють у серверні логи і потенційно розкривають інформацію про внутрішню структуру системи.
Приклад:
@QueryMapping
public User user(@Argument("id") String id) {
return userService.findById(id);
}
Якщо користувач з вказаним id не знайдений, скоріше за все, буде викинуто NullPointerException.
Рішення: Огорніть помилки в кастомні виключення.
@QueryMapping
public User user(@Argument("id") String id) {
return userService.findById(id).orElseThrow(() -> new CustomException("User not found"));
}
Помилки захисту даних
Відсутність обмежень по глибині та складності запитів
GraphQL дає клієнту таку гнучкість, що недобросовісні користувачі можуть відправити запити з глибиною в 1000 рівнів або зі складністю, що перевищує ліміти в 100 тисяч операцій.
Рішення:
- Увімкніть обмеження глибини і складності запитів.
- Використовуйте бібліотеки такі як
graphql-javaабо плагіни для налаштування лімітів.
GraphQLSchema schema = GraphQLSchema.newSchema()
.query(QueryType)
.maxQueryDepth(10)
.maxQueryComplexity(1000)
.build();
Відсутність перевірки авторизації та аутентифікації
Помилка: Часто забувають про перевірку авторизації користувачів при виконанні запитів.
Рішення:
Використовуйте DataFetchEnvironment, щоб валідовувати поточного користувача:
public CompletableFuture<User> user(DataFetchingEnvironment env) {
String token = env.getContext();
if (!authService.isAuthenticated(token)) {
throw new AuthenticationException("Invalid token");
}
return userService.getUserByToken(token);
}
Помилки в тестуванні
Непокриття тестами складних запитів
Багато хто обмежується тестами для простих запитів, але забуває тестувати запити з фрагментами, вкладеними полями і мутаціями.
Рішення:
Пишіть тести для всіх сценаріїв використання. Наприклад, використовуйте MockMvc для інтеграційних тестів:
mockMvc.perform(post("/graphql")
.content("{\"query\":\"{ user { id, name } }\"}")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.user.id").exists());
Помилки у проєктуванні API
Дублювання логіки в схемах і резолверах
Помилка: Логіка повторюється для однієї і тієї ж поведінки в GraphQL API.
Рішення:
Використовуйте сервісний шар для обробки бізнес-логіки і підлаштовуйте її під контекст запиту в резолверах.
Практика: виправлення помилок
На завершення лекції розглянемо вправи:
- Виправте сценарій з проблемою N+1 з використанням
DataLoader. - Реалізуйте обмеження глибини запитів.
- Напишіть інтеграційні тести для мутації з вкладеними об'єктами.
На реальному проєкті це дасть вам більш стійке API, а клієнти будуть вдячні.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ