Вы могли слышать о новеньком: GraphQL. А если не слышали, то GraphQL, это новый способ описывать API, альтернатива REST. Началось всё как внутренний проект Facebook, а после того как проект стал открытым, привлёк много внимания.

Цель этой статьи помочь упростить переход от REST к GraphQL, независимо от того, решили ли вы точно перейти на GraphQL или просто хотите попробовать. Никаких знаний для начала не требуется, но хотя бы некоторое знакомство с REST необходимо, чтобы понять статью.

GraphQL схема

Graphql vs. REST: что не так REST?

Если вы всё еще в нерешительности по поводу того, подходит GraphQL под ваши нужды или нет, то вот несколько причин использовать этот механизм.

Причина 1: сетевая производительность

Допустим, у вас есть ресурс «пользователь» со следующими параметрами: имя, фамилия, e-mail и 10ю другими. На клиенте нужны только несколько из них. Соответственно будут дополнительные затраты на передачу данных, что может быть критично для мобильных клиентов.

GraphQL по-умолчанию производит как можно меньшую выборку данных. Если вам требуются только имя и фамилия, то нужно это указать в запросе.

Для запроса всех пользователей можно сделать GET запрос на /users и получить только фамилии и имена.

Query

query {
  users {
    firstname
    lastname
  }
}

Result

{
  "data": {
    "users": [
      {
        "firstname": "John",
        "lastname": "Doe"
      },
      {
        "firstname": "Alicia",
        "lastname": "Smith"
      }
    ]
  }
}

Если мы хотим получить еще и e-mail, нужно просто добавить “email” под “lastname”.

Некоторые REST API предлагают такую опцию /users?fields=firstname,lastname, чтобы получить ответ с частью данных. Но по-умолчанию такой функционал не предполагается. Плюс запрос становится сложно читаемым, если добавляются дополнительные параметры, например:

  • &status=active для фильтрации пользователей
  • &sort=createdAt для сортировки под дате регистрации
  • &sortDirection=desc, просто потому что нужно
  • &include=projects для выборки дополнительных данных по пользователю

Эти параметры добавляются, чтобы имитировать язык запросов. А GraphQL и есть язык запросов, который делает запросы краткими и точными.

Причина 2: Выбор между “Include” и “Endpoint”

Давайте представим, что мы хотим построить простой инструмент для управления проектами. У нас есть три ресурса: пользователи, проекты и задачи. Также мы определяем следующие связи между ресурсами:

GraphQL - взаимоотношение модейлей

Вот некоторые из методов API, доступные всем:

  • GET /users  - список всех пользователей
  • GET /users/:id - выборка пользователя по ID
  • GET /users/:id/projects выборка всех проектов для пользователя

Endpoint-ы просты, легко читаемы, с хорошей организацией.

Но всё становится сложнее, когда запросы становятся сложнее. Возьмём endpoint GET /users/:id/projects. Мне нужно выводить только названия проектов на главной странице и проекты с их задачами на странице dashboard, но так чтобы пришлось делать только один запрос. Придётся делать так:

  • GET /users/:id/projects для главной страницы
  • GET /users/:id/projects?include=tasks (допустим) для страницы dashboard

Это нормальная практика добавлять параметр ?include=... для запроса дополнительных данных, и даже рекомендуется спецификацией JSON API. Запрос с параметром ?include=tasks читаем, но до тех пор, пока не станет таким: ?include=tasks,tasks.owner,tasks.comments,tasks.comments.author.

Может в этом случае будет мудрее создать эндпоинт /projects. Будет что-то вроде /projects?userId=:id&include=tasks, с одним уровнем отношений. Также будет метод API /tasks?userId=:id. В общем всё будет достаточно сложно при большом количестве отношений.

GraphQL использует include-ы повсеместно. Это делает синтаксис запроса отношений мощным и консистентным.

Пример того, как можно получить все проекты и задачи пользователя с ID=1.

Query

{
  user(id: 1) {
    projects {
      name
      tasks {
        description
      }
    }
  }
}

Result

{
  "data": {
    "user": {
      "projects": [
        {
          "name": "Migrate from REST to GraphQL",
          "tasks": [
            {
              "description": "Read tutorial"
            },
            {
              "description": "Start coding"
            }
          ]
        },
        {
          "name": "Create a blog",
          "tasks": [
            {
              "description": "Write draft of article"
            },
            {
              "description": "Set up blog platform"
            }
          ]
        }
      ]
    }
  }
}

Как видите, запрос легко читаем. Если нам нужно получить еще задачи, комментарии, картинки и авторов, мы сто раз подумает как организовать наш API. С GraphQL всё гораздо проще.

