Best practise for getting access to the app instance - node.js

I'm creating a NestJS app with Firebase cloud functions. I have to use both onRequest and onCreate (Firestore Events) from Firebase in the NestJS application. It's quite straightforward how to address the onRequest events. However, I'm not sure if I'm doing it right when having to do both at the same time. In order for me to pass the onCreate event changes and context to the service layer, I need to get access to the AppService class. To do that I need access to the app instance that has been created. However, I feel like I'm creating two instances of the app (refer to the code). I would like to know if my current my implementation is best to practice or if there is any way it can be improved. Please note I'm a frontend developer so this work is outside my comfort zone. I would like to know the best way to do this, especially if I have to work with more events such as onUpate, onDelete etc.
import { NestFactory } from '#nestjs/core';
import { ExpressAdapter } from '#nestjs/platform-express';
import { AppModule } from './app.module';
import * as express from 'express';
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import { Express } from 'express-serve-static-core';
import { AppService } from './app.service';
const server = express();
export const createNestServer = async (expressInstance: Express) => {
//FIRST APP INSTANCE
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressInstance),
);
admin.initializeApp();
return app.init();
};
createNestServer(server)
.then((v) => console.log('Nest Ready'))
.catch((err) => console.error('Nest broken', err));
//exporting all onRequest events
export const api = functions.https.onRequest(server);
//exporting the onUserCreate event
exports.onUserCreate = functions.firestore
.document('users/{docId}')
.onWrite(async (change, context) => {
console.log('Changes are tracked');
//SECOND APP INSTANCE
const app = await NestFactory.create(AppModule);
return app.get(AppService).onCreate(change, context);
});

I assume you have something like a UsersController (and others) that handles retrieving, creating, updating the users as part of the NestJS application. Just pass the Express instance (wrapped by the NestJS adapter) to the Cloud Function HTTPS handler (onRequest). NestJS will take care of setting up the routes.
export const api = functions.https.onRequest(server)
#Controller('users')
export class UsersController {
constructor(private readonly usersService: UserService) {}
#Get(':userId')
async getUser(#Param('userId') id: String) {
// retrieve user
}
#Post()
async create(#Body() createUserDto: CreateUserDto) {
// create a new user
}
...
}
#Module({
controllers: [UsersController],
providers: [AppService, UsersService],
exports: [AppService, UsersService]
})
export class AppModule {}
The AppService is a provider registered in the AppModule. You could extract it from the INestApplication instance.
const server = express();
let appService: AppService;
export const createNestServer = async (expressInstance: Express) => {
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressInstance),
);
admin.initializeApp();
appService = app.get(AppService);
return app.init();
};
Then you could use it within the onWrite firestore trigger.
Seems like you are trying to do 2 differents tasks with the same NestJS app. Just let the NestJS (or ExpressJS) app handle the HTTPS requests. And for the onCreate, onWrite ... triggers implement a different solution. Perhaps don't rely on NestJS to handle these types of triggers.
NestJS personally feels like overkill, you could just use ExpressJS. You wouldn't have to register the AppService as a provider and jump through hoops.

The following approach has allowed me to solve the multiple instatiation issue of the app instance. Hopefully this is a good and acceptable way to do this. Feedback is more than welcome.
const server = express();
export const createNestServer = async (expressInstance: Express) => {
//FIRST APP INSTANCE
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressInstance),
);
admin.initializeApp();
return app.init();
};
const main = createNestServer(server);
export const api = functions.https.onRequest(server);
exports.onUserWrite = functions.firestore
.document('users/{docId}')
.onWrite((change, context) =>
main.then((app) => {
return app.get(AppService).onCreate(change, context);
}),
);

Related

Binding interceptor to NestJS microservice method

I just created a simple interceptor to override every error thrown by my application, just like the one on Nest's documentation:
#Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(() => new ApplicationException())),
);
}
}
And altough exceptions caused by http requests indeed are caught in that interceptor, I just can't make it work with RPC requests (like KafjaJS and events).
Just like the documentation, I've binded it on my app.module:
{
provide: APP_INTERCEPTOR,
useClass: ErrorsInterceptor,
}
I know I'm probably missing something out, can someone clarify where and why what I'm doing is not working and how to make it work?
#Edit: I forgot to mention that I made it work #UseInterceptors() above my controller's method, but I'd like to make it work without it.
#Edit 2: I have a hybrid appplication, this is what my main looks like (as asked by Jay):
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger: WinstonModule.createLogger(winstonTransports),
});
app.connectMicroservice<MicroserviceOptions>(kafkaConfig);
const logger = app.get<winston.Logger>(WINSTON_MODULE_NEST_PROVIDER);
app.useLogger(logger);
app.enableCors(corsConfig);
await app.startAllMicroservices();
await app.listen(env.PORT);
}
When working with hybrid applications you need to add { inheritAppConfig: true } to the connectMicroservice() method as a second parameter as described in the docs. This means your main.ts should be
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger: WinstonModule.createLogger(winstonTransports),
});
app.connectMicroservice<MicroserviceOptions>(kafkaConfig, { inheritAppConfig: true });
const logger = app.get<winston.Logger>(WINSTON_MODULE_NEST_PROVIDER);
app.useLogger(logger);
app.enableCors(corsConfig);
await app.startAllMicroservices();
await app.listen(env.PORT);
}

