express-typescript icon indicating copy to clipboard operation
express-typescript copied to clipboard

Not able to crate post method with schema validation.

Open PRE-ShashankPatil opened this issue 1 year ago • 11 comments

Not able to crate post method.

Please provide one example of post, put method with schema validation.

PRE-ShashankPatil avatar Jun 23 '24 14:06 PRE-ShashankPatil

Missing code:

const app: Express = express();

app.use(express.urlencoded()) 
app.use(express.json()) // need to add for request body parser

Also add example for post method zod with open api

PRE-ShashankPatil avatar Jun 23 '24 20:06 PRE-ShashankPatil

Same here. Could not create post requests. Didn't manage to make Swagger working but via Postman the create endpoint works. Here is a simple code for creating a user:

userModel.ts

import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';

import { commonValidations } from '@/common/utils/commonValidation';

extendZodWithOpenApi(z);

export type User = z.infer<typeof UserSchema>;
export const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  createdAt: z.date(),
});

// Input Validation for 'GET users/:id' endpoint
export const GetUserSchema = z.object({
  params: z.object({ id: commonValidations.id }),
});

export const CreateUserSchema = z.object({
  body: z.object(
    {
      name: z.string().min(1, 'Name is required'),
    }
  )
  
});

userRepository.ts

import { User } from '@/api/user/userModel';

export const users: User[] = [
 { id: 1, name: 'Alice', createdAt: new Date() },
 { id: 2, name: 'Bob', createdAt: new Date() },
];

export const userRepository = {
 findAllAsync: async (): Promise<User[]> => {
   return users;
 },

 findByIdAsync: async (id: number): Promise<User | null> => {
   return users.find((user) => user.id === id) || null;
 },

 createAsync: async (userData: User): Promise<User> => {
   const newUser = {
     ...userData,
     id: users.length + 1, // Simple way to generate a new ID
     createdAt: new Date(),
   };
   users.push(newUser);
   return newUser;
 }
};

userService

import { StatusCodes } from 'http-status-codes';

import { User } from '@/api/user/userModel';
import { userRepository } from '@/api/user/userRepository';
import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse';
import { logger } from '@/server';

export const userService = {
  // Retrieves all users from the database
  findAll: async (): Promise<ServiceResponse<User[] | null>> => {
    try {
      const users = await userRepository.findAllAsync();
      if (!users) {
        return new ServiceResponse(ResponseStatus.Failed, 'No Users found', null, StatusCodes.NOT_FOUND);
      }
      return new ServiceResponse<User[]>(ResponseStatus.Success, 'Users found', users, StatusCodes.OK);
    } catch (ex) {
      const errorMessage = `Error finding all users: $${(ex as Error).message}`;
      logger.error(errorMessage);
      return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR);
    }
  },

  // Retrieves a single user by their ID
  findById: async (id: number): Promise<ServiceResponse<User | null>> => {
    try {
      const user = await userRepository.findByIdAsync(id);
      if (!user) {
        return new ServiceResponse(ResponseStatus.Failed, 'User not found', null, StatusCodes.NOT_FOUND);
      }
      return new ServiceResponse<User>(ResponseStatus.Success, 'User found', user, StatusCodes.OK);
    } catch (ex) {
      const errorMessage = `Error finding user with id ${id}:, ${(ex as Error).message}`;
      logger.error(errorMessage);
      return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR);
    }
  },
  // Creates a new user
  createUser: async (userData: User): Promise<ServiceResponse<User | null>> => {
    try {
      const newUser = await userRepository.createAsync(userData);
      return new ServiceResponse<User>(
        ResponseStatus.Success,
        'User created successfully',
        newUser,
        StatusCodes.CREATED
      );
    } catch (ex) {
      const errorMessage = `Error creating user: ${(ex as Error).message}`;
      const newUser = await userRepository.createAsync(userData);
      logger.error(errorMessage);
      return new ServiceResponse<User>(
        ResponseStatus.Failed,
        errorMessage,
        newUser,
        StatusCodes.INTERNAL_SERVER_ERROR
      );
    }
  },
};

userRouter

import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import express, { Request, Response, Router } from 'express';
import { z } from 'zod';

