Лучшие практики для разработки REST API

Опубликовано 12 April 2020

В этой статье мы рассмотрим, как спроектировать API-интерфейсы REST, чтобы их было легко понять всем, кто их использует, рассчитанные на будущее, безопасны и быстры, поскольку они предоставляют данные клиентам, которые могут быть конфиденциальными.


Лучшие практики для разработки REST API


REST API - это один из наиболее распространенных видов веб-сервисов, доступных сегодня. Они позволяют различным клиентам, включая приложения браузера, взаимодействовать с сервером через REST API.


Поэтому очень важно правильно спроектировать API REST, чтобы в будущем мы не столкнулись с проблемами. Мы должны учитывать безопасность, производительность и простоту использования для пользователей API.


В противном случае мы создаем проблемы для клиентов, которые используют наши API, что не очень приятно и отвлекает людей от использования нашего API. Если мы не следуем общепринятым соглашениям, то мы путаем разработчиков API и клиентов, которые их используют, поскольку он отличается от того, что все ожидают.


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


Принять и ответить с JSON


API REST должны принимать JSON для полезной нагрузки запросов, а также отправлять ответы в JSON. JSON - это стандарт для передачи данных. Практически все сетевые технологии могут использовать его: в JavaScript есть встроенные методы для кодирования и декодирования JSON либо через Fetch API, либо через другой HTTP-клиент. У серверных технологий есть библиотеки, которые могут декодировать JSON без особой работы.


Есть и другие способы передачи данных. XML не широко поддерживается фреймворками, не превращая сами данные в то, что можно использовать, и обычно это JSON. Мы не можем так легко манипулировать этими данными на стороне клиента, особенно в браузерах. В итоге получается много дополнительной работы только для обычной передачи данных.


Данные формы хороши для отправки данных, особенно если мы хотим отправить файлы. Но для текста и чисел нам не нужны данные формы для их передачи, поскольку - в большинстве сред - мы можем передавать JSON, просто получая данные из него непосредственно на стороне клиента. Это, безусловно, самый простой способ сделать это.


Чтобы убедиться, что когда наше приложение REST API отвечает с помощью JSON, что клиенты интерпретируют его как таковое, мы должны установить Content-Type в заголовке ответа на application / json после выполнения запроса. Многие серверные каркасы приложений устанавливают заголовок ответа автоматически. Некоторые клиенты HTTP смотрят на заголовок ответа Content-Type и анализируют данные в соответствии с этим форматом.


Единственное исключение - если мы пытаемся отправлять и получать файлы между клиентом и сервером. Затем нам нужно обработать ответы файла и отправить данные формы с клиента на сервер. Но это тема для другого времени.


Мы также должны убедиться, что наши конечные точки возвращают JSON в качестве ответа. Многие серверные инфраструктуры имеют это как встроенную функцию.


Давайте рассмотрим пример API, который принимает полезные данные JSON. В этом примере будет использоваться базовая среда Express для Node.js. Мы можем использовать промежуточное программное обеспечение анализатора тела для анализа тела запроса JSON, а затем мы можем вызвать метод res.json с объектом, который мы хотим вернуть в качестве ответа JSON, следующим образом:


const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.post('/', (req, res) => {
  res.json(req.body);
});

app.listen(3000, () => console.log('server started'));


bodyParser.json () анализирует строку тела запроса JSON в объект JavaScript и затем назначает ее объекту req.body.


Установите заголовок Content-Type в ответе на application / json; charset = utf-8 без каких-либо изменений. Вышеприведенный метод применим к большинству других серверных структур.


Используйте существительные вместо глаголов в путях конечных точек.


Мы не должны использовать глаголы в наших путях конечных точек. Вместо этого мы должны использовать существительные, которые представляют сущность, которую конечная точка мы извлекаем или манипулируем, в качестве имени пути.


Это потому, что наш метод HTTP-запроса уже имеет глагол. Наличие глаголов в путях к конечным точкам API бесполезно и делает это излишне долгим, поскольку оно не передает никакой новой информации. Выбранные глаголы могут варьироваться в зависимости от прихоти разработчика. Например, некоторые как «get», а некоторые как «retrieve», так что лучше просто позволить HTTP-глаголу GET сообщать нам, что и как делает конечная точка.


Действие должно быть указано методом HTTP-запроса, который мы делаем. Наиболее распространенные методы включают GET, POST, PUT и DELETE.


GET восстанавливает ресурсы. POST отправляет новые данные на сервер. PUT обновляет существующие данные. DELETE удаляет данные. Глаголы отображаются на операции CRUD.


