vue.jsnestjsvue-routerfastify

Nestjs + Vuejs routing


I want to build monorepo with Vue and Nestjs. Main pages gonna be handled with Nestjs + Twig with paths /<page>. Admin panel gonna be at /admin/<page> (Vue app). And api routes, created with Nestjs /api/<route>. Vue admin builds in its dist directory and then we serve it via useStaticAssets method from https://github.com/fastify/fastify-static, here is Nestjs app's main.ts file:

import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
import helmet from '@fastify/helmet';
import { join } from 'path';

async function bootstrap() {
  const PORT = process.env.PORT || 3000;
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter({ logger: true }),
  );

  await app.register(helmet);

  /** client assets and backend (uploaded files etc.) */
  app.useStaticAssets({
    root: join(__dirname, '..', 'public'),
    prefix: '/public/',
  });
  /** vue admin */
  app.useStaticAssets({
    root: join(__dirname, '..', 'vue-admin', 'dist'),
    prefix: '/admin',
    prefixAvoidTrailingSlash: true,
    decorateReply: false,
  });
  app.setViewEngine({
    engine: {
      twig: require('twig'),
    },
    templates: join(__dirname, '..', 'views'),
  });

  await app.listen(PORT);
}

bootstrap();

Expected behaviour: surfing Vue app pages without reloading and when refresh page in browser, get the same page. It works with Symfony, solution is here.

Actual behaviour: the same, but when refresh page at existing Vue route (e.g. /admin/about) it responds with 404. Seems like it tries to find static file, but we want it to be handled with Vue router.

I've tried to perform something like this to solve problem, but it doesn't work as expected:

import { All, Controller, Res } from '@nestjs/common';
import { FastifyReply } from 'fastify';
import { join } from 'path';
import { readFileSync } from 'fs';

@Controller()
export class AppController {
  @All('admin/*')
  vueAdmin(@Res() res: FastifyReply) {
    res
      .type('text/html')
      .send(
        readFileSync(
          join(__dirname, '..', 'vue-admin/dist/index.html'),
          'utf8',
        ),
      );
  }
}


Solution

  • The behavior I wanted to achieve in this question was achieved and it works as expected. Final code for main.ts:

    import { NestFactory } from '@nestjs/core';
    import {
      FastifyAdapter,
      NestFastifyApplication,
    } from '@nestjs/platform-fastify';
    import { AppModule } from './app.module';
    import fastifyHelmet from '@fastify/helmet';
    import fastyfyMultipart from '@fastify/multipart';
    import { join } from 'path';
    import { queryStringParse } from './utils';
    import { readFileSync } from 'node:fs';
    import * as fastifyHttpsRedirect from 'fastify-https-redirect';
    
    async function bootstrap() {
      const HOST = process.env.HOST || '127.0.0.1';
      const PORT = process.env.USE_HTTPS ? 443 : process.env.PORT || 3000;
    
      const fastifyAdapterOptions: any = {
        logger: true,
        http2: true,
        querystringParser: (str) => queryStringParse(str),
      };
    
      if (process.env.USE_HTTPS) {
        const httpsOptions = {
          key: readFileSync(join(__dirname, '..', 'certs', 'ssl.key')),
          cert: readFileSync(join(__dirname, '..', 'certs', 'ssl.crt')),
          allowHTTP1: true,
          requestCert: false, // process.env.NODE_ENV === 'production',
          rejectUnauthorized: false, // process.env.NODE_ENV === 'production',
        };
        fastifyAdapterOptions.https = httpsOptions;
      }
    
      const app = await NestFactory.create<NestFastifyApplication>(
        AppModule,
        new FastifyAdapter(fastifyAdapterOptions),
      );
    
      app.register(fastifyHttpsRedirect);
      app.register(fastyfyMultipart);
    
      if (process.env.NODE_ENV === 'development') {
        app.enableCors({
          credentials: true,
          origin: true,
        });
      }
    
      await app.register(fastifyHelmet, {
        crossOriginResourcePolicy: false,
        contentSecurityPolicy: {
          directives: {
            'img-src': ["'self'", 'https: data: blob:'],
          },
        },
      });
    
      /** client assets and backend (uploaded files etc.) */
      app.useStaticAssets({
        root: join(__dirname, '..', 'public'),
        prefix: '/public/',
      });
      /** vue admin */
      app.useStaticAssets({
        root: join(__dirname, '..', 'vue-admin', 'dist'),
        prefix: '/admin',
        prefixAvoidTrailingSlash: true,
        decorateReply: false,
      });
      app.setViewEngine({
        engine: {
          nunjucks: require('nunjucks'),
        },
        options: {
          onConfigure: (env) => {
            // eslint-disable-next-line @typescript-eslint/no-var-requires
            env.addFilter('date', require('nunjucks-date-filter'));
          },
          autoescape: true,
          web: {
            useCache: process.env.NODE_ENV === 'production',
            async: process.env.NODE_ENV === 'production',
          },
          debug: process.env.NODE_ENV === 'development',
          watch: process.env.NODE_ENV === 'development',
        },
        templates: join(__dirname, '..', 'views'),
      });
    
      await app.listen(PORT, HOST);
    }
    
    bootstrap();
    

    Important thing is to add in builder configuration of vue-admin base url path. I used Vite and in my vite.config.ts I've declared base: '/admin/' https://vitejs.dev/config/shared-options.html#base

    But this technical solution entails some cons. Fastify team explains it here: https://fastify.dev/docs/latest/Guides/Recommendations/#use-a-reverse-proxy