🔥

Тред (Богдан Чадкин)


Сегодня расскажу как я мигрировал больше приложение с express на нативный нодовый сервер. Идея пришла когда я взглянул сколько express тащит ненужного. packagephobia.com/result?p=expre… npm.anvaka.com/#/view/2d/expr… Его API вообще кошмар, особенно с точки зрения типов.

Express патчит req и res своими пропертями и даже перезаписывает нативные. Такая конструкция очень напоминает дженгу. Поэтому в первую очень я стал заменять специфичные для express методы на нативные.

Например, вот JSON ответ res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify(data)) Не такая большая проблема указать один хедер вручную сериализовать данные.

У редиректа аналогичная структура res.writeHead(302, { Location: url }) res.end() Для большинства случаев двух методов writeHead и end достаточно.

Самая неприятная проблема express это отсутствие поддержки async/await. Middleware как бы могут быть асинхронными, но любое исключение приведет к зависшему роуту, а начиная с node 15 вырубит приложение.

Решают эту проблему оборачивая каждый роут в try/catch, что, конечно, не очень практично и легко забыть. const middleware = async (req, res, next) => { try { await rejectedPromise } catch (error) { next(error) } }

Вернемся к началу, как создать нодовый сервер. Для примера буду использовать http. Конфигурация https не сильно отличается. const http = require('http') const server = http.createServer((req, res) => { appRoute(req, res) }) server.listen(port, host)

appRoute сам обрабатывает ошибки и может быть асинхронным. Весь код оборачивается в общий try/catch как централизованный обработчик ошибок. const app = async (req, res) => { try { // app code } catch (error) { res.writeHead(500) res.end(error.message) } }

Если использовать пакет вроде github.com/jshttp/http-er… или кастомные ошибки, можно подменять код с фолбеком на 500 res.writeHead(error.code ?? 500)

Если код ни один роут не сработал, выкидываем 404. res.headersSent - полпулярный способ проверки, отправился ли респонз. try { // app code if (res.headersSent === false) { throw new NotFoundError(); } } catch(error) {...}

Теперь приступим к роутам. Отделим желток от белка const [pathname, search] = req.url.split('?') и взобьем венчиком if (pathname === '/users' && req.method === 'GET') { await usersListRoute(req, res) } Вуаля, во многих случаях в роутере нет необходимости.

Однако бывают требования с параметрами в роутах, к примеру, /user/:id. Воспользуемся старым добрым регэкспом const userByIdMatch = pathname.match(/^/user/([^/]+)$/) if (userByIdMatch) { await userByIdRoute(req, res, { id: userByIdMatch[1] }) }

Если приложение большое и параметров много, можно воспользоваться микро роутером, с которым можно подстраивать под себя как душе угодно. github.com/lukeed/trouter Вот пример в sofa-api github.com/Urigo/SOFA/blo…

Альтернатива req.query В ноде уже давно появилcz совместимыq с вебом класс URLSearchParams. Он проще с точки зрения типов чем querystring, qs, qss и тп. Нет необходимости проверять является ли параметр массивом или строкой.

const [pathname, search] = req.url.split('?') const searchParams = new URLSearchParams(search) searchParams.get('id') searchParams.getAll('item')

Альтернатива req.body Каждый роут знает в каком формате и какие данные ему необходимы. К примеру данные переданы постом как json. Чтобы получить строку из стрима, воспользуемся микро пакетом get-stream, а затем парсим результат. const data = JSON.parseawait getStream(req))

И наконец, что если необходимо переиспользовать готовый express middleware. await new Promise((resolve, reject) => { middleware(req, res, (error) => { if (error) reject(error) else resolve() }) })

очепятка const data = JSON.parse(await getStream(req))

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