Помня о двух принципах, которые мы обсуждали выше, мы должны создать такие маршруты, как GET / Articles / для получения новостных статей. Аналогично, POST / article / предназначен для добавления новой статьи, PUT / articles /: id для обновления статьи с указанным идентификатором. DELETE / article /: id предназначен для удаления существующей статьи с указанным идентификатором.


/ Articles представляет ресурс REST API. Например, мы можем использовать Express, чтобы добавить следующие конечные точки для манипулирования статьями следующим образом:


const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.get('/articles', (req, res) => {
  const articles = [];
  // code to retrieve an article...
  res.json(articles);
});

app.post('/articles', (req, res) => {
  // code to add a new article...
  res.json(req.body);
});

app.put('/articles/:id', (req, res) => {
  const { id } = req.params;
  // code to update an article...
  res.json(req.body);
});

app.delete('/articles/:id', (req, res) => {
  const { id } = req.params;
  // code to delete an article...
  res.json({ deleted: id });
});

app.listen(3000, () => console.log('server started'));


В приведенном выше коде мы определили конечные точки для манипулирования статьями. Как мы видим, в именах путей нет глаголов. Все, что у нас есть, это существительные. Глаголы в глаголах HTTP.


Все конечные точки POST, PUT и DELETE принимают JSON в качестве тела запроса, и все они возвращают JSON в качестве ответа, включая конечную точку GET.


Название коллекции с существительными во множественном числе.


Мы должны назвать коллекции с существительными во множественном числе. Нечасто мы хотим получить только один элемент, поэтому мы должны соответствовать нашим именам, мы должны использовать множественные существительные.


Мы используем множественное число, чтобы соответствовать тому, что находится в наших базах данных. Таблицы обычно имеют более одной записи и имеют имена, отражающие это, поэтому для соответствия им мы должны использовать тот же язык, что и таблица, к которой обращается API.


С конечной точкой / article у нас есть форма множественного числа для всех конечных точек, поэтому нам не нужно менять ее на множественное число.


Вложенные ресурсы для иерархических объектов.


Путь конечных точек, которые имеют дело с вложенными ресурсами, должен быть сделан путем добавления вложенного ресурса в качестве имени пути, который идет после родительского ресурса.


Мы должны убедиться, что он проверяет, что то, что мы считали вложенными ресурсами, соответствует тому, что мы имеем в наших таблицах базы данных. В противном случае, это будет сбивать с толку.


Например, если мы хотим, чтобы конечная точка получала комментарии для новостной статьи, мы должны добавить путь / comments к концу пути / Articles. Это предполагает, что у нас есть комментарии как дочерняя статья в нашей базе данных.


Например, мы можем сделать это с помощью следующего кода в Express:

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.get('/articles/:articleId/comments', (req, res) => {
  const { articleId } = req.params;
  const comments = [];
  // code to get comments by articleId
  res.json(comments);
});


app.listen(3000, () => console.log('server started'));


В приведенном выше коде мы можем использовать метод GET по пути «/ article /: articleId / comments». Мы получаем комментарии к статье, идентифицированной articleId, а затем возвращаем ее в ответе. Мы добавляем «comments» после сегмента пути «/ article /: articleId», чтобы указать, что это дочерний ресурс для / article.


Это имеет смысл, поскольку комментарии являются дочерними объектами статей, при условии, что каждая статья имеет свои комментарии. В противном случае это сбивает с толку пользователя, так как эта структура обычно используется для доступа к дочерним объектам. Тот же принцип применим и к конечным точкам POST, PUT и DELETE. Все они могут использовать одинаковую структуру вложенности для имен путей.


Изящно обрабатывать ошибки и возвращать стандартные коды ошибок.


Чтобы исключить путаницу для пользователей API при возникновении ошибки, мы должны корректно обрабатывать ошибки и возвращать HTTP-коды ответов, которые указывают, какой тип ошибки произошел. Это дает разработчикам API достаточно информации, чтобы понять возникшую проблему. Мы не хотим, чтобы ошибки приводили к выходу из строя нашей системы, поэтому мы можем оставить их необработанными, что означает, что потребитель API должен их обрабатывать.


Общие коды ошибок HTTP включают в себя:


400 Bad Request - Это означает, что ввод на стороне клиента не проходит проверку.

401 Unauthorized - это означает, что пользователь не авторизован для доступа к ресурсу. Обычно он возвращается, когда пользователь не аутентифицирован.