import { GetUserSchema, UserSchema, CreateUserSchema } from '@/api/user/userModel';
import { userService } from '@/api/user/userService';
import { createApiResponse } from '@/api-docs/openAPIResponseBuilders';
import { handleServiceResponse, validateRequest } from '@/common/utils/httpHandlers';

export const userRegistry = new OpenAPIRegistry();

userRegistry.register('User', UserSchema);

export const userRouter: Router = (() => {
  const router = express.Router();

  userRegistry.registerPath({
    method: 'get',
    path: '/users',
    tags: ['User'],
    responses: createApiResponse(z.array(UserSchema), 'Success'),
  });

  router.get('/', async (_req: Request, res: Response) => {
    const serviceResponse = await userService.findAll();
    handleServiceResponse(serviceResponse, res);
  });

  userRegistry.registerPath({
    method: 'get',
    path: '/users/{id}',
    tags: ['User'],
    request: { params: GetUserSchema.shape.params },
    responses: createApiResponse(UserSchema, 'Success'),
  });

  router.get('/:id', validateRequest(GetUserSchema), async (req: Request, res: Response) => {
    const id = parseInt(req.params.id as string, 10);
    const serviceResponse = await userService.findById(id);
    handleServiceResponse(serviceResponse, res);
  });

  userRegistry.registerPath({
    method: 'post',
    path: '/users',
    tags: ['User'],
    request: { body: {
      content: {
        "application/json": {
          schema: CreateUserSchema,
        },
      
      }
    } },
    responses: createApiResponse(UserSchema, 'Success'),
  });


  // POST /users - Create a new user
  router.post('/', validateRequest(CreateUserSchema), async (req: Request, res: Response) => {
  const newUserData = req.body;
  const serviceResponse = await userService.createUser(newUserData);

  res.status(serviceResponse.statusCode).send(serviceResponse);
});

  return router;
})();

server.ts

import cors from 'cors';
import express, { Express } from 'express';
import helmet from 'helmet';
import mongoose from 'mongoose';
import { pino } from 'pino';

import { healthCheckRouter } from '@/api/healthCheck/healthCheckRouter';
import { userRouter } from '@/api/user/userRouter';
import { openAPIRouter } from '@/api-docs/openAPIRouter';
import errorHandler from '@/common/middleware/errorHandler';
import rateLimiter from '@/common/middleware/rateLimiter';
import requestLogger from '@/common/middleware/requestLogger';

import { userPointsRouter } from './api/points/userPointsRouter';

const logger = pino({ name: 'server start' });
const app: Express = express();

app.use((req, res, next) => {
  console.log(`Incoming request: ${req.method} ${req.path}`);
  console.log('Body:', req.body);
  next();
});
 // MAKE SURE YOU HAVE THIS IN THE FOLLOWING ORDER 
// Parsing application/json
app.use(express.json()); 
app.use(express.urlencoded({ extended: false })) 

// Set the application to trust the reverse proxy
app.set('trust proxy', true);

// CORS configuration
const corsOptions = {
  origin: 'https://localhost:5173',       // Specify YOUR frontend URL
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  credentials: true, // Allow cookies to be sent
  optionsSuccessStatus: 204,
};
// Middlewares
app.use(cors(corsOptions));
app.use(helmet());
app.use(rateLimiter);

// Request logging
app.use(requestLogger);

// Routes
app.use('/health-check', healthCheckRouter);
app.use('/users', userRouter);
app.use('/points', userPointsRouter); // Use the user points router

    // Swagger UI
    app.use(openAPIRouter);

// Error handlers
app.use(errorHandler());

export { app, logger };

devdomsos avatar Jul 13 '24 02:07 devdomsos

@devdomsos In userModel instead of

export const CreateUserSchema = z.object({
  body: z.object(
    {
      name: z.string().min(1, 'Name is required'),
    }
  )
  
});

use,

export const CreateUserSchema = z.object({
      name: z.string().min(1, 'Name is required')
});

amabirbd avatar Aug 15 '24 10:08 amabirbd

I managed to get around this by adding the following lines of code

server.ts app.use(express.json())

httpHandlers.ts > validateRequest

 schema.parse({
            ...req.body,
            query: req.query,
            params: req.params,
        });

