Наконец, настал этот момент, и я решился написать статью. Давно хотел, но как-то не хватало мотивации. А ведь, знаете, как говорят: «гнев — лучший мотиватор». Есть же такое выражение?

Предыстория

Я приглашаю вас в путешествие, но сначала нужно расставить декорации. Представьте, что вы работаете в некой компании X, и один из ваших сервисов на Next.js крякнулся. Ну и поскольку это Next.js, то мы понятия не имеем, что конкретно произошло, так как логирование процессов по умолчанию включено только при разработке.

И теперь перед нами квест — найти и настроить механизм логирования для продакшена. Будет нелегко, но нам как бы не привыкать.

Промежуточный слой

Первым на своём пути мы встречаем промежуточное ПО. В документации даже сказано:

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

Хорошо, вроде ничего сложного. Пора выбирать библиотеку логирования. Я обратился к pino, так как уже с ней знаком. Хотя любое решение будет лучше, чем console.log. Думаю, разберёмся с этим до обеда.

Начнём с настройки основного промежуточного ПО:

// middleware.ts

import { NextResponse, NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
  return new NextResponse.next({
    request: request,
    headers: request.headers,
    // status: 200,
    // statusText: 'OK'
  });
}

export const config = {
  matcher: "/:path*",
};

Думаю, что у нас уже возникла проблема. Из своего промежуточного ПО мы можем передать не более 4 параметров. Единственное, что реально влияет на задействованный маршрут, это headers. Давайте не будем упускать тот факт, что нельзя использовать несколько промежуточных программ или связывать их в цепочку. Как же можно было так налажать? Мы используем программные прослойки с начала 2010-х, когда только появился Express.

Как бы то ни было, мы достаточно умны, и можем воспользоваться изящными решениями, которые предлагает нам современный Node.js. Обратимся к AsyncLocalStorage.

// app/logger.ts
import { AsyncLocalStorage } from "async_hooks";
import { Logger, pino } from "pino";

const loggerInstance = pino({
  // Необходимая конфигурация.
  level: process.env.LOG_LEVEL ?? "trace",
});

export const LoggerStorage = new AsyncLocalStorage<Logger>();

export function logger(): Logger | null {
  return LoggerStorage.getStore() ?? null;
}

export function requestLogger(): Logger {
  return loggerInstance.child({ requestId: crypto.randomUUID() });
}

// middleware.ts
export async function middleware(request: NextRequest) {
  LoggerStorage.enterWith(requestLogger());
  logger()?.debug({ url: request.url }, "Started processing request!");

  return NextResponse.next();
}

Уфф…самое тяжёлое позади. Теперь протестируем всё это. Переходим на localhost:3000 и видим следующее:

{ requestId: 'ec7718fa-b1a2-473e-b2e2-8f51188efa8f' } { url: 'http://localhost:3000/' } 'Started processing request!'
 GET / 200 in 71ms
{ requestId: '09b526b1-68f4-4e90-971f-b0bc52ad167c' } { url: 'http://localhost:3000/next.svg' } 'Started processing request!'
{ requestId: '481dd2ff-e900-4985-ae15-0b0a1eb5923f' } { url: 'http://localhost:3000/vercel.svg' } 'Started processing request!'
{ requestId: 'e7b29301-171c-4c91-af25-771471502ee4' } { url: 'http://localhost:3000/file.svg' } 'Started processing request!'
{ requestId: '13766de3-dd00-42ce-808a-ac072dcfd4c6' } { url: 'http://localhost:3000/window.svg' } 'Started processing request!'
{ requestId: '317e054c-1a9a-4dd8-ba21-4c0201fbeada' } { url: 'http://localhost:3000/globe.svg' } 'Started processing request!'

Не знаю, использовали ли вы pino ранее, но так быть не должно. А можете понять, почему?

Я не Next.js и томить вас ожиданиями не стану. Это вывод браузера. Почему? Ну, потому что по умолчанию средой выполнения промежуточного ПО в Next.js является edje. Да, мы можем переключиться на среду nodejs, которая должна нормально заработать. Вот только на деле это может оказаться не так.

Я пробовал такой подход в свеженьком проекте Next.js, и у меня получилось. Но вот повторить это в реальном проекте мне не удалось. Не подумайте, я не сумасшедший. Ну да ладно, основная проблема всё равно не в этом. Мы постепенно к ней приближаемся.

Перелистывая местные хроники безумств

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

// app/page.tsx
export default function Home() {
  logger()?.info("Logging from the page!");
 
  return <div>Real simple website!</div>
}

 Теперь обновляем страницу и получаем: 

