nestjs / nest

A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀
https://nestjs.com
MIT License
67.7k stars 7.63k forks source link

FastifyAdapter can't take a FastifyInstance due to a type error #12979

Closed toptal-dave closed 10 months ago

toptal-dave commented 10 months ago

Is there an existing issue for this?

Current behavior

I am following some example code (that needed adapting) to deploy a Nest.js application using Fastify on AWS Lambda. The code is found in this issue and this PR.

When I adapt the code to be error free, I get a type error at the point where I create the FastifyAdapter using a FastifyInstance:

Argument of type 'FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTypeProviderDefault>' is not assignable to parameter of type 'FastifyHttp2Options<any, FastifyBaseLogger> | FastifyHttp2SecureOptions<any, FastifyBaseLogger> | FastifyHttpsOptions<...> | FastifyInstance<...> | FastifyServerOptions<...>'.
  Type 'FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTypeProviderDefault>' is not assignable to type 'FastifyInstance<any, any, any, FastifyBaseLogger, FastifyTypeProviderDefault>'.
    The types of 'withTypeProvider().decorate' are incompatible between these types.
      Type 'DecorationMethod<FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, Provider>, FastifyInstance<...>>' is not assignable to type 'DecorationMethod<FastifyInstance<any, any, any, FastifyBaseLogger, Provider>, FastifyInstance<any, any, any, FastifyBaseLogger, Provider>>'.
        Target signature provides too few arguments. Expected 2 or more, but got 1.

Fastify type error

Minimum reproduction code

https://github.com/toptal-dave/fastify-adapter-type-error

Steps to reproduce

All I did was replace the original code with the following code in the main.ts:

import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
import awsLambdaFastify, { PromiseHandler } from '@fastify/aws-lambda';
import fastify, { FastifyInstance, FastifyServerOptions } from 'fastify';
import { Context, APIGatewayProxyEvent } from 'aws-lambda';
import { Logger } from '@nestjs/common';

interface NestApp {
  app: NestFastifyApplication;
  instance: FastifyInstance;
}

let cachedNestApp;

async function bootstrap(): Promise<NestApp> {
  const serverOptions: FastifyServerOptions = {
    logger: (process.env.LOGGER || '0') == '1',
  };
  const instance: FastifyInstance = fastify(serverOptions);
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(instance),
    {
      logger: !process.env.AWS_EXECUTION_ENV ? new Logger() : console,
    },
  );

  const CORS_OPTIONS = {
    origin: '*',
    allowedHeaders: '*',
    exposedHeaders: '*',
    credentials: false,
    methods: ['GET', 'PUT', 'OPTIONS', 'POST', 'DELETE'],
  };

  app.register(require('fastify-cors'), CORS_OPTIONS);

  app.setGlobalPrefix(process.env.API_PREFIX);

  await app.init();

  return {
    app,
    instance,
  };
}

export const handler = async (
  event: APIGatewayProxyEvent,
  context: Context,
): Promise<PromiseHandler> => {
  if (!cachedNestApp) {
    const nestApp: NestApp = await bootstrap();
    cachedNestApp = awsLambdaFastify(nestApp.instance, {
      decorateRequest: true,
    });
  }

  return cachedNestApp(event, context);
};

Expected behavior

It should be possible to pass a FastifyInstance into FastifyAdapter without causing a type error because it looks like FastifyAdapter takes either options or an instance by the typing:

FastifyAdapter argument types

Package

Other package

@fastify/aws-lambda

NestJS version

10.3.0

Packages versions

{
  "name": "@iluvcoffee/application",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:watch:cov": "jest --watch --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@fastify/aws-lambda": "^3.5.0",
    "@nestjs/common": "^10.0.0",
    "@nestjs/core": "^10.0.0",
    "@nestjs/mapped-types": "^2.0.4",
    "@nestjs/platform-express": "^10.0.0",
    "@nestjs/platform-fastify": "^10.3.0",
    "@nestjs/typeorm": "^10.0.1",
    "aws-lambda": "^1.0.7",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.14.0",
    "fastify": "^4.25.2",
    "fastify-cors": "^6.1.0",
    "pg": "^8.11.3",
    "reflect-metadata": "^0.1.13",
    "rxjs": "^7.8.1",
    "typeorm": "^0.3.17"
  },
  "devDependencies": {
    "@nestjs/cli": "^10.0.0",
    "@nestjs/schematics": "^10.0.0",
    "@nestjs/testing": "^10.0.0",
    "@types/express": "^4.17.17",
    "@types/jest": "^29.5.2",
    "@types/node": "^20.3.1",
    "@types/supertest": "^2.0.12",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.42.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-prettier": "^5.0.0",
    "jest": "^29.5.0",
    "prettier": "^3.0.0",
    "source-map-support": "^0.5.21",
    "supertest": "^6.3.3",
    "ts-jest": "^29.1.0",
    "ts-loader": "^9.4.3",
    "ts-node": "^10.9.1",
    "tsconfig-paths": "^4.2.0",
    "typescript": "^5.1.3"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}

Also, the output of npx nest info:


 _   _             _      ___  _____  _____  _     _____
