Assign route dynamically Node/Express - node.js

I need dynamically assign a new route but it for some reason refuses to work.
When I send a request in the Postman it just keeps waiting for a response
The whole picture of what I am doing is the following:
I've got a controller with a decorator on one of its methods
#Controller()
export class Test {
#RESTful({
endpoint: '/product/test',
method: 'post',
})
async testMe() {
return {
type: 'hi'
}
}
}
export function RESTful({ endpoint, method, version }: { endpoint: string, version?: string, method: HTTPMethodTypes }) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value
Reflect.defineMetadata(propertyKey, {
endpoint,
method,
propertyKey,
version
}, target)
return originalMethod
}
}
export function Controller() {
return function (constructor: any) {
const methods = Object.getOwnPropertyNames(constructor.prototype)
Container.set(constructor)
for (let action of methods) {
const route: RESTfulRoute = Reflect.getMetadata(action, constructor.prototype)
if (route) {
const version: string = route.version ? `/${route.version}` : '/v1'
Container.get(Express).injectRoute((instance: Application) => {
instance[route.method](`/api${version}${route.endpoint}`, async () => {
return await Reflect.getOwnPropertyDescriptor(constructor, route.propertyKey)
// return await constructor.prototype[route.propertyKey](req, res)
})
})
}
}
}
}
Is it possible to dynamically set the route in the way?
I mainly use GraphQL but sometimes I need RESTful API too. So, I want to solve this by that decorator

In order for the response to finish, there must be a res.end() or res.json(...) or similar. But I cannot see that anywhere in your code.

Related

Fastify Typescript request query

I'm trying to put together a simple endpoint following the Fastify with Typescript docs here:
https://www.fastify.io/docs/v3.1.x/TypeScript/
export default async function foo(fastify: any) {
const MyInstance = new Foo(fastify.db);
app.get<{ Querystring: IQueryString, Headers: IHeaders }>(
"/foo",
async (request: FastifyRequest, reply: FastifyReply) => {
console.log(request.query); // *prints query object*
const { queryObj } = request.query; // *Gives error: Object is of type 'unknown'*
const result = await MyInstance.getFoo(queryObj);
reply.status(200).send(result);
}
);
}
Why do I get the error when I try to access the request.query object and how do I fix it?
By default FastifyRequest.query's type RequestQuerystringDefault maps to unknown because one cannot guess which attributes/type you'll want to set for it.
Should you have a defined type for the query of some request, just define that request type and use it:
type MyRequest = FastifyRequest<{
Querystring: { queryObj: MyQueryObject }
}>
then specify it as the expected request type:
async (request: MyRequest, reply: FastifyReply) => {
const { queryObj } = request.query // Ok
}
If you write the code to look it like Express.js, try that one:
app.get('/foo', async (req: FastifyRequest<{
Params: {
name: string,
};
}>,
rep: FastifyReply,) => {
const name = req.params.name // string
})

How to pass a child Interface to a parent class?

I have this:
LocationController.ts
import {GenericController} from './_genericController';
interface Response {
id : number,
code: string,
name: string,
type: string,
long: number,
lat: number
}
const fields = ['code','name','type','long','lat'];
class LocationController extends GenericController{
tableName:string = 'location';
fields:Array<any> = fields;
}
const locationController = new LocationController();
const get = async (req, res) => {
await locationController._get(req, res);
}
export {get};
GenericController.ts
interface Response {
id : number
}
export class GenericController{
tableName:string = '';
fields:Array<any> = [];
_get = async (req, res) => {
try{
const id = req.body['id'];
const send = async () => {
const resp : Array<Response> = await db(this.tableName).select(this.fields).where('id', id)
if (resp[0] === undefined) {
// some error handling
}
res.status(status.success).json(resp[0]);
}
await send();
}catch (error){
// some error handling
}
}
}
What I want to do is to pass the Response interface from LocationController to the GenericController parent, so that the response is typed accurately depending on how the child class has defined it. Clearly it doesn't work like this since the interface is defined outside of the class so the parent has no idea about the Response interface in the LocationController.ts file.
I've tried passing interface as an argument in the constructor, that doesn't work. So is there a way I can make this happen? I feel like I'm missing something really simple.
Typically, generics are used in a situation like this. Here's how I'd do it:
interface Response {
id: number;
}
// Note the generic parameter <R extends Response>
export class GenericController<R extends Response> {
tableName: string = "";
fields: Array<any> = [];
_get = async (req, res) => {
try {
const id = req.body["id"];
const send = async () => {
// The array is now properly typed. You don't know the exact type,
// but you do know the constraint - R is some type of `Response`
let resp: Array<R> = await db(this.tableName).select(this.fields).where("id", id);
if (resp[0] === undefined) {
// some error handling
}
res.status(status.success).json(resp[0]);
};
await send();
} catch (error) {
// some error handling
}
};
}
import { GenericController } from "./_genericController";
interface Response {
id: number;
code: string;
name: string;
type: string;
long: number;
lat: number;
}
const fields = ["code", "name", "type", "long", "lat"];
// Here we tell the GenericController exactly what type of Response it's going to get
class LocationController extends GenericController<Response> {
tableName: string = "location";
fields: Array<any> = fields;
}
const locationController = new LocationController();
const get = async (req, res) => {
await locationController._get(req, res);
};
export { get };
If this is not enough and you wish to somehow know the exact response type you're going to get, I believe the only way is a manual check. For example:
import { LocationResponse } from './locationController';
// ... stuff
// Manual runtime type check
if (this.tableName === 'location') {
// Manual cast
resp = resp as Array<LocationResponse>
}
// ...
You could also check the form of resp[0] (if (resp[0].hasOwnProperty('code')) { ... }) and cast accordingly. There are also nicer ways to write this, but the basic idea remains the same.
Generally, a properly written class should be unaware of any classes that inherit from it. Putting child-class-specific logic into your generic controller is a code smell. Though as always, it all depends on a particular situation.