and my schema format is

export const CreateUserSchema = z.object({
      name: z.string().min(1, 'Name is required'),
      ....
});

so that the client doesn't have to send a json with nested "body" property

ktyntang avatar Aug 28 '24 16:08 ktyntang

hi i am still getting this issue validation fail.

export const SignupSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  phoneNumber: z
    .string()
    .regex(/^\d{10}$/, "Invalid phone number. Must be 10 digits."),
});
export const validateRequest =
  (schema: ZodSchema) => (req: Request, res: Response, next: NextFunction) => {
    try {
      schema.parse({ body: req.body, query: req.query, params: req.params });
      next();
    } catch (err) {
      const errorMessage = `Invalid input: ${(err as ZodError).errors
        .map((e) => e.message)
        .join(", ")}`;
      const statusCode = StatusCodes.BAD_REQUEST;
      const serviceResponse = ServiceResponse.failure(
        errorMessage,
        null,
        statusCode
      );
      console.log(err);
      return handleServiceResponse(serviceResponse, res);
    }
  };
SignupRegistry.registerPath({
  method: "post",
  path: "/signup",
  tags: ["Signup"],
  requestBody: {
    content: {
      "application/json": {
        schema: {
          properties: {
            name: {
              type: "string",
              example: "xyze",
            },
            email: {
              type: "string",
              example: "[email protected]",
            },
            phoneNumber: {
              type: "string",
              example: "9000000001",
            },
          },
        },
      },
    },
  },
  responses: createApiResponse(z.array(SignupSchema), "Success"),
});

signupRouter.post(
  "/",
  validateRequest(SignupSchema),
  signupController.createUser
);

following is the error

{
      code: 'invalid_type',
      expected: 'string',
      received: 'undefined',
      path: [Array],
      message: 'Required'
    },
    {
      code: 'invalid_type',
      expected: 'string',
      received: 'undefined',
      path: [Array],
      message: 'Required'
    },
    {
      code: 'invalid_type',
      expected: 'string',
      received: 'undefined',
      path: [Array],
      message: 'Required'
    }

609harsh avatar Oct 23 '24 16:10 609harsh

I managed to get around this by adding the following lines of code

server.ts app.use(express.json())

httpHandlers.ts > validateRequest

 schema.parse({
            ...req.body,
            query: req.query,
            params: req.params,
        });

and my schema format is

export const CreateUserSchema = z.object({
      name: z.string().min(1, 'Name is required'),
      ....
});

so that the client doesn't have to send a json with nested "body" property

i tried this but getting TypeError: req.body is not a function

609harsh avatar Oct 23 '24 16:10 609harsh

Structure for Post method with body should be something like this:

userRegistry.registerPath({
  method: "post",
  path: "/users",
  tags: ["User"],
  request: {
    body: {
      description: "post request create a User",
      content: {
        "application/json": {
          schema: CreateUserSchema,
        },
      },
    },
  },
  responses: createApiResponse(CreateUserSchema, "Success"),
});```

amabirbd avatar Oct 23 '24 16:10 amabirbd

Structure for Post method with body should be something like this:

userRegistry.registerPath({
  method: "post",
  path: "/users",
  tags: ["User"],
  request: {
    body: {
      description: "post request create a User",
      content: {
        "application/json": {
          schema: CreateUserSchema,
        },
      },
    },
  },
  responses: createApiResponse(CreateUserSchema, "Success"),
});```

Thanks for this one. But validationRequest is the problem. Any suggestions on that?

609harsh avatar Oct 23 '24 17:10 609harsh

@609harsh you should probably pass SignupSchema instead of signupValidation in

  validateRequest(signupValidation)

amabirbd avatar Oct 23 '24 18:10 amabirbd

@609harsh you should probably pass SignupSchema instead of signupValidation in

  validateRequest(signupValidation)

oh my bad. in actual codebase it is SignupSchema only. I was just trying some other things ended up pasting this one😅

609harsh avatar Oct 23 '24 19:10 609harsh

sample post schema i have working - https://github.com/Decoupled-Saas/api/blob/main/src/schemas/authSchema.ts#L29 you need to wrap your schema in an additional body object

allanice001 avatar Apr 23 '25 00:04 allanice001