| \ | |           | |    |_  |/  ___|/  __ \| |   |_   _|
|  \| |  ___  ___ | |_     | |\ `--. | /  \/| |     | |
| . ` | / _ \/ __|| __|    | | `--. \| |    | |     | |
| |\  ||  __/\__ \| |_ /\__/ //\__/ /| \__/\| |_____| |_
\_| \_/ \___||___/ \__|\____/ \____/  \____/\_____/\___/

[System Information]
OS Version     : Linux 6.6
NodeJS Version : v18.19.0
NPM Version    : 10.2.3 

[Nest CLI]
Nest CLI Version : 10.2.1 

[Nest Platform Information]
platform-express version : 10.3.0
platform-fastify version : 10.3.0
mapped-types version     : 2.0.4
schematics version       : 10.0.3
typeorm version          : 10.0.1
testing version          : 10.3.0
common version           : 10.3.0
core version             : 10.3.0
cli version              : 10.2.1

Node.js version

18.19.0

In which operating systems have you tested?

Other

No response

toptal-dave commented 10 months ago

Do you think a better approach would be to use the getHttpAdapter or getHttpServer or similar from the returned promise to get the Fastify server?

toptal-dave commented 10 months ago

I was able to fix the code myself using the nestApp.getHttpAdapter().getHttpServer() to get the instance:

import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
import awsLambdaFastify, { PromiseHandler } from '@fastify/aws-lambda';
import { Context, APIGatewayProxyEvent } from 'aws-lambda';
import { Logger } from '@nestjs/common';

let cachedNestApp;

async function bootstrap(): Promise<NestFastifyApplication> {
  // Create the app
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    // Use an env var to set the logger to nest or console
    new FastifyAdapter({ logger: (process.env.LOGGER || '0') == '1' }),
    {
      logger: !process.env.AWS_EXECUTION_ENV ? new Logger() : console,
    },
  );

  // Enable cors
  app.enableCors({
    origin: '*',
    allowedHeaders: '*',
    exposedHeaders: '*',
    credentials: false,
    methods: ['GET', 'PUT', 'OPTIONS', 'POST', 'DELETE'],
  });

  // Set the prefix as necessary
  app.setGlobalPrefix(process.env.API_PREFIX);

  await app.init();

  return app;
}

export const handler = async (
  event: APIGatewayProxyEvent,
  context: Context,
): Promise<PromiseHandler> => {
  // If there's no cached app
  if (!cachedNestApp) {
    // Bootstrap
    const nestApp: NestFastifyApplication = await bootstrap();
    // Create an AWS Lambda Fastify cached app from the Nest app
    cachedNestApp = awsLambdaFastify(nestApp.getHttpAdapter().getHttpServer(), {
      decorateRequest: true,
    });
  }

  return cachedNestApp(event, context);
};

I know it's a separate issue but it would be pretty cool to get the serverless documentation updated with a proper example of how to run a Fastify-based Nest.js application in a Lambda. I'm trying this specific solution to reach a balance of speed (which lowers cost in Lambdas) & avoiding running an instance anywhere (as a container or an EC2) to demonstrate and MVP for a project with Nest.js. It's pretty cool to think that you could start a project this way and when traffic increases, with a few small tweaks, you could probably migrate to EKS or Kubernetes.

toptal-dave commented 10 months ago

Actually I don't think that fixes it entirely because when I try to run the code as a containerized Lambda created by SST, I get the following error:

2023-12-31T23:36:48.020Z    5f37cec7-815e-4f51-a59d-7a20e93c69cf    ERROR   Invoke Error    {
    "errorType": "TypeError",
    "errorMessage": "app.decorateRequest is not a function",
    "stack": [
        "TypeError: app.decorateRequest is not a function",
        "    at module.exports (/var/task/node_modules/@fastify/aws-lambda/index.js:23:9)",
        "    at Runtime.handler (/var/task/dist/main.js:28:50)",
        "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"
    ]
}

There's a demo repo here. Perhaps using getHttpAdapter().getHttpServer() isn't the way to get an instance after all...

kamilmysliwiec commented 10 months ago

Your local fastify dependency version must match the one installed by the @nestjs/platform-fastify package

toptal-dave commented 10 months ago

@kamilmysliwiec thanks for the response! I'm going through the fundamentals course now so it feels like I'm replying to a celebrity!

Anyway, I took your advice and locked the version to 4.25.1; however, I'm still getting the decorate response error:

node_modules/@nestjs/platform-fastify/package.json

node_modules/@nestjs/platform-fastify/node_modules/fastify

packages/application/package.json

packages/application/node_modules/fastify/package.json

The error is as follows:

2024-01-05T11:53:49.911Z    3c1d2db4-a768-4e91-ba05-44c73b9982e2    ERROR   Invoke Error    {
    "errorType": "TypeError",
    "errorMessage": "app.decorateRequest is not a function",
    "stack": [
        "TypeError: app.decorateRequest is not a function",
        "    at module.exports (/var/task/node_modules/@fastify/aws-lambda/index.js:23:9)",
        "    at Runtime.handler (/var/task/dist/main.js:28:50)",
        "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"
    ]
}

I think my next step is actually going to be to abandon the serverless Fastify adapter and see if I can get it working without. I was able to recently deploy to AWS via an ECS+EC2+ASG+ALB setup but for demo-ing and early growth, Lambda would be extremely awesome.

toptal-dave commented 10 months ago

Actually, @kamilmysliwiec , when I restored the code to a prior version that declared the instance separately instead of trying to reference it (here), I was able to get it working. Thanks for the help!