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.
Thats basically the model of the code. I tried somethings but i`m new at testing, so the lines isnt been covered.
So, I don't have idea of how I can acess and testing the content from createParamDecorator(). Or of how I test the headers, dto, and everything.
I tried to mock de ExecutionContext value and the switchToHttp() and put everything to return null and the test pass. But the lines wasn't covered.
import { plainToClass } from 'class-transformer';
// The import below fails
import { ClassType } from 'class-transformer/ClassTransformer';
import { validateOrReject } from 'class-validator';
export const RequestHeader = createParamDecorator(
async (value: any, ctx: ExecutionContext) => {
// extract headers
const headers = ctx.switchToHttp().getRequest().headers;
// Convert headers to DTO object
const dto = plainToClass(value, headers, { excludeExtraneousValues: true });
// Validate
await validateOrReject(dto);
// return header dto object
return dto;
},
);
I'm using Nestjs decorators and am trying to make the most of custom decorators. I'm trying to write my own custom #Body param decorator that validates and applies multiple decorators at the same time.
Does anyone know if the below is possible? I'm having difficulty getting the second argument in the transform call of the pipes to have metadata: ArgumentMetadata.
export const MyParamDecorator = <T>(myDto: T) => {
return createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
applyDecorators( // also get SetMeta and Pipes to validate DTO
SetMetadata('thisWorks', true)
UsePipes(CustomValidationPipe, OtherPipe), // + add MyDTO - type T somehow..
);
return doAsyncWork()
},
)();
}
#Controller('users')
export class UsersController {
#Patch(':id')
update(#MyParamDecorator() asyncWork: Promise<any>) { // <------ Promise<any> is custom async opperation that will be handled. (So I can't type the DTO here..)
return reqBody;
}
}
I ran across this question because I needed a similar answer. Hopefully what I've found is helpful.
Part 1. You can do async processing in a decorator.
And, it would resolve for you, so your controller would look like:
update(#MyParamDecorator() asyncWork: any) {
Notice that Promise<any> is just any.
Part 2. You can get ArgumentMetadata using an enhancer.
Here is a quick example, let's assume METADATA__PARAM_TYPE is myType.
param-type.enhancer.ts
export const paramTypeEnhancer: ParamDecoratorEnhancer = (
target: Record<string, unknown>,
propertyKey: string,
parameterIndex: number,
): void => {
const paramTypes = Reflect.getOwnMetadata('design:paramtypes', target, propertyKey);
const metatype = paramTypes[parameterIndex];
Reflect.defineMetadata(METADATA__PARAM_TYPE, metatype, target[propertyKey]);
};
my-decorator.ts
import { paramTypeEnhancer } from './param-type.enhancer';
export const MyDecorator = createParamDecorator(
async (data: unknown, ctx: ExecutionContext): Promise<any> => {
const metatype = Reflect.getOwnMetadata(METADATA__PARAM_TYPE, ctx.getHandler());
const argument: ArgumentMetadata = {
type: 'custom',
data: undefined,
metatype: metatype,
};
// Do processing here... You can return a promise.
},
[paramTypeEnhancer],
);
See this gist for a full annotated version: https://gist.github.com/josephdpurcell/fc04cfd428a6ee9d7ffb64685e4fe3a6
I am trying to validate that the headers of the request contain some specific data, and I am using NestJS. I found this information. While this is what I want to do, and it looks proper, the ClassType reference does not exist, and I am not sure what to use instead.
From the example, the decorator is referring to.
request-header.decorator.ts
import { createParamDecorator, ExecutionContext } from '#nestjs/commom'
import { plainToClass } from 'class-transformer';
// The import below fails
import { ClassType } from 'class-transformer/ClassTransformer';
import { validateOrReject } from 'class-validator';
export const RequestHeader = createParamDecorator(
async (value: ClassType<unknown>, ctx: ExecutionContext) => {
// extract headers
const headers = ctx.switchToHttp().getRequest().headers;
// Convert headers to DTO object
const dto = plainToClass(value, headers, { excludeExtraneousValues: true });
// Validate
await validateOrReject(dto);
// return header dto object
return dto;
},
);
Rather than passing the type through a decorator like this, I'd suggest making a custom decorator and setting the validateCustomDecorators option for the ValidationPipe to true. The decorator would look something like
const Header = createParamDecorator((data: unknown, context: ExecutionContext) => {
const req = context.switchToHttp().getRequest();
if (data) {
return req.headers[data];
}
return req.headers;
});
And now instead of #Header() from #nestjs/common you can you #Header() from this file and get the ValidationPipe to run after applying the appropriate type metadata
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);
};