0

I am having trouble creating an endpoint to serve the swagger documentation. The only way I can do it locally is if the path has a double slash at the end localhost:3003/dev/swagger//; If I omit one of the forward slashes, it returns a 404 for the path localhost:3003/swagger without /dev. Once deployed, API Gateway will return {"message": "Forbidden"} for either swagger endpoint (with or without //). How can I get the API Gateway /swagger endpoint to return the swagger UI?, I'm not sure if I have missed some steps.

Below are the main.ts for my NestJS application as well as the serverless.yml and here is a sample repo with minimum setup to duplicate my issue. https://github.com/MRdgz/serverless-nestj-swagger

main.ts

// main.ts
import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { configure as serverlessExpress } from '@vendia/serverless-express';
import { Callback, Context, Handler } from 'aws-lambda';
import { AppModule } from './app.module';

let server: Handler;

function setupSwagger(nestApp: INestApplication): void {
  const config = new DocumentBuilder()
    .setTitle('Sample API')
    .setDescription('Sample API Documentation')
    .setVersion('0.0.1')
    .addServer('/dev')
    .build();

  const document = SwaggerModule.createDocument(nestApp, config);

  SwaggerModule.setup('/swagger', nestApp, document, {
    customSiteTitle: 'Sample',
    swaggerOptions: {
      docExpansion: 'none',
      operationSorter: 'alpha',
      tagSorter: 'alpha',
    },
  });
}

async function bootstrap(): Promise<Handler> {
  const app = await NestFactory.create(AppModule);

  setupSwagger(app);
  await app.init();

  const expressApp = app.getHttpAdapter().getInstance();
  return serverlessExpress({ app: expressApp });
}

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) => {
  event.path = `${event.path}/`;
  event.path = event.path.includes('swagger-ui')
    ? `swagger${event.path}`
    : event.path;

  server = server ?? (await bootstrap());

  return server(event, context, callback);
};

severless.yml

service: sample-api

variablesResolutionMode: 20210326
useDotenv: true

plugins:
  - serverless-offline
  - serverless-plugin-optimize

# functions will inherit settings from provider properties if available,
provider:
  name: aws
  runtime: nodejs14.x
  lambdaHashingVersion: 20201221
  # memorySize: 1024 # default 1024 MB
  timeout: 30 # default 6 seconds
  # sls deploy --stage {stage} otherwise defaults to dev
  stage: ${opt:stage, 'dev'}

functions:
  main:
    handler: dist/main.handler
    name: ${opt:stage, 'dev'}-${self:service}
    events:
      - http:
          method: ANY
          path: /{proxy+}
          cors: true

custom:
  serverless-offline:
    httpPort: 3003
  optimize:
    external: ['swagger-ui-dist']

mcr
  • 1
  • 2
  • I'm dealing with the same issue. Where you able to get something working? Right now, I see the swagger-ui attempting to be served from api gateway but a 500 is being returned for some of the files. What did you ultimately decide to do? I can't seem to find anything on how to do this with the most recent version of nestjs framework. All the other solutions are outdated. – joker1979 Jul 15 '22 at 12:07

3 Answers3

1

You can check this GitHub issue, this may give you some ideas, and this is how I end up with:

const document = SwaggerModule.createDocument(app, config);
const forwardedPrefixSwagger = async (
    req: any,
    _: Response,
    next: NextFunction,
) => {
   req.originalUrl = (req.headers['x-forwarded-prefix'] || '') + req.url;
   next();
};
app.use(
   '/api/',
   forwardedPrefixSwagger,
   swaggerUi.serve,
   swaggerUi.setup(document),
);
Ahmed Gad
  • 691
  • 1
  • 7
  • 26
gary he
  • 11
  • 2
0

I think the issue is described here serverless-http/issues/86

What helped in my case (please note: it is still a kind of workaround; I have checked it locally only npm run sls:offline; It is a simplified example)

  • serverless.yaml

My custom section:

   plugins:
     - serverless-plugin-typescript
     - serverless-plugin-optimize
     - serverless-offline

   [...]

    custom:
      serverless-offline:
        noPrependStageInUrl: true    <----- this guy!
      optimize:
        external: ['swagger-ui-dist']

    [...]

    functions:
      main:
        handler: src/lambda.handler
     events:
        - http:
           method: ANY
           path: /
        - http:
           method: ANY
           path: '/{proxy+}'
  • lambda.ts

     import { AppModule } from './app.module';
     import { Callback, Context, Handler } from 'aws-lambda';
     import { NestFactory } from '@nestjs/core';
     import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
     import serverlessExpress from '@vendia/serverless-express';
     import { INestApplication } from '@nestjs/common';
    
     let cachedServer: Handler;
    
     async function bootstrap(): Promise<Handler> {
       const app = await NestFactory.create(AppModule);
       setupSwagger(app);
       await app.init();
    
       const expressApp = app.getHttpAdapter().getInstance();
       return serverlessExpress({ app: expressApp });
     }
    
     export const handler: Handler = async (
       event: any,
       context: Context,
       callback: Callback,
     ) => {
      cachedServer = cachedServer ?? (await bootstrap());
      return cachedServer(event, context, callback);
    };
    
    function setupSwagger(app: INestApplication) {
      const options = new DocumentBuilder()
        .setTitle('My API')
        .setDescription('My application API')
        .setVersion('1.0.0')
        .addTag('#tag')
        .build();
      const document = SwaggerModule.createDocument(app, options);
      SwaggerModule.setup('api/swagger', app, document);
    }
    

The result is that http://localhost:3000/api/swagger returns the api page, http://localhost:3000/api is handled by a default

@Controller('api')
   @Get()
gvlax
  • 740
  • 4
  • 17
0

Try

functions:
  main:
    handler: dist/main.handler
    name: ${opt:stage, 'dev'}-${self:service}
    events:
      - httpApi: "*"
  • Remember that Stack Overflow isn't just intended to solve the immediate problem, but also to help future readers find solutions to similar problems, which requires understanding the underlying code. This is especially important for members of our community who are beginners, and not familiar with the syntax. Given that, **can you [edit] your answer to include an explanation of what you're doing** and why you believe it is the best approach? – Jeremy Caney Jun 05 '23 at 00:19