403 Запрещено - это означает, что пользователь прошел проверку подлинности, но ему не разрешен доступ к ресурсу.

404 Not Found - указывает, что ресурс не найден.

500 Внутренняя ошибка сервера - это общая ошибка сервера. Это, вероятно, не должно быть брошено явно.

502 Bad Gateway - указывает неверный ответ от вышестоящего сервера.

503 Сервис недоступен - это указывает на то, что на стороне сервера произошло что-то непредвиденное (это может быть что-то вроде перегрузки сервера, сбоя некоторых частей системы и т. Д.).

Мы должны выдавать ошибки, которые соответствуют проблеме, с которой столкнулось наше приложение. Например, если мы хотим отклонить данные из полезной нагрузки запроса, мы должны вернуть ответ 400 в Express API следующим образом:

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

// existing users
const users = [
  { email: 'abc@foo.com' }
]

app.use(bodyParser.json());

app.post('/users', (req, res) => {
  const { email } = req.body;
  const userExists = users.find(u => u.email === email);
  if (userExists) {
    return res.status(400).json({ error: 'User already exists' })
  }
  res.json(req.body);
});


app.listen(3000, () => console.log('server started'));


В приведенном выше коде у нас есть список существующих пользователей в массиве пользователей с указанным адресом электронной почты.


Затем, если мы попытаемся передать полезную нагрузку со значением электронной почты, которое уже существует у пользователей, мы получим код состояния ответа 400 с сообщением «Пользователь уже существует», чтобы пользователи знали, что пользователь уже существует. С помощью этой информации пользователь может исправить действие, изменив адрес электронной почты на то, что не существует.


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


Всякий раз, когда наш API не завершается успешно, мы должны грациозно завершить работу, отправив сообщение об ошибке с информацией, чтобы помочь пользователям принять корректирующие меры.


Разрешить фильтрацию, сортировку и разбиение на страницы.


Базы данных за API REST могут стать очень большими. Иногда данных так много, что их не нужно возвращать сразу, потому что они слишком медленные или разрушают наши системы. Поэтому нам нужны способы фильтрации элементов.


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


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


Вот небольшой пример, где API может принять строку запроса с различными параметрами запроса, чтобы мы могли отфильтровать элементы по их полям:

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

// employees data in a database
const employees = [
  { firstName: 'Jane', lastName: 'Smith', age: 20 },
  //...
  { firstName: 'John', lastName: 'Smith', age: 30 },
  { firstName: 'Mary', lastName: 'Green', age: 50 },
]

app.use(bodyParser.json());

app.get('/employees', (req, res) => {
  const { firstName, lastName, age } = req.query;
  let results = [...employees];
  if (firstName) {
    results = results.filter(r => r.firstName === firstName);
  }

  if (lastName) {
    results = results.filter(r => r.lastName === lastName);
  }

  if (age) {
    results = results.filter(r => +r.age === +age);
  }
  res.json(results);
});

app.listen(3000, () => console.log('server started'));


В приведенном выше коде у нас есть переменная req.query для получения параметров запроса. Затем мы извлекаем значения свойств, деструктурируя отдельные параметры запроса в переменные, используя синтаксис деструктуризации JavaScript. Наконец, мы запускаем фильтр для каждого значения параметра запроса, чтобы найти элементы, которые мы хотим вернуть.


Как только мы это сделаем, мы возвращаем результаты в качестве ответа. Поэтому, когда мы делаем запрос GET по следующему пути со строкой запроса:

/employees?lastName=Smith&age=30


Мы получаем:

[
    {
        "firstName": "John",
        "lastName": "Smith",
        "age": 30
    }
]

как возвращенный ответ, так как мы отфильтровали по lastName и возрасту.


Аналогично, мы можем принять параметр запроса страницы и вернуть группу записей в позиции от (page - 1) * 20 до page * 20.


Мы также можем указать поля для сортировки в строке запроса. Например, мы можем получить параметр из строки запроса с полями, для которых мы хотим отсортировать данные. Затем мы можем отсортировать их по этим отдельным полям.


Например, мы можем извлечь строку запроса из URL, например:

http://example.com/articles?sort=+author,-datepublished


Где + означает восходящий, а - означает нисходящий. Таким образом, мы сортируем по имени автора в алфавитном порядке и по дате публикации из самых последних в наименее недавние.


Соблюдайте правила безопасности.


Большая часть общения между клиентом и сервером должна быть конфиденциальной, поскольку мы часто отправляем и получаем личную информацию. Поэтому использование SSL / TLS для безопасности является обязательным.


