NestJS as BFF for angular 19 using SSR with hybrid rehydration

Discussão em 'Angular' iniciado por Øystein Amundsen, Dezembro 29, 2024.

  1. Angular 19 comes along with big improvements to how SSR works. In particular, the server part is now actually used when booting up the vite dev server. Which means that I can use express as my reverse proxy so I do not have to define my proxies as a json config for development and once again in code for production. Great!

    This got me thinking; I know about separation of concerns and frontends should only do frontend and backends should only do backend, right? But using hybrid rehydration, my frontend is already served from a backend. Why should I have a backend for frontend and another separate backend for backend? Well, I don't think I need to. The changes made to @angular/ssr package allows me to have a full blown expressjs server as my BFF dev and prod server with all the percs that follows. So not only serving my frontend with hybrid rehydration and providing reverse proxies, I can also define my actual backend api here. All from just a simple ng serve.

    But it is kind of tiresome to define backend endpoints and their logic in express. I would want to leverage a backend framework like NestJS instead. This is where I come to a halt, because I just cannot get my NestJS controllers to respond in this setup.

    This works:

    function widgetRoutes(server: express.Express) {
    const widgetData = [
    { id: 1, name: 'Weather', componentName: 'weather' },
    { id: 2, name: 'Taxes', componentName: 'widget2' },
    { id: 3, name: 'Something else', componentName: 'widget3' },
    ];

    server.get('/api/widgets', (req, res) => {
    res.json(widgetData);
    });

    /**
    * Fetch a single widget by ID.
    */
    server.get('/api/widgets/:id', (req, res) => {
    if (req.params.id) {
    const widget = widgetData.find((w) => w.id === +req.params.id);
    if (!widget) {
    res.status(404).json({ error: 'Widget not found' });
    } else {
    res.json([widget]);
    }
    }
    console.log('[APP]', req.method, req.url, res.statusCode);
    });
    }

    export function bootstrap(): express.Express {
    const server = express();
    const serverDistFolder = dirname(fileURLToPath(import.meta.url));
    const browserDistFolder = resolve(serverDistFolder, '../browser');

    // Here, we now use the `AngularNodeAppEngine` instead of the `CommonEngine`
    const angularNodeAppEngine = new AngularNodeAppEngine();

    // Setup api routes
    widgetRoutes(server);

    // Setup reverse proxy routes
    Object.entries(proxyRoutes).forEach(([path, config]) =>
    server.get(path, createProxyMiddleware(config)),
    );

    // Serve static files from the browser distribution folder
    server.get(
    '**',
    express.static(browserDistFolder, {
    maxAge: '1y',
    index: 'index.html',
    }),
    );

    server.get('**', (req, res, next) => {
    // Yes, this is executed in devMode via the Vite DevServer
    console.log('[APP]', req.method, req.url, res.statusCode);

    angularNodeAppEngine
    .handle(req, { server: 'express' })
    .then((response) =>
    response ? writeResponseToNodeResponse(response, res) : next(),
    )
    .catch(next);
    });

    return server;
    }

    const server = bootstrap();
    if (isMainModule(import.meta.url)) {
    const port = process.env['PORT'] || 4000;
    server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:\${port}`);
    });
    }

    console.warn('Node Express server started');

    // This exposes the RequestHandler
    export const reqHandler = createNodeRequestHandler(server);


    This doesn't:

    @Controller('api/widgets')
    export class WidgetController {
    widgetData = [
    { id: 1, name: 'Weather', componentName: 'weather' },
    { id: 2, name: 'Taxes', componentName: 'widget2' },
    { id: 3, name: 'Something else', componentName: 'widget3' },
    ];

    @Get()
    findAll() {
    return this.widgetData;
    }

    @Get(':id')
    findOne(@Param('id') id: string) {
    return this.widgetData.find((w) => w.id === +id);
    }
    }

    @Module({
    // Setup api routes
    controllers: [WidgetController],
    })
    export class AppModule {}

    export async function bootstrap() {
    const app = await NestFactory.create<NestExpressApplication>(AppModule);
    // Get the express instance from the NestJS app
    const server = app.getHttpAdapter().getInstance();
    const serverDistFolder = dirname(fileURLToPath(import.meta.url));
    const browserDistFolder = resolve(serverDistFolder, '../browser');

    // Here, we now use the `AngularNodeAppEngine` instead of the `CommonEngine`
    const angularNodeAppEngine = new AngularNodeAppEngine();

    // Setup reverse proxy routes
    Object.entries(proxyRoutes).forEach(([path, config]) =>
    server.get(path, createProxyMiddleware(config)),
    );

    // Serve static files from the browser distribution folder
    server.get(
    '**',
    express.static(browserDistFolder, {
    maxAge: '1y',
    index: 'index.html',
    }),
    );

    server.get('**', (req, res, next) => {
    // Yes, this is executed in devMode via the Vite DevServer
    console.log('[APP]', req.method, req.url, res.statusCode);

    angularNodeAppEngine
    .handle(req, { server: 'express' })
    .then((response) =>
    response ? writeResponseToNodeResponse(response, res) : next(),
    )
    .catch(next);
    });

    return app;
    }

    const server = await bootstrap();
    if (isMainModule(import.meta.url)) {
    const port = process.env['PORT'] || 4000;
    server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:\${port}`);
    });
    }

    // This exposes the RequestHandler
    export const reqHandler = createNodeRequestHandler(
    server.getHttpAdapter().getInstance(),
    );



    What am I doing wrong? The setup looks to be the same, but the NestJS controller does not respond. Or, if my thoughts stink, why should I not venture down this road?

    Continue reading...

Compartilhe esta Página