Причина 3: Управление разными типами клиентов

Разрабатывая backend, мы всегда стараемся сделать наш API рассчитанным на широкий круг клиентов. Однако клиенты хотят делать меньше запросов и получать больше данных. Запросы от разных клиентских приложений (web, mobile) могут различаться набором include-ов, фильтров и частичных ресурсов.

Для REST есть пара решений. Можно создать дополнительный endpoint (например, /mobile_user), нестандартный тип данных в заголовке(например, Content-Type: application/vnd.rest-app-example.com+v1+mobile+json) или отдельный клиенто-зависимый API. Каждый из вариантов требует дополнительных затрат от разработчиков.

GraphQL даёт больше возможностей клиентам. Если клиенту нужен более сложный запрос, то он сам может его построить.

Как начать работать с GraphQL

Во многих спорах о “GraphQL vs. REST”, люди считают, что нужно использовать что-то одно. Но это не совсем так.

Современные приложения состоят из нескольких сервисов, которые предоставляют несколько API. GraphQL лучше рассматривать как обёртку для всех этих сервисов. Клиентское приложение обращается к endpoint-у GraphQL, а этот эндпоинт обращается с БД, внешнему сервису (например, ElasticSearch или Sendgrid) или REST API.

GraphQL as wrapper

Второй путь это реализация отдельного эндпоинта /graphql в вашем REST API. Это будет удобно, если у вашим API уже пользуются, а вы хотите попробовать GraphQL без изменения инфраструктуры. Это решение мы и рассмотрим.

Для примеров будем использовать Node.js и Express для web-сервера, SQLite как БД и Sequelize как ORM. Будет три модели – user, project и task. REST endpoint-ы /api/users, /api/projects и /api/tasks выставлены наружу.

Нужно отметить, что GraphQL может быть реализован на любой платформе и на любом языке.

Наша цель – создать эндпоинт /graphql без изменения методов REST API. GraphQL будет обращаться к БД через ORM, соотв. полностью независим от части REST.

Типы

Модель данных, представляется в GraphQLв виде типов, которые строго типизированы. Отношение наших моделей и типов GraphQL должно быть 1 к 1. Пользователь описывается так:

type User {
  id: ID! # The "!" means required
  firstname: String
  lastname: String
  email: String
  projects: [Project] # Project is another GraphQL type
}

Запросы

Queries определяют какие запросы можно выполнять через GraphQL API. По соглашению, должен быть RootQuery, который содержит все существующие запросы. Также рассмотрим эквивалент на REST.

type RootQuery {
  user(id: ID): User           # Corresponds to GET /api/users/:id
  users: [User]                # Corresponds to GET /api/users
  project(id: ID!): Project    # Corresponds to GET /api/projects/:id
  projects: [Project]          # Corresponds to GET /api/projects
  task(id: ID!): Task          # Corresponds to GET /api/tasks/:id
  tasks: [Task]                # Corresponds to GET /api/tasks
}

Изменения

Если запросы это GET, то изменения могут быть POST/PATCH/PUT/DELETE.

По рекомендациям, все запросы на изменение описываются в RootMutation:

type RootMutation {
  createUser(input: UserInput!): User             # Corresponds to POST /api/users
  updateUser(id: ID!, input: UserInput!): User    # Corresponds to PATCH /api/users
  removeUser(id: ID!): User                       # Corresponds to DELETE /api/users

  createProject(input: ProjectInput!): Project
  updateProject(id: ID!, input: ProjectInput!): Project
  removeProject(id: ID!): Project
  
  createTask(input: TaskInput!): Task
  updateTask(id: ID!, input: TaskInput!): Task
  removeTask(id: ID!): Task
}

Заметьте, что мы вводим новые типы, такие как UserInput, ProjectInput и TaskInput. Это нормальная практика и для REST создать дополнительные модели данных для добавления и изменения данных. Например, модель UserInput это тот же User только без полей id и project:

input UserInput {
  firstname: String
  lastname: String
  email: String
}

Схема

С помощью типов, запросов и изменений мы описываем схему GraphQL, которая домтупна из-вне.

schema {
  query: RootQuery
  mutation: RootMutation
}

Resolvers

Схема у нас описана, теперь нужно указать GraphQL что и когда делать на каждый запрос. Это называется resolvers. Они выполняют самую сложную работу, например:

  • Дёргают методы REST API
  • Обращаются к внутренним микросервисам
  • Работают с БД

Посмотрим на наши resolvers:

const models = sequelize.models;

