Создание Node.js/TypeScript REST API, часть 2: модели, промежуточное ПО и службы
Опубликовано: 2022-03-11В первой статье нашей серии REST API мы рассмотрели, как использовать npm для создания серверной части с нуля, добавления зависимостей, таких как TypeScript, использования модуля debug , встроенного в Node.js, создания структуры проекта Express.js и регистрации времени выполнения. события гибко с Winston. Если вы уже знакомы с этими концепциями, просто клонируйте это, переключитесь на toptal-article-01 с помощью git checkout и читайте дальше.
Службы REST API, ПО промежуточного слоя, контроллеры и модели
Как и было обещано, теперь мы подробно рассмотрим эти модули:
- Сервисы , которые делают наш код чище, инкапсулируя операции бизнес-логики в функции, которые могут вызывать промежуточное ПО и контроллеры.
- ПО промежуточного слоя , которое будет проверять предварительные условия перед тем, как Express.js вызовет соответствующую функцию контроллера.
- Контроллеры , которые используют службы для обработки запроса, прежде чем окончательно отправить ответ запрашивающей стороне.
- Модели , которые описывают наши данные и помогают в проверках во время компиляции.
Мы также добавим очень примитивную базу данных, которая никак не подходит для производства. (Его единственная цель — упростить освоение этого руководства, проложив путь для нашей следующей статьи, посвященной подключению к базе данных и интеграции с MongoDB и Mongoose.)
Практическое занятие: первые шаги с DAO, DTO и нашей временной базой данных
В этой части нашего руководства наша база данных даже не будет использовать файлы. Он просто будет хранить пользовательские данные в массиве, что означает, что данные испаряются всякий раз, когда мы выходим из Node.js. Он будет поддерживать только самые основные операции создания, чтения, обновления и удаления (CRUD).
Здесь мы будем использовать два понятия:
- Объекты доступа к данным (DAO)
- Объекты передачи данных (DTO)
Эта разница в одну букву между аббревиатурами имеет важное значение: DAO отвечает за подключение к определенной базе данных и выполнение операций CRUD; DTO — это объект, содержащий необработанные данные, которые DAO будет отправлять в базу данных и получать из нее.
Другими словами, DTO — это объекты, соответствующие типам моделей данных, а DAO — это сервисы, которые их используют.
Хотя DTO могут быть более сложными — например, представлять вложенные объекты базы данных — в этой статье один экземпляр DTO будет соответствовать определенному действию в одной строке базы данных.
Почему DTO?
Использование DTO для приведения наших объектов TypeScript в соответствие с нашими моделями данных помогает поддерживать согласованность архитектуры, как мы увидим в разделе, посвященном службам ниже. Но есть важная оговорка: ни DTO, ни сам TypeScript не обещают какой-либо автоматической проверки ввода пользователя, поскольку это должно происходить во время выполнения. Когда наш код получает пользовательский ввод в конечной точке нашего API, этот ввод может:
- Иметь дополнительные поля
- Отсутствуют обязательные поля (т. е. те, у которых нет суффикса
?) - Иметь поля, в которых данные не относятся к тому типу, который мы указали в нашей модели с помощью TypeScript.
TypeScript (и JavaScript, в который он транспилируется) не волшебным образом проверит это для нас, поэтому важно не забывать об этих проверках, особенно при открытии вашего API для публики. В этом могут помочь такие пакеты, как ajv, но обычно они работают, определяя модели в объекте схемы, специфичном для библиотеки, а не в родном TypeScript. (Мангуст, о котором пойдет речь в следующей статье, сыграет аналогичную роль в этом проекте.)
Вы можете подумать: «Неужели лучше использовать и DAO, и DTO, а не что-то более простое?» Корпоративный разработчик Гюнтер Попп предлагает ответ; вам следует избегать DTO в большинстве небольших реальных проектов Express.js/TypeScript, если вы не можете разумно рассчитывать на масштабирование в среднесрочной перспективе.
Но даже если вы не собираетесь использовать их в производственной среде, этот пример проекта — полезная возможность на пути к освоению архитектуры TypeScript API. Это отличный способ попрактиковаться в использовании типов TypeScript дополнительными способами и работе с DTO, чтобы увидеть, как они сравниваются с более простым подходом при добавлении компонентов и моделей.
Наша модель пользовательского REST API на уровне TypeScript
Сначала мы определим три DTO для нашего пользователя. Давайте создадим папку с именем dto внутри папки users и создадим там файл с именем create.user.dto.ts , содержащий следующее:
export interface CreateUserDto { id: string; email: string; password: string; firstName?: string; lastName?: string; permissionLevel?: number; }Мы говорим, что каждый раз, когда мы создаем пользователя, независимо от базы данных, он должен иметь идентификатор, пароль и адрес электронной почты, а также, возможно, имя и фамилию. Эти требования могут меняться в зависимости от бизнес-требований данного проекта.
Для запросов PUT мы хотим обновить весь объект, поэтому теперь требуются наши необязательные поля. В той же папке создайте файл с именем put.user.dto.ts с таким кодом:
export interface PutUserDto { id: string; email: string; password: string; firstName: string; lastName: string; permissionLevel: number; } Для запросов PATCH мы можем использовать функцию Partial из TypeScript, которая создает новый тип, копируя другой тип и делая все его поля необязательными. Таким образом, файл patch.user.dto.ts должен содержать только следующий код:
import { PutUserDto } from './put.user.dto'; export interface PatchUserDto extends Partial<PutUserDto> {} Теперь давайте создадим временную базу данных в памяти. Давайте создадим папку с именем daos внутри папки users и добавим файл с именем users.dao.ts .
Во-первых, мы хотим импортировать созданные нами DTO:
import { CreateUserDto } from '../dto/create.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; import { PutUserDto } from '../dto/put.user.dto';Теперь, чтобы обрабатывать наши идентификаторы пользователей, давайте добавим библиотеку shortid (используя терминал):
npm i shortid npm i --save-dev @types/shortid Вернувшись в users.dao.ts , мы импортируем shortid:
import shortid from 'shortid'; import debug from 'debug'; const log: debug.IDebugger = debug('app:in-memory-dao'); Теперь мы можем создать класс UsersDao , который будет выглядеть так:
class UsersDao { users: Array<CreateUserDto> = []; constructor() { log('Created new instance of UsersDao'); } } export default new UsersDao(); Используя шаблон singleton, этот класс всегда будет предоставлять один и тот же экземпляр — и, что очень важно, один и тот же массив users — когда мы импортируем его в другие файлы. Это связано с тем, что Node.js кэширует этот файл везде, где он импортируется, и весь импорт происходит при запуске. То есть любому файлу, ссылающемуся на users.dao.ts , будет передана ссылка на тот же new UsersDao() , который экспортируется при первой обработке Node.js этого файла.
Мы увидим, как это работает, когда мы будем использовать этот класс далее в этой статье, и использовать этот общий шаблон TypeScript/Express.js для большинства классов во всем проекте.
Примечание. Часто упоминаемый недостаток синглетонов заключается в том, что для них сложно писать модульные тесты. В случае многих наших классов этот недостаток не применим, поскольку нет никаких переменных-членов класса, которые нужно было бы сбрасывать. Но для тех, где это возможно, мы оставляем читателю в качестве упражнения рассмотреть возможность решения этой проблемы с использованием внедрения зависимостей.
Теперь мы собираемся добавить основные операции CRUD в класс как функции. Функция создания будет выглядеть так:
async addUser(user: CreateUserDto) { user.id = shortid.generate(); this.users.push(user); return user.id; }Чтение будет доступно в двух вариантах: «прочитать все ресурсы» и «прочитать один по идентификатору». Они закодированы следующим образом:
async getUsers() { return this.users; } async getUserById(userId: string) { return this.users.find((user: { id: string }) => user.id === userId); } Точно так же обновление будет означать перезапись всего объекта (как PUT ) или только части объекта (как PATCH ):
async putUserById(userId: string, user: PutUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1, user); return `${user.id} updated via put`; } async patchUserById(userId: string, user: PatchUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); let currentUser = this.users[objIndex]; const allowedPatchFields = [ 'password', 'firstName', 'lastName', 'permissionLevel', ]; for (let field of allowedPatchFields) { if (field in user) { // @ts-ignore currentUser[field] = user[field]; } } this.users.splice(objIndex, 1, currentUser); return `${user.id} patched`; } Как упоминалось ранее, несмотря на наше объявление UserDto в этих сигнатурах функций, TypeScript не обеспечивает проверку типов во время выполнения. Это означает, что:
-
putUserById()имеет ошибку. Это позволит потребителям API хранить значения для полей, которые не являются частью модели, определенной нашим DTO. -
patchUserById()зависит от дублирующегося списка имен полей, которые должны синхронизироваться с моделью. Без этого ему пришлось бы использовать обновляемый объект для этого списка. Это означало бы, что он будет молча игнорировать значения для полей, которые являются частью модели, определенной DTO, но не были сохранены ранее для этого конкретного экземпляра объекта.
Но оба эти сценария будут правильно обработаны на уровне базы данных в следующей статье.
Последняя операция по удалению ресурса будет выглядеть так:
async removeUserById(userId: string) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1); return `${userId} removed`; }В качестве бонуса, зная, что предварительным условием для создания пользователя является проверка того, не дублируется ли адрес электронной почты пользователя, давайте добавим функцию «получить пользователя по электронной почте» прямо сейчас:
async getUserByEmail(email: string) { const objIndex = this.users.findIndex( (obj: { email: string }) => obj.email === email ); let currentUser = this.users[objIndex]; if (currentUser) { return currentUser; } else { return null; } }Примечание. В реальном сценарии вы, вероятно, будете подключаться к базе данных с помощью уже существующей библиотеки, такой как Mongoose или Sequelize, которая абстрагирует все основные операции, которые могут вам понадобиться. По этой причине мы не будем вдаваться в детали реализованных выше функций.
Наш уровень сервисов REST API
Теперь, когда у нас есть базовый DAO в памяти, мы можем создать службу, которая будет вызывать функции CRUD. Поскольку функции CRUD — это то, что должна иметь каждая служба, которая будет подключаться к базе данных, мы собираемся создать интерфейс CRUD , содержащий методы, которые мы хотим реализовать каждый раз, когда мы хотим реализовать новую службу.
В настоящее время IDE, с которыми мы работаем, имеют функции генерации кода для добавления функций, которые мы реализуем, уменьшая количество повторяющегося кода, который нам нужно писать.
Быстрый пример использования WebStorm IDE:
Среда IDE выделяет имя класса MyService и предлагает следующие варианты:
Параметр «Реализовать все элементы» мгновенно формирует функции, необходимые для соответствия интерфейсу CRUD :
Итак, давайте сначала создадим наш интерфейс TypeScript с именем CRUD . В нашей common папке давайте создадим папку с именем interfaces и добавим crud.interface.ts со следующим:
export interface CRUD { list: (limit: number, page: number) => Promise<any>; create: (resource: any) => Promise<any>; putById: (id: string, resource: any) => Promise<string>; readById: (id: string) => Promise<any>; deleteById: (id: string) => Promise<string>; patchById: (id: string, resource: any) => Promise<string>; } После этого давайте создадим папку services в папке users и добавим туда файл users.service.ts , содержащий:
import UsersDao from '../daos/users.dao'; import { CRUD } from '../../common/interfaces/crud.interface'; import { CreateUserDto } from '../dto/create.user.dto'; import { PutUserDto } from '../dto/put.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; class UsersService implements CRUD { async create(resource: CreateUserDto) { return UsersDao.addUser(resource); } async deleteById(id: string) { return UsersDao.removeUserById(id); } async list(limit: number, page: number) { return UsersDao.getUsers(); } async patchById(id: string, resource: PatchUserDto) { return UsersDao.patchUserById(id, resource); } async readById(id: string) { return UsersDao.getUserById(id); } async putById(id: string, resource: PutUserDto) { return UsersDao.putUserById(id, resource); } async getUserByEmail(email: string) { return UsersDao.getUserByEmail(email); } } export default new UsersService(); Нашим первым шагом здесь было импортировать наш DAO в памяти, нашу зависимость интерфейса и тип TypeScript каждого из наших DTO, пришло время реализовать UsersService как одноэлементный сервис, тот же шаблон, который мы использовали с нашим DAO.
Все функции CRUD теперь просто вызывают соответствующие функции UsersDao . Когда придет время заменить DAO, нам больше не придется вносить изменения в проект, за исключением некоторых изменений в этом файле, где вызываются функции DAO, как мы увидим в части 3.
Например, нам не нужно будет отслеживать каждый вызов list() и проверять его контекст перед его заменой. Это преимущество наличия этого слоя разделения за счет небольшого количества начального шаблона, который вы видите выше.
Async/Await и Node.js
Наше использование async для сервисных функций может показаться бессмысленным. На данный момент это так: все эти функции сразу же возвращают свои значения, без какого-либо внутреннего использования Promise или await . Это делается исключительно для того, чтобы подготовить нашу кодовую базу для сервисов, которые будут использовать async . Точно так же ниже вы увидите, что все вызовы этих функций используют await .
К концу этой статьи у вас снова будет работоспособный проект для экспериментов. Это отличный момент, чтобы попробовать добавить различные типы ошибок в разные места кодовой базы и посмотреть, что происходит во время компиляции и тестирования. В частности, ошибки в async контексте могут вести себя не совсем так, как вы ожидаете. Стоит покопаться и изучить различные решения, которые выходят за рамки этой статьи.