How to Type Fastify Reply payloads?

I'm just getting into Fastify with Typescript and really enjoying it.
However, I'm trying to figure out if I can type the response payload. I have the response schema for serialization working and that may be sufficient, but I have internally typed objects (such as IUser) that it would be nice to have Typescript check against.
The following works great, but I'd like to return an TUser for example and have typescript if I return something different. Using schema merely discludes fields.
interface IUser {
firstname: string,
lastname: string
} // Not in use in example
interface IUserRequest extends RequestGenericInterface {
Params: { username: string };
}
const getUserHandler = async (
req: FastifyRequest<IUserRequest, RawServerBase, IncomingMessage | Http2ServerRequest>
) => {
const { username } = req.params;
return { ... }; // Would like to return instance of IUser
};
app.get<IUserRequest>('/:username', { schema }, helloWorldHandler);
Is there an equivalent of RequestGenericInterface I can extend for the response?
Small Update: It seems that the reply.send() can be used to add the type, but it would be nice for self-documentation sake to provide T higher up.
From the documentation:
Using the two interfaces, define a new API route and pass them as generics. The shorthand route methods (i.e. .get) accept a generic object RouteGenericInterface containing five named properties: Body, Querystring, Params, Headers and Reply.
You can use the Reply type.
interface MiscIPAddressRes {
ipv4: string
}
server.get<{
Reply: MiscIPAddressRes
}>('/misc/ip-address', async (req, res) => {
res
.status(_200_OKAY)
.send({
ipv4: req.ip // this will be typechecked
})
})
After looking at the type definitions, I found out that there is also an alternative way to only type-check the handler (like in Julien TASSIN's answer), like this:
import { FastifyReply, FastifyRequest, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { RouteGenericInterface } from "fastify/types/route";
interface IUser {
firstname: string;
lastname: string;
}
interface IUserRequest extends RouteGenericInterface {
Params: { username: string };
Reply: IUser; // put the response payload interface here
}
function getUserHandler(
request: FastifyRequest<IUserRequest>,
reply: FastifyReply<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
IUserRequest // put the request interface here
>
) {
const { username } = request.params;
// do something
// the send() parameter is now type-checked
return reply.send({
firstname: "James",
lastname: "Bond",
});
}
You can also create your own interface with generic to save writing repeating lines, like this:
import { FastifyReply, FastifyRequest, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { RouteGenericInterface } from "fastify/types/route";
export interface FastifyReplyWithPayload<Payload extends RouteGenericInterface>
extends FastifyReply<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
Payload
> {}
then use the interface like this:
function getUserHandler(
request: FastifyRequest<IUserRequest>,
reply: FastifyReplyWithPayload<IUserRequest>
) {
const { username } = request.params;
// do something
// the send() parameter is also type-checked like above
return reply.send({
firstname: "James",
lastname: "Bond",
});
}
If you want to type the handler only, you can perform it this way
import { RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault, RouteHandler, RouteHandlerMethod } from "fastify";
const getUserHandler: RouteHandlerMethod<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
{ Reply: IUser; Params: { username: string } }
> = async (
req: FastifyRequest<IUserRequest, RawServerBase, IncomingMessage | Http2ServerRequest>
) => {
const { username } = req.params;
return { ... }; // Would like to return instance of IUser
};
Trying to type these was a truely awful experience. Thanks to the other answers, this is where I ended up. Bit of a code dump to make life easier for others.
request-types.ts
With this I am standardising my response to optionally have data and message.
import {
FastifyReply,
FastifyRequest,
RawReplyDefaultExpression,
RawRequestDefaultExpression,
RawServerDefault,
} from 'fastify';
type ById = {
id: string;
};
type ApiRequest<Body = void, Params = void, Reply = void> = {
Body: Body;
Params: Params;
Reply: { data?: Reply & ById; message?: string };
};
type ApiResponse<Body = void, Params = void, Reply = {}> = FastifyReply<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
ApiRequest<Body, Params, Reply>
>;
type RouteHandlerMethod<Body = void, Params = void, Reply = void> = (
request: FastifyRequest<ApiRequest<Body, Params, Reply>>,
response: ApiResponse<Body, Params, Reply>
) => void;
export type DeleteRequestHandler<ReplyPayload = ById> = RouteHandlerMethod<void, ById, ReplyPayload>;
export type GetRequestHandler<ReplyPayload> = RouteHandlerMethod<void, ById, ReplyPayload>;
export type PostRequestHandler<Payload, ReplyPayload> = RouteHandlerMethod<Payload, void, ReplyPayload>;
export type PatchRequestHandler<Payload, ReplyPayload> = RouteHandlerMethod<Payload, ById, ReplyPayload>;
export type PutRequestHandler<Payload, ReplyPayload> = RouteHandlerMethod<Payload, ById, ReplyPayload>;
Usage
get-account.ts - GetRequestHandler
export const getAccount: GetRequestHandler<AccountResponseDto> = async (request, reply) => {
const { id } = request.params;
...
const account = await Account.findOne....
...
if (account) {
return reply.status(200).send({ data: account });
}
return reply.status(404).send({ message: 'Account not found' });
};
delete-entity.ts - DeleteRequestHandler
export const deleteEntity: DeleteRequestHandler = async (request, reply) => {
const { id } = request.params;
...
// Indicate success by 200 and returning the id of the deleted entity
return reply.status(200).send({ data: { id } });
};
update-account.ts - PatchRequestHandler
export const updateAccount: PatchRequestHandler<
UpdateAccountRequestDto,
AccountResponseDto
> = async (request, reply) => {
const { id } = request.params;
...
return reply.status(200).send({ data: account });
};
register-account-routes.ts - No errors with provided handler.
export const registerAccountRoutes = (app: FastifyInstance) => {
app.get(EndPoints.ACCOUNT_BY_ID, getAccount);
app.patch(EndPoints.ACCOUNT_BY_ID, updateAccount);
app.post(EndPoints.ACCOUNTS_AUTHENTICATE, authenticate);
app.put(EndPoints.ACCOUNTS, createAccount);
};

Import module with folder and passing data to module in nodejs

I Found The Tutorial about
Designing a clean REST API with Node.js (Express + Mongo)
project in github.
but the problem is i didn't get the concept of routing in one part.
the misundrestanding part is how is it possible to pass httpRequest data to handle method within contact-endpoint module?
because handle method is in here export default function makeContactsEndpointHandler({ contactList }) {
return async function handle(httpRequest) {
this is the index of project:
import handleContactsRequest from "./contacts";
import adaptRequest from "./helpers/adapt-request";
app.all("/contacts", contactsController);
app.get("/contacts/:id", contactsController);
function contactsController(req, res) {
const httpRequest = adaptRequest(req);
handleContactsRequest(httpRequest)
.then(({ headers, statusCode, data }) =>
res.set(headers).status(statusCode).send(data)
)
.catch((e) => res.status(500).end());
}
this is the adaptRequest:
export default function adaptRequest (req = {}) {
return Object.freeze({
path: req.path,
method: req.method,
pathParams: req.params,
queryParams: req.query,
body: req.body
})
}
this is the handleContactsRequest module:
import makeDb from "../db";
import makeContactList from "./contact-list";
import makeContactsEndpointHandler from "./contacts-endpoint";
const database = makeDb();
const contactList = makeContactList({ database });
const contactsEndpointHandler = makeContactsEndpointHandler({ contactList });
export default contactsEndpointHandler;
this is part of contact-endpoint module:
export default function makeContactsEndpointHandler({ contactList }) {
return async function handle(httpRequest) {
switch (httpRequest.method) {
case "POST":
return postContact(httpRequest);
case "GET":
return getContacts(httpRequest);
default:
return makeHttpError({
statusCode: 405,
errorMessage: `${httpRequest.method} method not allowed.`,
});
}
}
makeContactsEndpointHandler is a function that returns a function (async handle(xxx)).
In handleContactsRequest, we export the result of the call: makeContactsEndpointHandler({ contactList }). Which is therefore the function async handle(xxx) itself.
So, in index, when we call handleContactsRequest with the constant httpRequest as argument, we're actually calling that handle(xxx) function. (I wrote xxx as parameter name to highlight the difference between the two httpRequest declarations.)

How to use value which returned from controller? Testing controllers on NestJs

Controller and method for testing:
import { Controller, Get, Response, HttpStatus, Param, Body, Post, Request, Patch, Delete, Res } from '#nestjs/common';
#Controller('api/parts')
export class PartController {
constructor(private readonly partsService: partsService) { }
#Get()
public async getParts(#Response() res: any) {
const parts = await this.partsService.findAll();
return res.status(HttpStatus.OK).json(parts);
}
}
And this is unit test which must test getParts method:
describe('PartsController', () => {
let partsController: PartsController;
let partsService: partsService;
beforeEach(async () => {
partsService = new partsService(Part);
partsController= new PartsController(partsService);
});
describe('findAll', () => {
it('should return an array of parts', async () => {
const result = [{ name: 'TestPart' }] as Part[];
jest.spyOn(partsService, 'findAll').mockImplementation(async () => result);
const response = {
json: (body?: any) => {
expect(body).toBe(result);
},
status: (code: number) => response,
};
await partsController.getParts(response);
});
});
});
This test works correctly, but I think this is a bad solution. When I investigated this problem, I saw this option:
const response = {
json: (body?: any) => {},
status: (code: number) => response,
};
expect(await partsController.getParts(response)).toBe(result);
But when I try it my test don't work, cause await partsController.getParts(response) // undefined
So what should I do to make my test look good?
In solution I use: nodeJS sequelize, nestJS, typescript
Alright so I guess your problems lies on the way you instantiate and use your controller & service.
Let NestJs Testing utils do the job for you, like this:
describe('Parts Controller', () => {
let partsController: PartsController;
let partsService: PartsService;
beforeEach(async () => {
// magic happens with the following line
const module = await Test.createTestingModule({
controllers: [
PartsController
],
providers: [
PartsService
//... any other needed import goes here
]
}).compile();
partsService = module.get<PartsService>(PartsService);
partsController = module.get<PartsController>(PartsController);
});
// The next 4 lines are optional and depends on whether you would need to perform these cleanings of the mocks or not after each tests within this describe section
afterEach(() => {
jest.restoreAllMocks();
jest.resetAllMocks();
});
it('should be defined', () => {
expect(partsController).toBeDefined();
expect(partsService).toBeDefined();
});
describe('findAll', () => {
it('should return an array of parts', async () => {
const result: Part[] = [{ name: 'TestPart' }];
jest.spyOn(partsService, 'findAll').mockImplementation(async (): Promise<Part[]> => Promise.resolve(result));
const response = {
json: (body?: any) => {},
status: (code: number) => HttpStatus.OK,
};
expect(await partsController.getParts(response)).toBe(result);
});
});
});
I haven't tested the code myself so give it a try (not too sure about the response mocking for the Response type in the Parts Controller tho).
Regarding the Parts Controller by the way you should take advantage of the Response type from express though - try to rewrite code as follows:
import { Controller, Get, Response, HttpStatus, Param, Body, Post, Request, Patch, Delete, Res } from '#nestjs/common';
import { Response } from 'express';
#Controller('api/parts')
export class PartController {
constructor(private readonly partsService: partsService) { }
#Get()
public async getParts(#Response() res: Response) { // <= see Response type from express being used here
const parts = await this.partsService.findAll();
return res.status(HttpStatus.OK).json(parts);
}
}
Finally have a look at this section of the nest official documentation, maybe it can give you some insights about what you're trying to achieve:
- Nest testing section
- Nest library approach
In the second link, at the almost beginning of the page, it is stated in the https://docs.nestjs.com/controllers#request-object section the following:
For compatibility with typings across underlying HTTP platforms (e.g., Express and Fastify), Nest provides #Res() and #Response()
decorators. #Res() is simply an alias for #Response(). Both directly
expose the underlying native platform response object interface. When
using them, you should also import the typings for the underlying
library (e.g., #types/express) to take full advantage. Note that when
you inject either #Res() or #Response() in a method handler, you put
Nest into Library-specific mode for that handler, and you become
responsible for managing the response. When doing so, you must issue
some kind of response by making a call on the response object (e.g.,
res.json(...) or res.send(...)), or the HTTP server will hang.
Hope it helps, don't hesitate to comment, or share your solution if it helped finding another one ! :)
Welcome to StackOverflow platform by the way !

Resources