RootQuery: {
  user (root, { id }, context) {
    return models.User.findById(id, context);
  },
  users (root, args, context) {
    return models.User.findAll({}, context);
  },
  // Resolvers for Project and Task go here
},
    
/* For reminder, our RootQuery type was:
type RootQuery {
  user(id: ID): User
  users: [User]
 
  # Other queries
}

Это означает, что если запрашивается user(id: ID!), то выполняется return User.findById(), что является функцией Sequelize ORM.

Чтобы объединять модели в ответе, нужно описать дополнительные resolvers:

User: {
  projects (user) {
    return user.getProjects(); // getProjects is a function managed by Sequelize ORM
  }
},
    
/* For reminder, our User type was:
type User {
  projects: [Project] # We defined a resolver above for this field
  # ...other fields
}
*/

Соответственно, когда мы запрашиваем проекты пользователя к запросу к БД будет добавлен join.

И наконец, обработчики для изменений:

RootMutation: {
  createUser (root, { input }, context) {
    return models.User.create(input, context);    
  },
  updateUser (root, { id, input }, context) {
    return models.User.update(input, { ...context, where: { id } });
  },
  removeUser (root, { id }, context) {
    return models.User.destroy(input, { ...context, where: { id } });
  },
  // ... Resolvers for Project and Task go here
}

Запрос

query getUserWithProjects {
  user(id: 2) {
    firstname
    lastname
    projects {
      name
      tasks {
        description
      }
    }
  }
}

mutation createProject {
  createProject(input: {name: "New Project", UserId: 2}) {
    id
    name
  }
}

Результат

{
  "data": {
    "user": {
      "firstname": "Alicia",
      "lastname": "Smith",
      "projects": [
        {
          "name": "Email Marketing Campaign",
          "tasks": [
            {
              "description": "Get list of users"
            },
            {
              "description": "Write email template"
            }
          ]
        },
        {
          "name": "Hire new developer",
          "tasks": [
            {
              "description": "Find candidates"
            },
            {
              "description": "Prepare interview"
            }
          ]
        }
      ]
    }
  }
}

Конечно, потребуется некоторое время, чтобы переписать все типы запросы и обработчики для вашего существующего приложения. Но существуют различные инструменты для перевода схемы SQL в GraphQL.

Соберём всё вместе

После того как всё описано, можно подключить метод /graphql к нашему backend-у:

// Mount GraphQL on /graphql
const schema = makeExecutableSchema({
  typeDefs, // Our RootQuery and RootMutation schema
  resolvers: resolvers() // Our resolvers
});
app.use('/graphql', graphqlExpress({ schema }));

Что дальше?

Цель этой статьи – дать почувствовать вкус GraphQL. А чтобы понять нужен вам GraphQL или нет, надо просто попробовать его внедрить.

Осталось много функций, которые не были затронуты в этой статье, такие как real-time обновления, группировка данных на сервере, авторизация и аутентификация, кеширование, загрузка файлов и т.д. Для более детального изучения можно обратиться к ресурсу «How to GraphQL».

Вот ещё несколько полезных ресурсов для backend:

  • graphql-js - реализация GraphQL. Можно использовать совместно с express-graphql, чтобы создать backend.
  • graphql-server - всё в одном. GraphQL сервер, реализованный Apollo team.
  • для других платформ - Ruby, PHP и т.д.

И для frontend:

GraphQL это не просто хайп, что быстро лопнет. Он, конечно, не заменит REST завтра, но предлагает эффективное решение актуальное проблемы. Технология достаточно новая и best practice еще только разрабатываются, но в следующие пару лет мы точно о ней услышим.



Комментарии

добавить
Комментариев пока нет. Будете первым?
Чтобы комментировать, нужно авторизоваться

Советуем почитать


Почему стоит изучать Ruby on Rails
Администратор 0

Почему стоит изучать Ruby on Rails читать далее

Вы начинающий программист? Или просто думаете какой бы язык изучить? Очень рекомендуем вам обратить внимание на Ruby on Rails. Не смотря на обилие языков программирования и доступных фреймворков, Ruby on Rails очень популярен среди web-разработчиков. Всё благодаря функционалу и скорости разработки.

0 28.01.2018 17:12:42

Федеральная система
Сергей 0

Федеральная система "Город" читать далее

В прошлый раз описал процесс работы с платёжной системой Cyberplat, теперь хочу поделиться опытом работы с ФСГ (Федеральная система город).

Разработано сие чудо ЦФТ. Старались делать все по ГОСТ, поэтому произвести интеграцию не так просто, как хотелось бы (рассматриваем PHP).

0 11.07.2016 17:40:15