Сертификат SSL не слишком сложен для загрузки на сервер, а стоимость бесплатна или очень низкая. Нет никаких причин, чтобы наши REST API не общались по защищенным каналам, а не находились в открытом доступе.


Люди не должны иметь доступа к дополнительной информации, которую они запрашивают. Например, обычный пользователь не должен иметь доступа к информации другого пользователя. Они также не должны иметь доступ к данным администраторов.


Чтобы обеспечить соблюдение принципа наименьших привилегий, нам нужно добавить проверки ролей для одной роли или иметь более детализированные роли для каждого пользователя.


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


Кэширование данных для повышения производительности.


Мы можем добавить кеширование для возврата данных из кеша локальной памяти вместо того, чтобы запрашивать базу данных, чтобы получать данные каждый раз, когда мы хотим получить некоторые данные, которые запрашивают пользователи. Преимущество кэширования в том, что пользователи могут получать данные быстрее. Однако данные, которые получают пользователи, могут быть устаревшими. Это также может привести к проблемам при отладке в производственных средах, когда что-то идет не так, как мы видим старые данные.


Существует много видов решений для кэширования, таких как Redis, кэширование в памяти и многое другое. Мы можем изменить способ кэширования данных при изменении наших потребностей.


Например, Express имеет промежуточное ПО apicache для добавления кэширования в наше приложение без особых настроек. Мы можем добавить простой кэш в памяти на наш сервер следующим образом:

const express = require('express');
const bodyParser = require('body-parser');
const apicache = require('apicache');
const app = express();
let cache = apicache.middleware;
app.use(cache('5 minutes'));

// employees data in a database
const employees = [
  { firstName: 'Jane', lastName: 'Smith', age: 20 },
  //...
  { firstName: 'John', lastName: 'Smith', age: 30 },
  { firstName: 'Mary', lastName: 'Green', age: 50 },
]

app.use(bodyParser.json());

app.get('/employees', (req, res) => {
  res.json(employees);
});

app.listen(3000, () => console.log('server started'));


Приведенный выше код просто ссылается на промежуточное ПО apicache с помощью apicache.middleware, и тогда мы имеем:

app.use(cache('5 minutes'))


применить кеширование ко всему приложению. Например, мы кешируем результаты в течение пяти минут. Мы можем настроить это для наших нужд.


Управление версиями наших API.


У нас должны быть разные версии API, если мы вносим в них какие-либо изменения, которые могут нарушить работу клиентов. Управление версиями может осуществляться в соответствии с семантической версией (например, 2.0.6 для обозначения основной версии 2 и шестого патча), как это делают большинство приложений в настоящее время.


Таким образом, мы можем постепенно свернуть старые конечные точки вместо того, чтобы заставлять всех переходить на новый API одновременно. Конечная точка v1 может оставаться активной для людей, которые не хотят меняться, в то время как v2 с ее блестящими новыми функциями может обслуживать тех, кто готов к обновлению. Это особенно важно, если наш API общедоступен. Мы должны сделать так, чтобы мы не ломали сторонние приложения, использующие наши API.


Управление версиями обычно выполняется с помощью / v1 /, / v2 / и т. Д., Добавляемых в начале пути API.


Например, мы можем сделать это с помощью Express следующим образом:

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());

app.get('/v1/employees', (req, res) => {
  const employees = [];
  // code to get employees
  res.json(employees);
});

app.get('/v2/employees', (req, res) => {
  const employees = [];
  // different code to get employees
  res.json(employees);
});

app.listen(3000, () => console.log('server started'));


Мы просто добавляем номер версии в начало пути URL-адреса конечной точки, чтобы указать версии.


Вывод.


Наиболее важные выводы для разработки высококачественных API REST - это согласованность с соблюдением веб-стандартов и соглашений. Коды состояния JSON, SSL / TLS и HTTP являются стандартными строительными блоками современного Интернета.


Производительность также является важным фактором. Мы можем увеличить его, не возвращая слишком много данных за один раз. Кроме того, мы можем использовать кэширование, чтобы нам не приходилось постоянно запрашивать данные.


Пути конечных точек должны быть согласованными, мы используем только существительные, поскольку методы HTTP указывают действие, которое мы хотим предпринять. Пути вложенных ресурсов должны идти после пути родительского ресурса. Они должны сообщить нам, что мы получаем или манипулируем без необходимости читать дополнительную документацию, чтобы понять, что она делает.