How can I use fastify request logger in other classes without having to pass it as a parameter?

I'm new in nodejs, I'm using fastify and I want to be able to use the req.logger in all the classes functions of the flow, this because I have a the request-id on req.logger, the first solution that came to my mind is to pass as a parameter the logger through all the function/classes but I think that would make the code kind of dirty, here is an example of my code:
app.ts
import pino from 'pino';
import fastify from 'fastify';
declare module 'fastify' {
interface FastifyInstance {
// augment fastify instance with the config object types
config: Config;
}
}
function build() {
const app = fastify({
logger: pino({
name: process.env.NAME,
level: process.env.LOG_LEVEL,
}),
disableRequestLogging: true,
requestIdHeader: 'correlation-id',
requestIdLogLabel: 'correlationId',
});
// register plugins
app.register(apiRoutes, fastify => ({
getObjectUseCase: new GetObjectUseCase(
new TestClass()),
}));
return app;
}
export { build };
routes.ts
import { FastifyPluginCallback } from 'fastify';
import { StatusCodes } from 'http-status-codes';
export const apiRoutes: FastifyPluginCallback<RoutesOpts> = async (fastify, options, done) => {
const getObjectUseCase = options.getObjectUseCase;
fastify.get<object>('/v1/api/:id', async (req, reply) => {
const id = req.params.payoutId;
req.logger.info('This is a logger print'); // has the correlation id inside it while printing
const storedObject = await getObjectCase.execute(id);
reply.code(StatusCodes.OK).send(storedObject);
});
}
GetObjectUseCase.ts
export class GetObjectUseCase {
private anotherClass: TestClass;
constructor(anotherClass: TestClass) {
this. anotherClass = anotherClass;
}
async execute(id: string): Promise<StoredObject> {
// I want to use the logger here with have the correlation id on it without having to pass it as an argument on the method, how is it posible?
return this.anotherClass.getById(id);
// also needed to use it inside anotherClass.getById so I will need to pass the logger also in the method
}
}
Hope I have been clear.
Thanks!
This may not be the best or only way to do it, but this has worked for me in the past.
Typically I structure my projects with an app.ts that just instantiates my FastifyInstance and then exports the log from that created instance. This allows me to use the log where ever I want to.
It looks something like this.
app.ts
import fastify from 'fastify';
const app = fastify({ logger: true /* Your logging configuration */});
export default app;
export const logger = app.log; // Allows me to log where ever I want.
server.ts
import app from './app';
... // All your fastify configuration and other stuff.
app.listen({ ... });
Now I can use the logger outside of fastify stuff.
get-object-use-case.ts
import { logger } from './app'; // Import your fastify logger to use in this class.
export class GetObjectUseCase {
private anotherClass: TestClass;
constructor(anotherClass: TestClass) {
this. anotherClass = anotherClass;
}
async execute(id: string): Promise<StoredObject> {
logger.info({/* Whatever you want to log here. */}); // Now you can use the logger here.
return this.anotherClass.getById(id); // You can just import the logger into the TestClass file to get logging enabled there.
}
}
This even allows you to log before your FastifyInstance is started. Check out this codesandbox for a running example.

Error: metatype is not a constructor when using instance of own HTTPS Server class

