Next.js: маршруты API и обработка ошибок
Настройка среднего уровня для маршрутов API Next.js с Next-Connect и TypeScript.
Введение.
В этом руководстве мы рассмотрим настройку маршрутов API Next.js с помощью next-connect
и создадим модуль, предлагающий стандартизированный и настраиваемый подход к обработке ошибок. Создавая конфигурации ошибок для конкретных кодов ошибок, мы можем создавать согласованные сообщения об ошибках и коды состояния HTTP для широкого диапазона ошибок, которые могут возникнуть в нашем приложении. Это позволит нам беспрепятственно обрабатывать ошибки во внешнем интерфейсе.
// API response: Unknown error. { "name":"UnknownError", "cause":"Server error", "status":500, "clientString": {"en":"An unknown error occurred","es":"Ocurrió un error desconocido"} } // API response: Known error { "name":"Unauthenticated", "cause":"Session not founded", "status":404, "clientString": {"en": "Unauthenticated, please sign in", "es": "No autenticado, por favor inicie sesion"} }
Ошибка расширения JS.
// ./lib/errorException.ts export class Exception extends Error { status: number; clientString: { [key: string]: string }; metaData?: string; constructor(code: ErrorCode, metaData?: string) { super(code); Object.setPrototypeOf(this, new.target.prototype); this.name = code; this.metaData = metaData; const errorConfig = errorConfigMap[code] || errorConfigMap[ErrorCode.UnknownError]; this.status = errorConfig.status; this.clientString = errorConfig.clientString; if (process.env.NODE_ENV === 'production') { // Override the `stack` property with an empty string to hide the call stack in a production environment Object.defineProperty(this, 'stack', { value: '', writable: true, configurable: true }); } else { // Capture the call stack using `Error.captureStackTrace` to show it in a development environment Error.captureStackTrace(this, this.constructor); } } }
Класс Exception
, который мы пишем здесь, расширяет встроенный класс Error
и добавляет несколько пользовательских свойств, таких как status
и clientString
. Он принимает два параметра: code
и metaData
.
Параметр code
указывает код ошибки, который используется для определения кода состояния HTTP и удобного для клиента сообщения для отправки обратно пользователю.
Параметр metaData
задает дополнительную информацию об ошибке для целей отладки.
Внутри конструктора метод Object.setPrototypeOf()
используется для установки прототипа экземпляра в качестве самого класса Exception
. Свойство name
установлено в параметр code
. Свойства status
и clientString
устанавливаются на основе параметра code
путем поиска соответствующей конфигурации в файле errorConfigMap
.
В зависимости от среды свойство stack
объекта ошибки либо задается пустой строкой (в производственной среде), либо захватывает стек вызовов с помощью Error.captureStackTrace
(в среде разработки).
Настройка наших кодов ошибок.
Эти коды могут различаться в зависимости от вашего приложения, но здесь я представляю вам несколько общих исключений, которые могут возникнуть на наших маршрутах API.
// ./lib/errorException.ts type ErrorConfig = { status: number; clientString?: { [key: string]: string }; }; export enum ErrorCode { // status 400-499 InvalidInput = 'InvalidInput', // 400 Unauthenticated = 'Unauthenticated', // 401 UnverifiedAccount = 'UnverifiedAccount', // 401 Forbidden = 'Forbidden', // 403 NotFound = 'NotFound', // 404 MethodNotAllowed = 'MethodNotAllowed', // 405 RequestLimit = 'RequestLimit', // 429 // status 500-599 UnknownError = 'UnknownError', // 500 } const errorConfigMap: Record<ErrorCode, ErrorConfig> = { [ErrorCode.InvalidInput]: { status: 400, clientString: { en: 'Invalid input', es: 'Entrada inválida' } }, [ErrorCode.Unauthenticated]: { status: 401, clientString: { en: 'Unauthenticated, please sign in', es: 'No autenticado, por favor inicie sesion' } }, [ErrorCode.UnverifiedAccount]: { status: 401, clientString: { en: 'Please, verify your account', es: 'Por favor, verifica tu cuenta' } }, [ErrorCode.Forbidden]: { status: 403, clientString: { en: 'You do not have permission to access this resource', es: 'No tiene permiso para acceder a este recurso' } }, [ErrorCode.RequestLimit]: { status: 429, clientString: { en: 'Request limit exceeded, try later', es: 'Límite de solicitudes excedido, intente luego' } }, [ErrorCode.NotFound]: { status: 404, clientString: { en: 'Resource not found', es: 'Recurso no encontrado' } }, [ErrorCode.MethodNotAllowed]: { status: 405, clientString: { en: 'Method not allowed', es: 'Método no permitido' } }, [ErrorCode.UnknownError]: { status: 500, clientString: { en: 'Unknown error occurred', es: 'Ocurrió un error desconocido' } } };
Возможно, вам не нужно настраивать clientString
, но это может помочь быстро доставлять сообщения клиенту.
При определении нашего перечисления ErrorCode
мы определяем уникальный код для передачи нашему Exception
. Это направлено на обработку каждого исключения контролируемым образом.
Настройка обработчика ошибок.
// ./lib/errorHandler.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { ErrorCode, Exception } from './errorException'; export function errorHandler(err: Exception | Error, _: NextApiRequest, res: NextApiResponse) { if (err instanceof Exception) { return res.status(err.status).send(err); } // Here you can further check if the error is an instance of, e.g: a Database error, and send back a proper Exception. console.error(err); const unknownErr = new Exception(ErrorCode.UnknownError, err.stack); return res.status(500).send(unknownErr); } export function noMatchHandler(_: NextApiRequest, res: NextApiResponse) { const matchErr = new Exception(ErrorCode.MethodNotAllowed); return res.status(matchErr.status).send(matchErr); }
Этот код экспортирует две функции: errorHandler
и noMatchHandler
.
errorHandler
принимает параметр err
, который может быть экземпляром класса Exception
или стандартным объектом Error
.
Если параметр err
является экземпляром класса Exception
, errorHandler
отправляет объект err
обратно клиенту в качестве ответа с соответствующим кодом состояния HTTP, указанным в err.status
.
Если параметр err
является стандартным объектом Error
, errorHandler
записывает ошибку в консоль и отправляет новый объект Exception
обратно клиенту с кодом состояния 500.
noMatchHandler
— это обработчик HTTP-запросов, которые не соответствуют ни одному из маршрутов API, определенных в приложении.
noMatchHandler
создает новый объект Exception
с кодом ошибки ErrorCode.MethodNotAllowed
и отправляет новый объект Exception
обратно клиенту с соответствующим кодом состояния HTTP, указанным в matchErr.status
.
Настройка маршрутов API.
В этом посте я не буду рассказывать об основных маршрутах API Next.js, вы можете обратиться к ресурсам. В этом случае мы создадим экземпляр маршрутизатора Node.js с next-connect
. Если вы раньше использовали Node или Express, этот шаблон будет вам знаком. При возникновении ошибки исключение будет передано обработчику ошибок. Дополнительные сведения об этой библиотеке см. в ресурсах.
// ./pages/api/...anyRoute.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; import { errorHandler, noMatchHandler } from '@lib/api/errorHandler'; const router = createRouter<NextApiRequest, NextApiResponse>(); router.get(async (req, res) => { // Handle GET request }); router.post(async (req, res) => { // Handle POST request }); // ... export default router.handler({ onError: errorHandler, onNoMatch: noMatchHandler });
Функция createRouter
создает экземпляр маршрутизатора, который может обрабатывать различные методы HTTP, такие как GET, POST и DELETE. Метод router.handler
используется для определения функций промежуточного ПО для обработки ошибок и обработки несоответствий.
Используйте примеры
Вот несколько наглядных примеров использования нашей установки. В Next.js вы, вероятно, будете использовать генерацию статических сайтов. Поэтому вам нужно будет написать служебные функции для использования в маршрутах API, а также с getStaticProps
. Кроме того, в этих случаях вы должны использовать класс Exception
.
// ./pages/api/users/index.ts // ... router.post(async (req, res) => { if (!req.body.email || !req.body.password) { throw new Exception(ErrorCode.InvalidInput, 'Email and password are required.'); } // ... }); // ... // ./pages/api/users/[id].ts // ... router.get(async (req, res) => { const user = getUserById(req.params.id); if (!user) { throw new Exception(ErrorCode.NotFound, `User with id ${req.params.id} not found.`); } // ... }); // ... // ./pages/api/users/[id].ts // ... router.get(async (req, res) => { const user = getUserById(req.params.id); if (!user) { throw new Exception(ErrorCode.NotFound, `User with id ${req.params.id} not found.`); } // ... }); // ... // ./pages/api/protected.ts // ... router.get(async (req, res) => { if (!req.user) { throw new Exception(ErrorCode.Unauthenticated); } // ... }); // ...