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);
  }
  // ...
});
// ...

Ресурсы