admin管理员组

文章数量:1293315

I'm trying out NestJS and there are a lot of things I like but there are a couple things I'm confused about as well. Nest uses DTOs for validation and handling the data throughout the request response flow which I like. The validation works really well and is intuitive but using it with the nest/swagger package has been pretty confusing.

I want my API to return consistent responses so I wrote an interceptor that modifies the response to look like this:

{
    "succes": true,
    "statusCode": 200,
    "path": "/api/users",
    "message": "success",
    "timestamp": "2025-02-12T15:36:37.973Z",
    "data": [
        {
            "id": "f89690a7-204b-4573-bfb7-de51cd2659bc",
            "username": "John Doe",
            "email": "[email protected]",
            "password": "$argon2i$v=19$m=16,t=2,p=1$ekVPNmVBVmJJWUpFSkZrcQ$MvHC9QYdqfbvqciNUqdPig",
            "createdAt": "2025-02-11T08:36:45.878Z",
            "updatedAt": "2025-02-11T08:36:45.878Z"
        }
    ]
}

This is the interceptor for reference:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpException, HttpStatus } from "@nestjs/common";
import { Observable, throwError } from "rxjs";
import { catchError, map } from "rxjs/operators";

export type Response<T> = {
    succes: boolean;
    statusCode: number;
    path: string;
    message: string;
    data: T;
    timestamp: string;
};

@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> {
    intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
        return next.handle().pipe(
            map((res: unknown) => this.responseHandler(res, context)),
            catchError((err: HttpException) => throwError(() => this.errorHandler(err, context))),
        );
    }

    errorHandler(exception: HttpException, context: ExecutionContext) {
        const ctx = context.switchToHttp();
        const response = ctx.getResponse();
        const request = ctx.getRequest();
        const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;

        response.status(status).json({
            succes: false,
            statusCode: status,
            path: request.url,
            message: exception.message,
            timestamp: new Date().toISOString(),
            result: exception,
        });
    }

    responseHandler(res: any, context: ExecutionContext) {
        const ctx = context.switchToHttp();
        const response = ctx.getResponse();
        const request = ctx.getRequest();
        const message = "success";

        return {
            succes: true,
            statusCode: response.statusCode,
            path: request.url,
            message: message,
            timestamp: new Date().toISOString(),
            data: res,
        };
    }
}

Now when i'm trying to define the swagger definitions in my user controller I don't know what i'm supposed to do. My Controller and user DTO look as follows:

import { UsersService } from "./users.service";
import { User, CreateUser, UpdateUser } from "./users.dto";

import { Body, Controller, Delete, Get, Param, Patch, Post } from "@nestjs/common";

import { ApiBadRequestResponse, ApiCreatedResponse, ApiInternalServerErrorResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";

@Controller("users")
export class UsersController {
    constructor(private readonly usersService: UsersService) {}

    @Get()
    @ApiOperation({ summary: "Retreive all users" })
    @ApiOkResponse({ description: "Succesfully retrieved users", type: User, isArray: true })
    @ApiInternalServerErrorResponse({ description: "Database error" })
    getUsers() {
        return this.usersService.getUsers();
    }
}
import { ApiProperty, PartialType } from "@nestjs/swagger";
import { IsEmail, IsNotEmpty, IsString, MinLength, IsDateString } from "class-validator";

export class User {
    @ApiProperty({ example: "f89690a7-204b-4573-bfb7-de51cd2659bc" })
    id: string;

    @ApiProperty({ example: "John Doe" })
    username: string;

    @ApiProperty({ example: "[email protected]" })
    email: string;

    @ApiProperty()
    dateCreated: Date;

    @ApiProperty()
    dateUpdated: Date;
}

export class CreateUser {
    @ApiProperty()
    @IsNotEmpty()
    @IsString()
    username: string;

    @ApiProperty()
    @IsNotEmpty()
    @IsEmail()
    email: string;

    @ApiProperty()
    @IsNotEmpty()
    @MinLength(8)
    password: string;
}

export class UpdateUser extends PartialType(CreateUser) {}

I'm using the ApiOkResponse provided by the swagger package with my User DTO but how can i wrap the UserDTO with a SuccesResponse DTO? I Can't use a typescript generic because the swagger package doesn't support using those. So this wouldn't doe the trick:

@ApiOkResponse({ description: "Succesfully retrieved users", type: SuccesResponse<User> })
import { ApiProperty } from "@nestjs/swagger";

export class SuccesResponse<Type> {
    @ApiProperty({ example: true })
    success: boolean;

    @ApiProperty({ example: 200 })
    statusCode: number;

    @ApiProperty({ example: "/api/users" })
    path: string;

    @ApiProperty({ example: "success" })
    message: string;

    @ApiProperty({ example: "2025-02-12T15:36:37.973Z" })
    timestamp: string;

    @ApiProperty()
    data: Type;
}

I feel like there should be an easy way to do this throughout the whole application. I found u can use the schema prop to define complexer objects but that just seems so repetitive.

Then there is the service layer where i'm using the types provided by Prisma because these follow the DB structure. Should i not use these and always make use of the DTO's or what are the best practices here?

import { Injectable, InternalServerErrorException, NotFoundException } from "@nestjs/common";
import { DatabaseService } from "src/database/database.service";
import { Prisma, User } from "@prisma/client";

@Injectable()
export class UsersService {
    constructor(private readonly databaseService: DatabaseService) {}

    async getUsers(): Promise<User[]> {
        try {
            return await this.databaseService.user.findMany();
        } catch (error) {
            throw new InternalServerErrorException("Failed to retrieve users");
        }
    }
}

Recap:

-> How can i create wrapper DTO's for consistent responses and good swagger docs?

-> Should i use DTO's everywhere or are Prisma types the way to go when interacting with the DB?

I hope i was clear enough. Thanks for any help!

本文标签: