Разработка GraphQL-резолверов для веб-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка GraphQL-резолверов для веб-приложения
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Разработка GraphQL-резолверов

GraphQL-резолвер — функция, возвращающая данные для конкретного поля схемы. Качество резолверов определяет производительность и поддерживаемость API: наивная реализация генерирует N+1 запросы, неправильная — утечки данных между пользователями.

Структура резолвера

// schema.graphql
type Query {
  user(id: ID!): User
  posts(limit: Int = 10, offset: Int = 0): [Post!]!
}

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  author: User!
  comments: [Comment!]!
}
// resolvers.js
const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      // context содержит: user (из auth), dataloaders, db
      if (!context.user) throw new AuthenticationError('Not authenticated')
      return context.db.users.findById(id)
    },

    posts: async (parent, { limit, offset }, context) => {
      return context.db.posts.findAll({ limit, offset })
    }
  },

  User: {
    // Резолвер поля posts на типе User
    posts: async (parent, args, context) => {
      // parent — объект User из родительского резолвера
      // БЕЗ DataLoader это N+1 запрос!
      return context.loaders.postsByUserId.load(parent.id)
    }
  },

  Post: {
    author: async (parent, args, context) => {
      return context.loaders.userById.load(parent.author_id)
    },

    comments: async (parent, args, context) => {
      return context.loaders.commentsByPostId.load(parent.id)
    }
  }
}

Контекст и dependency injection

// server.js — формирование контекста запроса
import { ApolloServer } from '@apollo/server'
import { DataloaderRegistry } from './dataloaders'

const server = new ApolloServer({ typeDefs, resolvers })

app.use('/graphql', expressMiddleware(server, {
  context: async ({ req }) => {
    // Аутентификация
    const token = req.headers.authorization?.replace('Bearer ', '')
    const user = token ? await verifyToken(token) : null

    // DataLoader'ы создаются per-request (важно! иначе кросс-пользовательский кеш)
    const loaders = new DataloaderRegistry(db)

    return { user, db, loaders, req }
  }
}))

Авторизация в резолверах

// Вспомогательные функции авторизации
function requireAuth(context) {
  if (!context.user) {
    throw new GraphQLError('Not authenticated', {
      extensions: { code: 'UNAUTHENTICATED' }
    })
  }
}

function requireRole(context, role) {
  requireAuth(context)
  if (!context.user.roles.includes(role)) {
    throw new GraphQLError('Forbidden', {
      extensions: { code: 'FORBIDDEN' }
    })
  }
}

// Применение в резолверах
const resolvers = {
  Mutation: {
    deletePost: async (parent, { id }, context) => {
      requireAuth(context)

      const post = await context.db.posts.findById(id)
      if (!post) throw new UserInputError('Post not found')

      // Проверить владение: только автор или admin
      if (post.author_id !== context.user.id && !context.user.roles.includes('admin')) {
        throw new GraphQLError('Cannot delete others\' posts', {
          extensions: { code: 'FORBIDDEN' }
        })
      }

      await context.db.posts.delete(id)
      return { success: true }
    }
  }
}

Обработка ошибок

import { GraphQLError } from 'graphql'
import { ApolloServerErrorCode } from '@apollo/server/errors'

const resolvers = {
  Mutation: {
    createPost: async (parent, { input }, context) => {
      requireAuth(context)

      // Валидация входных данных
      if (!input.title?.trim()) {
        throw new GraphQLError('Title is required', {
          extensions: { code: ApolloServerErrorCode.BAD_USER_INPUT }
        })
      }

      if (input.title.length > 200) {
        throw new GraphQLError('Title too long (max 200)', {
          extensions: {
            code: ApolloServerErrorCode.BAD_USER_INPUT,
            field: 'title'
          }
        })
      }

      try {
        return await context.db.posts.create({
          ...input,
          author_id: context.user.id
        })
      } catch (err) {
        // Не пропускать детали БД в продакшен
        console.error('DB error creating post:', err)
        throw new GraphQLError('Internal server error', {
          extensions: { code: 'INTERNAL_SERVER_ERROR' }
        })
      }
    }
  }
}

// Форматирование ошибок перед отправкой клиенту
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (formattedError, error) => {
    // Скрыть технические детали в продакшен
    if (process.env.NODE_ENV === 'production') {
      if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {
        return { message: 'Internal server error' }
      }
    }
    return formattedError
  }
})

Подписки (Subscriptions)

// schema.graphql
type Subscription {
  postCreated: Post!
  commentAdded(postId: ID!): Comment!
}

// resolvers
import { PubSub } from 'graphql-subscriptions'
const pubsub = new PubSub()

const resolvers = {
  Mutation: {
    createPost: async (parent, { input }, context) => {
      const post = await context.db.posts.create(input)

      // Публикация события
      pubsub.publish('POST_CREATED', { postCreated: post })

      return post
    }
  },

  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
    },

    commentAdded: {
      subscribe: (parent, { postId }) => {
        return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`])
      },
      resolve: (payload) => payload.commentAdded
    }
  }
}

Тестирование резолверов

// post.resolver.test.js
describe('Post resolvers', () => {
  const mockDb = {
    posts: {
      findById: jest.fn(),
      delete: jest.fn()
    }
  }

  const mockContext = (overrides = {}) => ({
    user: { id: '1', roles: ['user'] },
    db: mockDb,
    loaders: { userById: { load: jest.fn() } },
    ...overrides
  })

  it('deletePost: owner can delete their post', async () => {
    mockDb.posts.findById.mockResolvedValue({ id: '1', author_id: '1' })
    mockDb.posts.delete.mockResolvedValue(true)

    const result = await resolvers.Mutation.deletePost(
      null, { id: '1' }, mockContext()
    )

    expect(result).toEqual({ success: true })
    expect(mockDb.posts.delete).toHaveBeenCalledWith('1')
  })

  it('deletePost: non-owner gets FORBIDDEN', async () => {
    mockDb.posts.findById.mockResolvedValue({ id: '1', author_id: '99' })

    await expect(
      resolvers.Mutation.deletePost(null, { id: '1' }, mockContext())
    ).rejects.toMatchObject({ extensions: { code: 'FORBIDDEN' } })
  })
})

Срок выполнения

Разработка набора GraphQL-резолверов с авторизацией, DataLoader-интеграцией и тестами — 2–4 рабочих дня в зависимости от количества типов.