Теперь, когда наш DAO и сервисы готовы, давайте вернемся к пользовательскому контроллеру.
Создание нашего контроллера REST API
Как мы уже говорили выше, идея контроллеров состоит в том, чтобы отделить конфигурацию маршрута от кода, который, наконец, обрабатывает запрос маршрута. Это означает, что все проверки должны быть выполнены до того, как наш запрос достигнет контроллера. Контроллеру нужно только знать, что делать с фактическим запросом, потому что, если запрос зашел так далеко, мы знаем, что он оказался действительным. Затем контроллер будет вызывать соответствующую службу каждого запроса, который он будет обрабатывать.
Прежде чем мы начнем, нам нужно установить библиотеку для безопасного хеширования пароля пользователя:
npm i argon2 Давайте начнем с создания папки с именем controllers внутри папки контроллеров users и создания в ней файла с именем users.controller.ts :
// we import express to add types to the request/response objects from our controller functions import express from 'express'; // we import our newly created user services import usersService from '../services/users.service'; // we import the argon2 library for password hashing import argon2 from 'argon2'; // we use debug with a custom context as described in Part 1 import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersController { async listUsers(req: express.Request, res: express.Response) { const users = await usersService.list(100, 0); res.status(200).send(users); } async getUserById(req: express.Request, res: express.Response) { const user = await usersService.readById(req.body.id); res.status(200).send(user); } async createUser(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); const userId = await usersService.create(req.body); res.status(201).send({ id: userId }); } async patch(req: express.Request, res: express.Response) { if (req.body.password) { req.body.password = await argon2.hash(req.body.password); } log(await usersService.patchById(req.body.id, req.body)); res.status(204).send(); } async put(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); log(await usersService.putById(req.body.id, req.body)); res.status(204).send(); } async removeUser(req: express.Request, res: express.Response) { log(await usersService.deleteById(req.body.id)); res.status(204).send(); } } export default new UsersController(); Примечание. Строки выше ничего не возвращают с ответом HTTP 204 No Content , что соответствует RFC 7231 по этой теме.
После того, как наш синглтон пользовательского контроллера готов, мы готовы закодировать другой модуль, который зависит от нашей примерной объектной модели и службы REST API: нашего пользовательского промежуточного программного обеспечения.
ПО промежуточного слоя REST Node.js с Express.js
Что мы можем сделать с промежуточным ПО Express.js? Во-первых, валидации отлично подходят. Давайте добавим некоторые базовые проверки, чтобы действовать как привратники для запросов, прежде чем они попадут в наш пользовательский контроллер:
- Убедитесь в наличии пользовательских полей, таких как
emailиpassword, необходимых для создания или обновления пользователя. - Убедитесь, что данный адрес электронной почты еще не используется
- Убедитесь, что мы не меняем поле
emailпосле создания (поскольку для простоты мы используем его в качестве основного идентификатора пользователя). - Проверить, существует ли данный пользователь
Чтобы эти проверки работали с Express.js, нам нужно будет преобразовать их в функции, которые следуют шаблону управления потоком Express.js с использованием next() , как описано в предыдущей статье. Нам понадобится новый файл, users/middleware/users.middleware.ts :
import express from 'express'; import userService from '../services/users.service'; import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersMiddleware { } export default new UsersMiddleware();Избавившись от знакомого одноэлементного шаблона, давайте добавим некоторые из наших промежуточных функций в тело класса:
async validateRequiredUserBodyFields( req: express.Request, res: express.Response, next: express.NextFunction ) { if (req.body && req.body.email && req.body.password) { next(); } else { res.status(400).send({ error: `Missing required fields email and password`, }); } } async validateSameEmailDoesntExist( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user) { res.status(400).send({ error: `User email already exists` }); } else { next(); } } async validateSameEmailBelongToSameUser( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user && user.id === req.params.userId) { next(); } else { res.status(400).send({ error: `Invalid email` }); } } // Here we need to use an arrow function to bind `this` correctly validatePatchEmail = async ( req: express.Request, res: express.Response, next: express.NextFunction ) => { if (req.body.email) { log('Validating email', req.body.email); this.validateSameEmailBelongToSameUser(req, res, next); } else { next(); } }; async validateUserExists( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.readById(req.params.userId); if (user) { next(); } else { res.status(404).send({ error: `User ${req.params.userId} not found`, }); } } Чтобы облегчить нашим потребителям API дальнейшие запросы о вновь добавленном пользователе, мы добавим вспомогательную функцию, которая будет извлекать userId из параметров запроса, поступающих из самого URL-адреса запроса, и добавлять его в тело запроса, где находятся остальные пользовательские данные.
Идея состоит в том, чтобы иметь возможность просто использовать полный запрос тела, когда мы хотим обновить информацию о пользователе, не беспокоясь о том, чтобы каждый раз получать идентификатор из параметров. Вместо этого об этом заботится только одно место — промежуточное ПО. Функция будет выглядеть так:
async extractUserId( req: express.Request, res: express.Response, next: express.NextFunction ) { req.body.id = req.params.userId; next(); } Помимо логики, основное различие между промежуточным программным обеспечением и контроллером заключается в том, что теперь мы используем функцию next() для передачи управления по цепочке сконфигурированных функций до тех пор, пока оно не достигнет конечного пункта назначения, которым в нашем случае является контроллер.
Собираем все вместе: рефакторинг наших маршрутов
Теперь, когда мы реализовали все новые аспекты архитектуры нашего проекта, давайте вернемся к файлу users.routes.config.ts , который мы определили в предыдущей статье. Он будет вызывать наше промежуточное ПО и наши контроллеры, оба из которых полагаются на нашу пользовательскую службу, которая, в свою очередь, требует нашей пользовательской модели.
Окончательный файл будет таким же простым, как этот:
import { CommonRoutesConfig } from '../common/common.routes.config'; import UsersController from './controllers/users.controller'; import UsersMiddleware from './middleware/users.middleware'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes(): express.Application { this.app .route(`/users`) .get(UsersController.listUsers) .post( UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailDoesntExist, UsersController.createUser ); this.app.param(`userId`, UsersMiddleware.extractUserId); this.app .route(`/users/:userId`) .all(UsersMiddleware.validateUserExists) .get(UsersController.getUserById) .delete(UsersController.removeUser); this.app.put(`/users/:userId`, [ UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailBelongToSameUser, UsersController.put, ]); this.app.patch(`/users/:userId`, [ UsersMiddleware.validatePatchEmail, UsersController.patch, ]); return this.app; } } Здесь мы переопределили наши маршруты, добавив промежуточное ПО для проверки нашей бизнес-логики и соответствующие функции контроллера для обработки запроса, если все верно. Мы также использовали .param() из Express.js для извлечения userId пользователя.
В функции .all() мы передаем нашу функцию validateUserExists из UsersMiddleware для вызова до того, как какие-либо GET , PUT , PATCH или DELETE смогут пройти через конечную точку /users/:userId . Это означает, что validateUserExists не обязательно должен быть в дополнительных массивах функций, которые мы передаем в .put() или .patch() — он будет вызываться перед указанными там функциями.
Мы использовали присущую промежуточному программному обеспечению возможность повторного использования еще одним способом. UsersMiddleware.validateRequiredUserBodyFields для использования в контекстах POST и PUT , мы элегантно комбинируем его с другими функциями промежуточного программного обеспечения.
Отказ от ответственности: в этой статье мы рассматриваем только базовые проверки. В реальном проекте вам нужно будет подумать и найти все ограничения, необходимые для кодирования. Для простоты мы также предполагаем, что пользователь не может изменить свою электронную почту.
Тестирование нашего REST API Express/TypeScript
Теперь мы можем скомпилировать и запустить наше приложение Node.js. После запуска мы готовы протестировать наши маршруты API с помощью клиента REST, такого как Postman или cURL.
Давайте сначала попробуем получить наших пользователей:
curl --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json'В этот момент у нас будет пустой массив в качестве ответа, что является точным. Теперь мы можем попробовать создать первый пользовательский ресурс следующим образом:
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json'Обратите внимание, что теперь наше приложение Node.js будет возвращать ошибку из промежуточного программного обеспечения:
{ "error": "Missing required fields email and password" } Чтобы исправить это, давайте отправим действительный запрос на публикацию в ресурс /users :
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23" }'На этот раз мы должны увидеть что-то вроде следующего:
{ "id": "ksVnfnPVW" } Этот id является идентификатором вновь созданного пользователя и будет другим на вашем компьютере. Чтобы упростить оставшиеся операторы тестирования, вы можете запустить эту команду с той, которую вы получили (при условии, что вы используете среду, подобную Linux):
REST_API_EXAMPLE_ Теперь мы можем увидеть ответ, который мы получаем при выполнении запроса GET с использованием указанной выше переменной:
curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' Теперь мы также можем обновить весь ресурс с помощью следующего запроса PUT :
curl --request PUT "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23", "firstName": "Marcos", "lastName": "Silva", "permissionLevel": 8 }'Мы также можем проверить, работает ли наша проверка, изменив адрес электронной почты, что должно привести к ошибке.
Обратите внимание, что при использовании PUT для идентификатора ресурса нам, как потребителям API, необходимо отправить весь объект, если мы хотим соответствовать стандартному шаблону REST. Это означает, что если мы хотим обновить только поле lastName , но используя нашу конечную точку PUT , мы будем вынуждены отправить весь объект для обновления. Было бы проще использовать запрос PATCH , так как он все еще находится в рамках стандартных ограничений REST для отправки только поля lastName :
curl --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "lastName": "Faraco" }' Напомним, что в нашей собственной кодовой базе это наша конфигурация маршрута, которая обеспечивает это различие между PUT и PATCH с помощью функций промежуточного программного обеспечения, которые мы добавили в этой статье.
PUT , PATCH или и то, и другое?
Может показаться, что нет особых причин поддерживать PUT , учитывая гибкость PATCH , и некоторые API используют этот подход. Другие могут настаивать на поддержке PUT , чтобы сделать API «полностью совместимым с REST», и в этом случае создание маршрутов PUT для каждого поля может быть подходящей тактикой для распространенных случаев использования.
На самом деле эти вопросы являются частью гораздо более широкого обсуждения, начиная от реальных различий между ними и заканчивая более гибкой семантикой только для PATCH . Мы представляем здесь поддержку PUT и широко используемую семантику PATCH для простоты, но призываем читателей углубиться в дальнейшие исследования, когда они почувствуют себя готовыми к этому.
Получив список пользователей снова, как мы делали выше, мы должны увидеть нашего созданного пользователя с обновленными полями:
[ { "id": "ksVnfnPVW", "email": "[email protected]", "password": "$argon2i$v=19$m=4096,t=3,p=1$ZWXdiTgb922OvkNAdh9acA$XUXsOHaRN4uVg5ltIwwO+SPLxvb9uhOKcxoLER1e/mM", "firstName": "Marcos", "lastName": "Faraco", "permissionLevel": 8 } ]Наконец, мы можем протестировать удаление пользователя следующим образом:
curl --request DELETE "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json'Получив список пользователей снова, мы должны увидеть, что удаленного пользователя больше нет.
При этом у нас работают все операции CRUD для users ресурса.
Node.js/TypeScript REST API
В этой части серии мы подробно рассмотрели ключевые этапы создания REST API с помощью Express.js. Мы разделили наш код на поддержку сервисов, промежуточного ПО, контроллеров и моделей. У каждой из их функций есть определенная роль, будь то проверка, логические операции или обработка допустимых запросов и ответ на них.
Мы также создали очень простой способ хранения данных с (извините за каламбур) явной целью разрешить некоторое тестирование на этом этапе, а затем заменить его чем-то более практичным в следующей части нашей серии.
Помимо создания API с учетом простоты — например, с использованием одноэлементных классов — есть несколько шагов, которые необходимо предпринять, чтобы сделать его более простым в обслуживании, более масштабируемым и безопасным. В последней статье цикла мы рассмотрим:
- Замена базы данных в памяти на MongoDB, а затем использование Mongoose для упрощения процесса кодирования.
- Добавление уровня безопасности и управление доступом в подходе без сохранения состояния с помощью JWT
- Настройка автоматического тестирования, позволяющая масштабировать наше приложение
Вы можете просмотреть окончательный код из этой статьи здесь.
