Jeremy Thille
Jeremy Thille

Reputation: 26390

NestJS - Default (wildcard) route?

In my Angular app, if I load the home page / and then navigate to, say, /products, it works fine (it's a lazy-loaded module). But if now I reload the page, the browser makes a GET /products call to the server, which results in a 404.

The solution is to send index.html and the Angular app is back on rails. So in Express I do app.all("*", (req,res) => { res.sendFile("index.html") }) and it works.

How to do the same thing in Nest?

There is a @All decorator, but each controller in a given component handles a subroute, for instance @Controller("cats") will match /cats routes, so if I add @All in this controller, it will match only /cats/*, not *.

Must I really create a whole separate module with a controller, just for this? That's what I did

@Controller() // Matches "/"
export class GenericController {

    @All() // Matches "*" on all methods GET, POST...
    genericFunction(){
        console.log("Generic route reached")
    }
}

And in my main module :

@Module({
    imports: [
        ItemsModule, // Other routes like /items
        GenericModule, // Generic "*" route last
    ],
})

It works, but it seems overkill. Is this the way to go or is there a simpler trick?

Upvotes: 8

Views: 13418

Answers (3)

Codeninja
Codeninja

Reputation: 103

An alternative answer in 2022.

I've solved this by specifying the routes in the order I want them evaluated. In my instance I am using a fallback route to catch all requests, but if I need custom processing I want to create a route which superceeds the fallback route.

However, in defining a catchall route /api/:resource in the AppController, I found the fallback route would overwrite all other routes.

My solution to this is to define the fallback route in it's own module and ensure that it is appended to the list of modules. This way it is created last and will only catch what falls through.

#router.ts

import {RouterModule} from '@nestjs/core/router';
import {ContentBlockModule} from './content_block/content_block.module';
import {FallbackModule} from './fallback/fallback.module';

const APIRoutesWithFallbackRoute = RouterModule.register([
  {
    // This lets me avoid prepending my routes with /api prefixes 
    path: 'api',
    
    // Overload the /api/content_blocks route and foward it to the custom module
    children: [
      {
        path: 'content_blocks',
        module: ContentBlockModule,
      },
    ],
  },
  { //Fallback Route catches any post to /api/:resource
    path: 'api',
    module: FallbackModule,
  },
]);

#app.module App module imports the fallback module. Important Ensure FallbackModule is the last module to be declaired or it will overwrite routes that are included after it.

import {Module} from '@nestjs/common';

import {AppService} from './app.service';
import {APIRoutesWithFallbackRoute} from './APIRoutesWithFallbackRoute';
import {ContentBlockModule} from './content_block/content_block.module';
import {FallbackModule} from './fallback/fallback.module';

// APIRoutes include first, Fallback Routes prepended.
@Module({
  imports: [APIRoutesWithFallbackRoute, ContentBlockModule, FallbackModule],
  controllers: [],
  providers: [AppService],
})
export class AppModule {}

FallbackController

import {Controller, Post, Req, Res} from '@nestjs/common';
import {defaultHandler} from 'ra-data-simple-prisma';

import {FallbackService} from './fallback.service';

@Controller()
export class FallbackController {
  constructor(private readonly prisma: FallbackService) {}

  @Post(':resource')
  fallback(@Req() req, @Res() res) {
    // return this.appService.getData();
    console.log('executing from the default fallback route');

    return defaultHandler(req, res, this.prisma);
  }
}

ContentBlockController The content block controller, included here for completeness.

@Controller()
export class ContentBlockController {
  constructor(
    private readonly contentBlockService: ContentBlockService,
    private readonly prisma: PrismaService,
  ) {}

  @Post()
  async create(
    @Body() contentBlock: content_blocks,
    @Req() req: Request,
    @Res() res: Response,
  ): Promise<void> {
    console.log('executing from the resource specific route');
 
    // lean on my service to do heavy business logic
    const [model, values] = await this.contentBlockService.createContentBlock(
      contentBlock,
    );

    // inject custom logic...
    const alteredRequest: CreateRequest = {
      ...req,
      body: {
        ...req.body,
        params: {
          data: values,
        },
      },
    };
   

    return createHandler(alteredRequest, res, model);
  } 
}

Using this system I am able to define a single route to handle 90% of the routes necessary to expose my Prisma models to my private API. And if I need custom logic I have full control.

Upvotes: 3

Yerkon
Yerkon

Reputation: 4798

So, will be best to use global-scoped exception filter.

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.useGlobalFilters(new NotFoundExceptionFilter());
  await app.listen(3000);
}
bootstrap();

NotFoundExceptionFilter:

import { ExceptionFilter, Catch, NotFoundException } from '@nestjs/common';
import { HttpException } from '@nestjs/common';

@Catch(NotFoundException)
export class NotFoundExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse();
        // here return `index.html`
    }
}

Maybe it will not work, will test later

Upvotes: 6

Kamil Myśliwiec
Kamil Myśliwiec

Reputation: 9178

You don't need to create a separated GenericModule. However, GenericController is fully valid and you approach is definitely a good one. The question is rather what would you like to achieve using this generic route. If handling "Route not found" error is your requirement, a better choice is an exception filter.

Upvotes: 4

Related Questions