I have been developing a lambda project and we are using lambda-api package. Then I have defined some decorators called Get
and Post
to map a route into lambda api object. With these decorators I have defined a class called ProductApi to hold methods that can be configured using those decorators and passing a route path. It works fine.
The problem is that when I have a class like ProductApi
the constructor is never called and if I want to add some dependencies (like a Service or a Repository) it will never be defined. In this example, the /health
route works fine because it does not use anything from the object instance, but other routes does not.
How can I make sure that the constructor will be called and define the service instance?
const api = createAPI();
function Get(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
api.get(path, descriptor.value.bind(target));
};
}
function Post(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
api.post(path, descriptor.value.bind(target));
};
}
class ProductApi {
private someValue: string;
constructor(private readonly productService: IProductService = new ProductService()) {
// this scope does not execute
this.someValue = "some value";
}
@Get('/health')
async healthCheckr(req: Request, res: Response) {
console.log(`Executing -- GET /health`);
// this.someValue does not exists here
return res.status(200).json({ ok: true });
}
@Get('/products')
async getProducts(req: Request, res: Response) {
console.log(`Executing -- GET /products`);
const data = this.productService.getProductsFromService(); // error: Cannot read properties of undefined (reading 'getProductsFromService')
return res.status(200).json(data);
}
@Post('/products')
async postProducts(req: Request, res: Response) {
console.log(`Executing -- POST /products`);
const product = this.productService.saveProduct('Drums', 1200); // erro: Cannot read properties of undefined (reading 'saveProduct')
return res.status(201).json(product);
}
}
export const lambdaHandler = async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
console.log('SCOPE lambda');
return await api.run(event, context);
};
Note: I don’t want to use frameworks, I just want a easy way to configure routes on the lamda api instanmce.
2
Answers
You need to first store metadata about how to bind routes, and then apply that on class creation
https://tsplay.dev/mbvA4m (runnable)
Unlike in C#, in JS a "method" is just a function stuck to an object. You can easily put it in a variable or stick it to another object. This basically defines what
this
is inside that "method".And a "class" constructor is just a function that creates a new object and tells it, "if someone’s looking for some property that you don’t have, forward them to my
prototype
object over here." Then it executes the code inside the constructor with that object asthis
.That is JS’ prototypal inheritance in a nutshell, and even if JS has received a
class
keyword in the meantime, that’s what still happens behind the scenes.Why am I explaining this?
Because decorator are working on that prototype object. This line here
api.get(path, descriptor.value.bind(target));
takes the method from that prototype, permanently binds the prototype object asthis
(so the resulting function will only know the prototype object and never ever see any real instance) and uses the bound function as a callback for that route.So currently, even if that class would magically be instantiated (by whom; I don’t know) the function that you’ve passed to the route will have no knowledge of that.
imo. Your decorator should look more like this:
Sidenote: Dimava brought up this topic; these are legacy decorators. TS adapted them long before there was a spec for decorator in JS. Now there is one and it significantly differs from these legacy decorators and TS has finally implemented the spec in V5. You (and me) should get updated on the new syntax and adopt it, because this syntax will probably be deprecated pretty soon.