✓ Compiled / in 16ms
 GET / 200 in 142ms

И всё? И всё. Ничего. Совсем.

Для сохранения исторической ясности покажу, как этот вывод должен выглядеть:

✓ Compiled / in 2.2s
[11:38:59.259] INFO (12599): Logging from the page!
    requestId: "2ddef9cf-6fee-4d1d-8b1e-6bb16a3e636b"
 GET / 200 in 2520ms

Ладно, что-то я затянул, пора переходить к сути. Функция logger возвращает null. Почему? Не уверен, но мне кажется, что рендеринг выполняется не в том же асинхронном контексте, что и промежуточное ПО.

И что с этим делать? Вы не поверите. Помните, что из промежуточной программы можно передать лишь одно значение — headers? Да, именно это нам и нужно.

Следующий код не для слабонервных:

// app/log/serverLogger.ts
import { pino } from "pino";

export const loggerInstance = pino({
  // Необходимая конфигурация.
  level: process.env.LOG_LEVEL ?? "info",
});

// app/log/middleware.ts
// Да, нужно разделить логгеры ...
// Здесь почти всё то же самое.
import { loggerInstance } from "./serverLogger";

export function requestLogger(requestId: string): Logger {
  return loggerInstance.child({ requestId });
}

// app/log/server.ts
import { headers } from "next/headers";
import { loggerInstance } from "./serverLogger";
import { Logger } from "pino";
import { NextRequest } from "next/server";

const REQUEST_ID_HEADER = "dominik-request-id";

export function requestHeaders(
  request: NextRequest,
  requestId: string,
): Headers {
  const head = new Headers(request.headers);
  head.set(REQUEST_ID_HEADER, requestId);
  return head;
} 

// Да, эта функция должна быть асинхронной ...
export async function logger(): Promise<Logger> {
  const hdrs = await headers();
  const requestId = hdrs.get(REQUEST_ID_HEADER);
 
  return loggerInstance.child({ requestId });
}
 
// middleware.ts
import { logger, LoggerStorage, requestLogger } from "./app/log/middleware";
import { requestHeaders } from "./app/log/server"; 

export async function middleware(request: NextRequest) {
  const requestId = crypto.randomUUID();
  LoggerStorage.enterWith(requestLogger(requestId));
 
  logger()?.debug({ url: request.url }, "Started processing request!");

  return NextResponse.next({ headers: requestHeaders(request, requestId) });
}

// app/page.tsx
export default async function Home() {
  (await logger())?.info("Logging from the page!");

  // ...
}

Разве не прекрасно? Мне особенно нравится, что теперь можно импортировать код логирования промежуточного слоя с сервера. Естественно, работать он не будет. Или, наоборот, импортировать код логирования сервера из промежуточного слоя. Который тоже работать не будет. Здесь важно ничего не напутать. И это мы ещё не говорили о логировании в клиентских компонентах, которые, вопреки своему названию, тоже выполняются на сервере. Да, это уже третье разделение.

Вас принимают за детей

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

Думаю, что каждый из нас достигал в своей жизни некой точки, когда чувствовал, что с него хватит. Для меня эта точка возникла здесь. К чёрту! Давайте использовать кастомный сервер.

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

Взглянем на пример из документации:

import { createServer } from 'http'
import { parse } from 'url'
import next from 'next'

const port = parseInt(process.env.PORT || '3000', 10)
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url!, true)
    handle(req, res, parsedUrl)
  }).listen(port)

  console.log(
    `> Server listening at http://localhost:${port} as ${
      dev ? 'development' : process.env.NODE_ENV
    }`
  )
})

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

Как бы то ни было, у нас есть AsyncLocalStorage, так что волноваться не стоит. Давайте слегка перепишем этот пример.

// app/logger.ts
// Возвращаемся к нашей вариации с AsyncLocalStorage.
import { pino, Logger } from "pino";
import { AsyncLocalStorage } from "async_hooks";

const loggerInstance = pino({
  // Вся необходимая конфигурация.
  level: process.env.LOG_LEVEL ?? "info",
});

export const LoggerStorage = new AsyncLocalStorage<Logger>();

export function logger(): Logger | null {
  return LoggerStorage.getStore() ?? null;
}

export function requestLogger(): Logger {
  return loggerInstance.child({ requestId: crypto.randomUUID() });
}

// server.ts
import { logger, LoggerStorage, requestLogger } from "./app/logger";