Good evening, I am playing around with nest and want to achieve an own HTTPS-Server that can be instantiated everywhere in other projects. Right at the beginning I get the following error-message:
TypeError: metatype is not a constructor
… when I init the following HTTPS-Server:
import { Injectable } from '#nestjs/common';
import { NestFactory } from '#nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '#nestjs/platform-fastify';
import * as fs from 'fs';
#Injectable()
export class HttpsServer {
constructor() {}
async bootstrap() {
const httpsOptions = {
key: fs.readFileSync('./certs/server.key'),
cert: fs.readFileSync('./certs/server.cert'),
};
const app = await NestFactory.create<NestFastifyApplication>(
new FastifyAdapter({ https: httpsOptions }),
);
await app.listen(443);
}
}
like this:
import { Logger } from '#nestjs/common';
import { HttpsServer } from 'server-lib';
const logger = new Logger();
const app = new HttpsServer();
app.bootstrap().then(() => {
logger.log('Bootstrap complete!');
}).catch((error) => {
logger.log('Bootstrap failed: ', error);
process.exit(1);
});
Thx for help...
All Nest applications needs to have a "RootModule" of some sort. With nest new applications this is a AppModule. This class holds the metadata for the server for how to tie everything together and run. What you could probably do is modify your HttpModule's constructor to do something like
export class HttpsServer {
constructor(private readonly rootModule: Type<any>) {} // Type comes from #nestjs/common
async bootstrap() {
const httpsOptions = {
key: fs.readFileSync('./certs/server.key'),
cert: fs.readFileSync('./certs/server.cert'),
};
const app = await NestFactory.create<NestFastifyApplication>(
this.rootModule,
new FastifyAdapter({ https: httpsOptions }),
);
await app.listen(443);
}
}
So now when you call new HttpServer() you pass in the root module and have everything else already set up. The NestFactory will instantiate the metadata from there properly, and you'll use the FastifyAdapter.
For more information, I suggest you follow the docs overview to get a feeling of how these classes fit together and why they're needed.
You probably have used an incorrect guard.
Check #UseGuards() and use the correct guard for that function.
If you're importing some files from index in the same folder such as all entities as
import * as Entities from './entities
make sure that you have that ./entities in the folder path and just plan '.'
It does not throws error but while building this is not recognized. use the full file path and it will most probably solve the issue.

Nestjs on AWS Lambda (Serverless Framework) | How to access the event parameter?

I'm hosting a Nestjs application on AWS Lambda (using the Serverless Framework).
Please note that the implementation is behind AWS API Gateway.
Question: How can I access to event parameter in my Nest controller?
This is how I bootstrap the NestJS server:
import { APIGatewayProxyHandler } from 'aws-lambda';
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { Server } from 'http';
import { ExpressAdapter } from '#nestjs/platform-express';
import * as awsServerlessExpress from 'aws-serverless-express';
import * as express from 'express';
let cachedServer: Server;
const bootstrapServer = async (): Promise<Server> => {
const expressApp = express();
const adapter = new ExpressAdapter(expressApp);
const app = await NestFactory.create(AppModule, adapter);
app.enableCors();
await app.init();
return awsServerlessExpress.createServer(expressApp);
}
export const handler: APIGatewayProxyHandler = async (event, context) => {
if (!cachedServer) {
cachedServer = await bootstrapServer()
}
return awsServerlessExpress.proxy(cachedServer, event, context, 'PROMISE')
.promise;
};
Here is a function in one controller:
#Get()
getUsers(event) { // <-- HOW TO ACCESS event HERE?? This event is undefined.
return {
statusCode: 200,
body: "This function works and returns this JSON as expected."
}
I'm struggling to understand how I can access the event paramenter, which is easily accessible in a "normal" node 12.x Lambda function:
module.exports.hello = async (event) => {
return {
statusCode: 200,
body: 'In a normal Lambda, the event is easily accessible, but in NestJS its (apparently) not.'
};
};
Solution:
Add AwsExpressServerlessMiddleware to your setup during bootstrap:
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
app.use(awsServerlessExpressMiddleware.eventContext())
Note: The app.use should be before app.init()
Now the event and context object can be accessed:
var event = req.apiGateway.event;
var context = req.apiGateway.context;
Credits: This answer on SO

Right way to get ConfigService when bootstrap microservice

I would like to know if i'm getting ConfigService the right way during bootstrap my microservice.
Is there a way to do it using NestFactory.createMicroservice()?
async function bootstrap() {
const app = await NestFactory.create(CoreModule, {
logger: new MyLogger(),
});
const configService: ConfigService = app.get(ConfigService);
app.connectMicroservice({
transport: Transport.TCP,
options: {
port: configService.PORT,
},
});
await app.startAllMicroservicesAsync();
}
Yes there is a way to do it in the NestFactory, you're already doing it the right way! If you'd like another example, this is my bootstrap function:
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new MyLogger()
});
const config = app.get<ConfigService>(ConfigService);
const port = config.get('PORT');
configure(app, config);
await app.listen(port);
scribe.info(
`Listening at http://localhost:${port}/${config.get('GLOBAL_PREFIX')}`
);
}
Where MyLogger is a custom implementation of the Nest default logger and configure(app, config) is a lot of application configuration done in a separate file to keep the bootstrap function lean and easy to maintain. Of course, the "listening at ..." also needs to be changed before heading to production, but this app is still in development for me.
The only thing I would suggest to you is changing
const configService: ConfigService = app.get(ConfigService);
to
const configService = app.get<ConfigService>(ConfigService);
using the generics for app.get() to give you the type information.

Resources