app.prepare().then(() => {
  createServer(async (req, res) => {
    // Новый код.
    LoggerStorage.enterWith(requestLogger());
    logger()?.info({}, "Logging from server!");

    const parsedUrl = parse(req.url!, true);
    await handle(req, res, parsedUrl);
  }).listen(port);
});

// middleware.ts
import { logger } from "./app/logger";

export async function middleware(request: NextRequest) {
  logger()?.info({}, "Logging from middleware!");
  return NextResponse.next();
}

// app/page.tsx
import { logger } from "./logger";

export default async function Home() {
  logger()?.info("Logging from the page!");

  // ...
}

Хорошо, теперь протестируем наше решение. Обновляем браузер, и …

> Server listening at http://localhost:3000 as development
[12:29:52.183] INFO (19938): Logging from server!
    requestId: "2ffab9a2-7e15-4188-8959-a7822592108f"
 ✓ Compiled /middleware in 388ms (151 modules)
 ○ Compiling / ...
 ✓ Compiled / in 676ms (769 modules)

И всё. Да они издеваются. Какого хрена?

Тут вы можете подумать, что AsyncLocalStorage работает не так. И вполне можете оказаться правы, но я напомню, что headers() и cookies() используют AsyncLocalStorage. Это то преимущество разработчиков Next.js, которого у нас нет.

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

  • Заголовки

  • NextResponse.redirect / NextResponse.rewrite для перенаправления ответа с дополнительными параметрами (например, /[requestId]/page.tsx)

Как вы могли заметить, радужным ни один из них в нашем случае не выглядит. К вам просто относятся как к детям. Разработчики Next.js имеют чёткое представление о том, как всё должно работать, и вы либо ему подчиняетесь, либо проходите мимо. Обратите внимание: если бы это касалось только промежуточного ПО, то я бы не стал тратить свои выходные на всю эту критику фреймворка React. У меня есть дела поважнее. Но это постоянная боль, с которой при работе с Next.js вы встречаетесь ежедневно.

Vercel может лучше

Бесит же в этом примере то, что Vercel может справиться с подобными задачами намного лучше. Я не хочу излишне хвалить Svelte(Kit), так как их последние решения вызывают у меня опасения, но этот фреймворк намного лучше Next.js. Давайте заглянем в их документацию по промежуточному ПО:

handle — эта функция выполняется при каждом получении запроса сервером SvelteKit [...] Она позволяет изменять заголовки или тело ответа, либо полностью обходить SvelteKit (для программной реализации маршрутов, например).

Пока звучит неплохо. 

locals — чтобы добавить собственные данные в запрос, который передаётся обработчикам в +server.js и серверным функциям load, заполните объект event.locals как показано ниже.

На моих глазах от радости навернулись слёзы. Туда также можно передавать реальные объекты и классы — например, логгер.

Вы можете определить несколько функций обработки и выполнять их последовательно.

Вот так выглядит реальный инжиниринг. SvelteKit — это продукт Vercel. Но как так получается, что флагманский проект уступает побочному по возможностям? Что за чертовщина?

Учёные открыли сверхмассивную чёрную дыру в https://github.com/vercel/next.js/issues

Мне больше нечего особо добавить, но раз уж мы тут все собрались, то будет уместным упомянуть про баг-трекер на GitHub. Это, пожалуй, вершина всей той мусорной кучи недоразумений, которые есть в Next.js. Это то место, куда все надежды и мольбы приходят умирать. Среднее время ответа на баг-репорт здесь — никогда. Я из спортивного интереса решил поискать истории отправки отчётов о багах и их обсуждения касательно тех проблем, с которыми сталкивался сам. В итоге я даже готов принимать ставки на то, сколько лет уйдёт, чтобы получить ответ от команды Next.js.

Думаете, я шучу? Здесь годами лежат сотни запросов с кучей эмодзи ? в ожидании официального ответа. И когда этот ответ, наконец, приходит, в нём говорится, что вы действуете неправильно, и решение для ваших реальных проблем уже в разработке. После этого упомянутое «решение» ещё несколько лет томится в канареечной версии.   

Я сам лично отправлял два баг-репорта год назад. Имейте в виду, что для создания валидного баг-репорта вам нужно воспроизвести проблему.

И что же ты получаешь за время, потраченное на минимальное воспроизведение бага? Всё верно. Полное молчание.

Я бы сообщил ещё о десятке проблем, которые встречал, но после такого уже не стал.

Честно говоря, даже не знаю, существуют ли ещё те баги.

Какие здесь можно сделать выводы?

Не знаю. Лично я больше не хочу использовать Next.js. Вы можете решить, что это всего-навсего одна проблема, которую я преувеличил. Но в этом фреймворке на каждом углу можно встретить баги и пограничные случаи. Как они вообще умудрились сделать так, что TypeScript компилируется медленнее Rust? Зачем проводить различие между кодом, выполняющемся на клиенте и на сервере, не предоставляя никаких инструментов для использования этого факта? Зачем? Почему? И так далее. Не думаю, что у меня хватит ресурса, чтобы вытащить всех нас из этого болота Next.js. Но я обязательно озвучу своё мнение, если в итоге мы напишем другое приложение. Посмотрим, вдруг трава в нём окажется зеленее.

Комментарии (51)


  1. Dhwtj
    07.09.2025 09:58

    Пишите веб бэк на Go, Rust, C#, Java

    Но не на JS, PHP (если только самый простой)


    1. Vadiok
      07.09.2025 09:58

      Почему вы считаете, что на Go и Rust лучше писать сложный бэк?


      1. Dhwtj
        07.09.2025 09:58

        Go = PHP (или node.js/next.js) + компиляция (скорость и статическая типизация) + отличный сетевой рантайм

        Rust = Go + более выразительные типы - рантайм - GC (но часто это и плюс)

        C# / Java где-то посередине: богатые библиотеки, GC есть, но предсказуемый, типы богаче Go, но без Rust'овской борьбы с borrow checker

        Rust, конечно, молодой и не про веб больше. Но я накатал приложение на 5000 строк, мне понравилось


        1. Vadiok
          07.09.2025 09:58

          Те преимущества, что вы описали - это про скорость. Микросервисы на Go - милое дело, но вот сложные штуки, где важна скорость разработки, но не в ущерб архитектуре, и есть множество готовых решений для ее ускорения, Rust и Go по-моему в пролете. А PHP с Node.js - вполне себе оправданные решения.

          Про Java и C# не сильно в курсе, но мой вопрос и не про них.


          1. Dhwtj
            07.09.2025 09:58

            Видел очень плохие приложения на PHP причем именно из-за языковых особенностей. Травмирующий опыт и всё такое.

            Представьте себе единый бизнес процесс (визард), описанный в нескольких файлах на 4-5000 строк без типов, только словари и понять логику что это за данные и каким правилам они отвечают невозможно не прочитав весь код.

            А теперь представьте ещё всё приложение целиком на и150-200.000 строк, где система согласования на 20 состояний, 50 переходов и десяток ролей равномерно размазана по жирным контроллерам-рендерам (ну, как PHP это любит). Да, можно было написать лучше. Но динамическая типизация провоцирует такое

            Жирные контроллеры-рендеры-запросы к БД в одной функции видел только в пхп

            В других языках такое физически неудобно писать.

            PHP :Барьер входа в говнокод нулевой

            <?php
            if($_POST) {
                mysqli_query($db, "UPDATE orders SET status='ok' WHERE id=".$_POST['id']);
                echo "<script>alert('OK')</script>";
            }
            ?>
            <h1>Orders</h1>
            <?php foreach(mysqli_query($db, "SELECT * FROM orders") as $r): ?>
                <form method=post>
                    Order #<?=$r['id']?> $<?=$r['sum']?>
                    <input type=hidden name=id value=<?=$r['id']?>>
                    <button onclick="return confirm('Sure?')">Approve</button>
                </form>
            <?php endforeach ?>

            HTML, JS, SQL, логика — всё тут. SQL injection в комплекте

            В Rust я бы на автомате написал так

            // main.rs
            #[tokio::main]
            async fn main() {
                let db = PgPool::connect(&env::var("DATABASE_URL")?).await?;
                let svc = OrderService::new(db);
            
                let app = Router::new()
                    .route("/", get(orders))
                    .route("/approve", post(approve))
                    .with_state(svc);
            
                axum::Server::bind(&"0.0.0.0:3000".parse()?)
                    .serve(app.into_make_service())
                    .await?;
            }
            // controller.rs
            use axum::{Router, routing::{get, post}, extract::{State, Form}};
            use askama::Template;
            
            #[derive(Template)]
            #[template(path = "orders.html")]
            struct OrdersTemplate { orders: Vec<Order> }
            
            async fn orders(State(svc): State<OrderService>) -> OrdersTemplate {
                OrdersTemplate { orders: svc.get_pending().await.unwrap() }
            }
            
            async fn approve(State(svc): State<OrderService>, Form(params): Form<HashMap<String, String>>) 
                -> axum::response::Redirect {
                if let Some(id) = params.get("id").and_then(|s| s.parse().ok()) {
                    svc.approve(id).await.unwrap();
                }
                axum::response::Redirect::to("/")
            }
            
            // service.rs
            impl OrderService {
                async fn get_pending(&self) -> Result<Vec<Order>> {
                    sqlx::query_as!(Order, "SELECT * FROM orders WHERE status='pending'")
                        .fetch_all(&self.db).await
                }
            
                async fn approve(&self, id: i32) -> Result<()> {
                    sqlx::query!("UPDATE orders SET status='ok' WHERE id=?", id)
                        .execute(&self.db).await?;
                    Ok(())
                }
            }
            // HTML 
            <!-- templates/orders.html -->
            <h1>Orders</h1>
            {% for order in orders %}
            <form method="post" action="/approve" onsubmit="return confirm('Sure?')">
                Order #{{ order.id }} ${{ order.sum }}
                <input type="hidden" name="id" value="{{ order.id }}">
                <button>Approve</button>
            </form>
            {% endfor %}


            1. Dhwtj
              07.09.2025 09:58

              Да, кода больше. Но читать просто, достаточно прочитать сигнатуры. В Rust они не врут в отличие от PHP, JS, python


              1. Rive
                07.09.2025 09:58

                Это вы лукавите, сравнивая лапшичный код на PHP уровня сайта школы 2001 года и упорядоченный код на Rust, который использует фреймворк Axum и шаблонизатор Askama (им обоим не больше 5 лет).

                На Rust так же можно написать грязный код уровня

                Скрытый текст
                use axum::{
                    extract::Extension,
                    response::Html,
                    routing::get,
                    Form, Router,
                };
                use serde::Deserialize;
                use sqlx::{MySql, Pool, query};
                use std::net::SocketAddr;
                use tokio;
                use tokio::net::TcpListener;
                
                #[derive(Deserialize)]
                struct ApproveForm {
                    id: i32,
                }
                
                #[tokio::main]
                async fn main() -> Result<(), Box<dyn std::error::Error>> {
                    let pool = sqlx::mysql::MySqlPoolOptions::new()
                        .connect("mysql://lamer:weak_password@localhost/my_poor_db")
                        .await?;
                
                    let app = Router::new()
                        .route(
                            "/",
                            get({
                                let pool = pool.clone();
                                move || {
                                    let pool = pool.clone();
                                    async move {
                                        let orders = query!("SELECT id, sum, status FROM orders")
                                            .fetch_all(&pool)
                                            .await
                                            .unwrap_or_default();
                
                                        let html = format!(
                                            r#"
                                            <h1>Orders</h1>
                                            {}
                                            "#,
                                            orders
                                                .into_iter()
                                                .map(|r| {
                                                    format!(
                                                        r#"
                                                        <form method="post">
                                                            Order #{} ${}
                                                            <input type="hidden" name="id" value="{}">
                                                            <button type="submit" onclick="return confirm('Sure?')">Approve</button>
                                                        </form>
                                                        "#,
                                                        r.id, r.sum, r.id
                                                    )
                                                })
                                                .collect::<Vec<_>>()
                                                .join("\n")
                                        );
                
                                        Html(html)
                                    }
                                }
                            })
                            .post({
                                let pool = pool.clone();
                                move |Form(form): Form<ApproveForm>| {
                                    let pool = pool.clone();
                                    async move {
                                        // sql-инъекция бережно сохранена
                                        let query_str = format!("UPDATE orders SET status='ok' WHERE id={}", form.id);
                                        let _ = sqlx::query(&query_str).execute(&pool).await;
                
                                        Html(r#"<script>alert('OK')</script>"#.to_string())
                                    }
                                }
                            }),
                        )
                        .layer(Extension(pool));
                
                    // Запуск сервера
                    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
                    let listener = TcpListener::bind(&addr).await.unwrap();
                    println!("Сервер запущен на http://{}", addr);
                
                    axum::serve(listener, app).await.unwrap();
                
                    Ok(())
                }

                Но в 2025 году код из вашего примера для PHP и моего примера для Rust будут одинаково ругать коллеги.


                1. Dhwtj
                  07.09.2025 09:58

                  Лукавство, конечно. Вы меня поймали.

                  Тем не менее, PHP провоцирует на говнокод. На Rust я бы просто не додумался до такого.


                  1. tempick
                    07.09.2025 09:58

                    Тем не менее, PHP провоцирует на говнокод

                    Тейки из 2010 до сих пор живы хех. Если что, в PHP есть такая вещь как фреймворки (laravel, symfony и иногда yii2 на легаси), которые являются просто базой. Вы выше просто навалили какой-то отрывок кода. Да, так можно написать, теоретически. Но так никто и не делает. Я бы мог в пример навалить такой же гавнокод на джаве или плюсах из своих первых задач в шараге. Но это не имеет смысла


                1. NN1
                  07.09.2025 09:58

                  С вашего позволения беру ваш прекрасный код, чтобы пугать коллег ;)


            1. Vedomir
              07.09.2025 09:58

              >Видел очень плохие приложения на PHP причем именно из-за языковых особенностей.

              Откровенный обман, так как языковые способности PHP никак не заставляют писать говнокод.

              Говнокод можно написать на любом языке.


        1. YegorP
          07.09.2025 09:58

          типы богаче

          Это как?


          1. Dhwtj
            07.09.2025 09:58

            Go

            // Слишком просто - теряем ошибки
            func ProcessOrder(orderID string) {
                payment, _ := chargeCard(orderID)
                inventory, _ := reserveStock(orderID)
                delivery, _ := scheduleDelivery(orderID)
                // что если payment прошёл, а inventory упал?
            }
            
            // Честный Go - verbose
            func ProcessOrder(ctx context.Context, orderID string) error {
                tx, err := db.BeginTx(ctx, nil)
                if err != nil {
                    return fmt.Errorf("begin tx: %w", err)
                }
                defer tx.Rollback()
            
                paymentID, err := chargeCard(ctx, orderID)
                if err != nil {
                    return fmt.Errorf("charge card: %w", err)
                }
            
                if err := reserveStock(ctx, tx, orderID); err != nil {
                    // откатить платёж - но это внешний API!
                    go refundPayment(context.Background(), paymentID)
                    return fmt.Errorf("reserve stock: %w", err)
                }
            
                if err := scheduleDelivery(ctx, orderID); err != nil {
                    // и тут откатывать всё...
                    return fmt.Errorf("schedule: %w", err)
                }
            
                return tx.Commit()
            }

            Rust это красиво!

            // Rust - типы кодируют состояния
            struct Order<S> {
                id: OrderId,
                state: S,
            }
            
            struct Unpaid { amount: Money }
            struct Paid { payment_id: PaymentId }
            struct Stocked { reservation_id: ReservationId }
            struct Scheduled { delivery_id: DeliveryId }
            
            impl Order<Unpaid> {
                fn pay(self) -> Result<Order<Paid>, PaymentError> {
                    let payment_id = charge_card(&self.id, self.state.amount)?;
                    Ok(Order { id: self.id, state: Paid { payment_id } })
                }
            }
            
            impl Order<Paid> {
                fn reserve_stock(self, tx: &mut Transaction) -> Result<Order<Stocked>, StockError> {
                    let reservation_id = inventory::reserve(tx, &self.id)?;
                    Ok(Order { id: self.id, state: Stocked { reservation_id } })
                }
            }
            
            // Использование - компилятор не даст пропустить шаг
            let order = Order::new(id, amount)
                .pay()?           // -> Order<Paid>
                .reserve_stock(&mut tx)?  // -> Order<Stocked>  
                .schedule()?;     // -> Order<Scheduled>

            JS не сильно отличается от Go

            // Node.js - async/await, динамическая типизация
            async function processOrder(orderId) {
                let order = { id: orderId, status: 'CREATED' }; // Состояние - просто строка
            
                try {
                    const paymentResult = await chargeCard(order.id);
                    order.status = 'PAID';
                    order.paymentId = paymentResult.paymentId;
                    console.log(`Order ${order.id} paid. Payment ID: ${order.paymentId}`);
            
                    const inventoryResult = await reserveStock(order.id);
                    order.status = 'STOCK_RESERVED';
                    order.reservationId = inventoryResult.reservationId;
                    console.log(`Order ${order.id} stock reserved. Reservation ID: ${order.reservationId}`);
            
                    const deliveryResult = await scheduleDelivery(order.id);
                    order.status = 'DELIVERED'; // Или 'SCHEDULED'
                    order.deliveryId = deliveryResult.deliveryId;
                    console.log(`Order ${order.id} delivery scheduled. Delivery ID: ${order.deliveryId}`);
            
                    // Сохранение финального состояния в БД
                    await saveOrder(order);
            
                    return order;
                } catch (error) {
                    console.error(`Error processing order ${order.id}:`, error.message);
                    order.status = 'FAILED'; // Обновляем статус на FAILED
                    await saveOrder(order); // Сохраняем состояние ошибки
                    // Здесь потребуется логика компенсации/отката, как и в Go
                    if (order.paymentId) {
                        console.log(`Attempting to refund payment ${order.paymentId}`);
                        // await refundPayment(order.paymentId); // Откат платежа
                    }
                    throw error; // Перебрасываем ошибку дальше
                }
            }


        1. DarthVictor
          07.09.2025 09:58

          На JS никто уже не пишет ни бэк, ни фронт, а система типов TS побогаче и Go, и Java, и пожалуй даже C#. Хоть и не без проблем, вроде отсутствия нормальной ковариантности и контрвариантности. Правда к TS есть вопросы к серверному рантайму (вроде реализации многопоточности) и экосистеме бэкенда.


    1. Wowfirst
      07.09.2025 09:58

      Почему вы считаете, что на JS не стоит писать бек?


      1. David_Osipov
        07.09.2025 09:58

        Потому что выстрелить себе в колено, открыв кучи уязвимостей, на JS в разы легче, чем на Rust (который за такое сильно бьёт по рукам).


        1. ionicman
          07.09.2025 09:58

          Вот не надо пинять на язык, ладно)

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

          Все проблемы у людей от головыи рук, не надо инструменты винить.

          При безграмотной арзитектуре, погоне за хайпом и борще в голове вас нечего не спасёт. Выбирать надо под задачу, под вектор развития и под приемственность (сходите ваканчии под go/rust помониторьте).


          1. Epsiloncool
            07.09.2025 09:58

            Тоже раньше считал js дырявым, пока не открыл для себя typescript. Эта штука с линтером точно не даст вам накостылить ошибок и заодно сильно поможет при разработке (подсказками и предупреждениями).


          1. David_Osipov
            07.09.2025 09:58

            Имхо, в среднем, все мы люди и все мы ошибаемся, поэтому инструменты должны защищать от ошибок, как это сделано в Rust или, как ниже заметили, в TS. Всё же JS - это язык с громадным легаси, от которого невозможно отказаться, иначе всё рухнет, но и которое иногда очень неясно работает и, чтобы написать безопасный и быстрый код, нужно все эти особенности знать, а на это убить надо от 5 лет безостановочного кодинга, т.е. тупо стать сеньором.

            Согласен, что сеньор сможет написать относительно безопасный и быстрый код (ну тут скорее просто быстро написать код, чем быстрый код), на фронте и на бэке, но то же самое сможет сделать на расте миддл.


            1. Dhwtj
              07.09.2025 09:58

              тупо стать сеньором

              Путём надра.. гриндинга


        1. DmitryOlkhovoi
          07.09.2025 09:58

          Rust (который за такое сильно бьёт по рукам)

          Я думал в раст комьюнити больше принято класть руку на коленку товарища


    1. gluck59
      07.09.2025 09:58

      Как вам удалось в одной фразе смешать чисто серверный PHP и выполняющийся в браузере (по крайней мере придуманный для браузера) JS?


      1. Dhwtj
        07.09.2025 09:58

        Догадайся


      1. DmitryOlkhovoi
        07.09.2025 09:58

        Какая разница для чего он придуман, по сути оба интерпретируемые, оба через апи работают I/O. Ну а так очевидно шутливый либо тупой наброс)


    1. khmm12
      07.09.2025 09:58

      Иногда неплохо бы загуглить незнакомый фреймворк / библиотеку, чтобы не раздавать советы писать SPA с SSR на языках, на которых слегка проблематично реализовать SPA, да ещё с SSR (кроме C# с Blazor, но удачи набрать специалистов).


      1. DmitryOlkhovoi
        07.09.2025 09:58

        А вы воспользуйтесь своим советом. Вдруг окажется SSR был изначально без этих инструментов))) Понимате мы "натягивали" верстку на пхп, которая где рендерилась?)


        1. khmm12
          07.09.2025 09:58

          В моих словах где-то есть отрицание того факта, что SSR возможен без Next.js? Если есть, приведите пожалуйста. Я вполне успешно реализовывал SSR с React до того как появился Next.js ещё в далеком 2016 году.

          Пост о проблемах конкретного фрейморка Next.js, автор комментария выше предлагает писать backend на Go, Rust, C#, Java, из чего можно предположить, что он не понимает, что такое Next.js и для чего он нужен.

          Я даже подчеркну, что особой любви не питал и не питаю к Next.js. Весь фреймворк сделан так, что шаг влево, шаг вправо, и он начинает вставлять в палки в колеса. Так было в 2018 году, когда взять react + react-router + loadable-components + webpack было безопаснее с точки жизни продукта. Так и осталось в настоящее время, если не стало хуже. Но я не буду отрицать, что Next.js имеет преимущества, особенно в скорости разработки на начальных этапах, когда продукт должен был выйти ещё вчера.


        1. khmm12
          07.09.2025 09:58

          Понимате мы "натягивали" верстку на пхп, которая где рендерилась?)

          Как и в ASP.NET, Django, Ruby on Rails, и даже, прости Господи, Nitrogen. Только какое это имеет отношение к SPA?


          1. DmitryOlkhovoi
            07.09.2025 09:58

            Партишины так же по апи возвращались, как например это делает сейчас htmx, там даже есть атрибут на всю сраницу для aync навигации. Вполне себе SPA. А еще помните был hashtag для роутинга до history api

            Так же у edge темплейтов сейчас есть асинхронный функционал, вполне себе можно сходить за частями в момент регдера темплейта и сделать себе микрофронты даже. И все на базе технологий 20 летней давности)

            Собственно, что даёт nextjs чего не может дать другой стек? Изоморфность? А она действительно нужна?


            1. khmm12
              07.09.2025 09:58

              Было дело, в RoR даже Stimulus на замену Turbolinks завезли. Но бизнес проголосовал деньгами: UX стал сложнее, микросервисы, backend'еры в своей массе заниматься frontend не хотят, а всё чаще и чаще не умеют, и уж тем более сложным UX, frontend'еры backend трогать не хотят и не умеют (дайте мне React/Vue/Angular/Svelte/Solidjs). Поэтому я бы всё таки констатировал смерть классического подхода, где backend рендерит свои шаблоны, а рядом прикрученный JavaScript добавляет UX логику. "Миром" сейчас правят react, vue, angular. И уже даже новостные сайты это приложения на React / Vue.

              Я не буду вступать в полемику хорошо это и плохо. Боюсь, скоро в Web ещё и Flutter войдёт (уже есть попытки), где для выделения текста вставляются элементы поверх Canvas. Просто констатирую, что SPA на сегодня это React / Vue / Angular / e.t.c.


              1. DmitryOlkhovoi
                07.09.2025 09:58

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


  1. DarthVictor
    07.09.2025 09:58

    Next.js уже во времена нового роутера и папочки php app для серверных компонентов шагнул не туда.


  1. gun_dose
    07.09.2025 09:58

    Главный недостаток next.js - он дико тормознутый. Когда они добавили SSG, это означало "ребята, мы не знаем, как ускорить это г-но, поэтому вот вам генератор статики". Проблема изначально была в том, что никому не нужен реактивный фронтенд на бэке. На бэке на странице нет и не может быть никаких событий, поэтому абсолютное большинство фреймворков, независимо от языка, используют строковые шаблонизаторы.


  1. devmargooo
    07.09.2025 09:58

    1. Next.js не дает вам возможность чейнить мидлвары, потому что это все-таки технология для разработки интерфейсов, а не логики веб-приложения. Не надо писать огромную логику в миддлварах на next.js, прикрутите себе бек и там чейните мидлвары сколько хотите :)

    2. Лог "Logging from the page!" не отобразился в браузерной консоли потому, что Next.js - это технология серверного рендеринга. Рендеринг был выполнен на сервере, там и нужно искать лог :) (в данном случае лог будет в билдтайме). В целом по рассказу такое ощущение, что автор ищет логи не там, где их возможно увидеть :)

    3. Клиентские компоненты называются таковыми, потому что там доступно клиентское апи (хуки реакта, обработчики событий)


    1. Jhayphal
      07.09.2025 09:58

      Интересно, чем автор оригинала заслужил такое уважение, чтобы переводить его труды.

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

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


  1. artalex
    07.09.2025 09:58

    "промежуточное ПО"? серьезно?


    1. eme
      07.09.2025 09:58

      Трудности перевода)



  1. Spaceguest
    07.09.2025 09:58

    А я на этом дерьме написал проект на 100К строк. Миддлвару использовал только для CSP. Она там вообще чисто чтоб было, а проку ноль. Rest маршруты тупо декорировал функциями с логированием, авторизацией и прочими делами. А вот с тем, как код пересекает границу сервер-клиент я дофига проблем словил. Так что второй раз пожалуй ни для чего серьезного я nextjs не возьму


  1. DmitryOlkhovoi
    07.09.2025 09:58

    У нас тоже дикие траблы с Next.js. Корни всех проблем, в том, что React, по сути заутсорсил развития фреймворка и новых фичей таким вот инструментам. Они за него имплементят SSR и прочее. И все оно полусырое. Не говоря уже в целом о том, что React'у пора на покой