Skip to content

Instantly share code, notes, and snippets.

@Sowed
Last active May 5, 2020 05:53
Show Gist options
  • Save Sowed/246614abb27f71f668b6e9a314ae6acd to your computer and use it in GitHub Desktop.
Save Sowed/246614abb27f71f668b6e9a314ae6acd to your computer and use it in GitHub Desktop.
Migrating from Custom Express Server with `express-validator` to Next API Routes
import express, { Request, Response, NextFunction } from 'express';
import { check, validationResult, ValidationChain } from 'express-validator';
import bodyParser from 'body-parser';
import next from 'next';
const postMail = async (req: Request, res: Response): Promise<void> => {
// Use nodemailer to post the email. Setup mailer() with nodemailer
// and call it to Post an Email and return response
const mailResponse = await mailer(req.body);
res.json(mailResponse);
};
const postValidationRules = (): ValidationChain[] => {
return [check('email').isEmail(), check('message').escape()];
};
const validate = (
req: Request,
res: Response,
nextFn: NextFunction
): Response<string> | void => {
const errors = validationResult(req);
if (errors.isEmpty()) {
return nextFn();
}
return res.status(422).json({
errors,
});
};
const port = parseInt(process.env.PORT || '3000', 10);
const clientUrl = process.env.CLIENT_URL || 'http://localhost';
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
(async (): Promise<void> => {
try {
await app.prepare().then(() => {
const server = express();
server.use(bodyParser.json());
server.use(bodyParser.urlencoded({ extended: true }));
server.post(
'/api/post-contact-form',
postValidationRules(),
validate,
postMail
);
server.get('*', (req: Request, res: Response) => {
return handle(req, res);
});
server.post('*', (req: Request, res: Response) => {
return handle(req, res);
});
server.listen(port, (error: Error) => {
if (error) throw error;
console.info(`> ✔ Ready on Server: ${clientUrl}:${port}`);
});
});
} catch (error) {
console.error(`❌ Error`, error);
process.exit(1);
}
})();
@hoangvvo
Copy link

hoangvvo commented May 4, 2020

My first two approaches do not actually work with Express middleware, but you can do something like this if you don't want to you next-connect:

function runMiddleware(req, res, fn) {
  return new Promise((resolve, reject) => {
    fn(req, res, err => err ? reject(err) : resolve(err))
  })
}

export default async function handler(req, res) {
await runMiddleware(req, res, postValidationRules())
await runMiddleware(req, res, validate);
await runMiddleware(req, res, postMail);
}

This is sort of like Approach #2.

Basically runMiddleware "promisfy" the callback function (which is what Express middleware is written as). The part err => err ? reject(err) : resolve(err) is actually the next function we see in Express. A middleware would call this with an error if any. So if we see that the function receives a value (error), we reject the promise.

@hoangvvo
Copy link

hoangvvo commented May 4, 2020

And obviously, remove server.ts and express after that and do next start / next. Create more inside the api folder based on your need.

Throwing in links here in case you need: https://nextjs.org/docs/api-routes/introduction

@Sowed
Copy link
Author

Sowed commented May 4, 2020

@hoangvvo, thank you for taking time to give some detailed feedback. I was hoping to attempt this without an extra dependency. Just migrate the custom server workflow to nextjs' api routes work flow.

So my first attempt was your approach with runMiddleware but got an issue with calling it with postValidationRules() rules, since this will return array of validation errors which is not callable by fn(req, res, err => err ? reject(err) : resolve(err))

const postValidationRules = (): ValidationChain[] => {
  return [check('email').isEmail(), check('message').escape()];
};

// Fails at
await runMiddleware(req, res, postValidationRules());

// Obviously equivalent to 
await runMiddleware(req, res, [check('email').isEmail(), check('message').escape()]);

// Failing at
 fn(req, res, err => err ? reject(err) : resolve(err))

From this express-validator docs , they pass an array of rules as a second argument to app.post. I have a huge array of rules and custom messages per rule so I extracted that out to postValidationRules, cutout here for brevity. My big pain point is how express-validator gets-away with passing just an array to the post handler. 🤯

const { check, validationResult } = require('express-validator');

app.post('/user', [check('username').isEmail()], (req, res) => {
  // Finds the validation errors in this request and wraps them in an object with handy functions
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }
// Do more stuff here...
});

I am wondering at how I can attach the rules array onto the req and validate it with validationResult(req)?

@hoangvvo
Copy link

hoangvvo commented May 4, 2020

Oh I see, I think postValidationRules returns an array of middleware. Well in that case I guess we have run it one by one.

Try this

const promises = postValidationRules().map(midd => runMiddleware(req, res, midd)) // Returns an array of promises of middleware executions
await Promise.all(promises)
await runMiddleware(req, res, validate);
await runMiddleware(req, res, postMail);
``

@Sowed
Copy link
Author

Sowed commented May 4, 2020

I have also tried the next-connect approach but still face the same type with the return type of ValidationChain[] not being compatible with RequestHandler.

handler.post(
  postValidationRules(), // This throws the above type error
  validate,
  postMail
);

@Sowed
Copy link
Author

Sowed commented May 4, 2020

const promises = postValidationRules().map(midd => runMiddleware(req, res, midd)) // Returns an array of promises of middleware executions
await Promise.all(promises)
  • That was the gold!!!

@hoangvvo, you are a "gentleman, a hero and a dude." 🤯 That worked like a charm.
Thank you very much.

@Sowed
Copy link
Author

Sowed commented May 5, 2020

@hoangvvo, I have spent some time trying to make the above approach fail on errors but it does not. The validation checks always return undefined regardless of errors in the form data or not.

// With runMiddleware helper
const runMiddleware = (
  req: NextApiRequest,
  res: NextApiResponse,
  nextFn: Function
): Promise<void> => {
  return new Promise((resolve, reject) => {
    nextFn(req, res, (err: Error) => (err ? reject(err) : resolve(err)));
    return resolve();
  });
};
// 1. Given a form request with a body of the form (postData)
{ phone: '1234X YZ', email: 'x yz@@.com' }

// 2. And validation rules of the format
const postValidationRules = (): ValidationChain[] => {
  return [
    check('phone').isNumeric().withMessage('The phone number is invalid'),
    check('email').isEmail().withMessage('The email address is invalid'),
  ];
};
const promises = postValidationRules().map(midd => runMiddleware(req, res, midd))
const resolvedPromises = await Promise.all(promises)
console.log({ promises, resolvedPromises});

// Expecting the rules running on postData to fail resolving to an array of errors but instead 
// resolve to an array of undefined.
{
  promises: [
    Promise { undefined },
    Promise { undefined },
  ],
  resolvedPromises: [ undefined, undefined ]
}

@hoangvvo
Copy link

hoangvvo commented May 5, 2020

const resolvedPromises = await Promise.all(promises).catch(e => console.log(e))
// log out that one error among the promises.

If you prefer to returns all the error in your resolvedPromises:

const promises = postValidationRules().map(midd => runMiddleware(req, res, midd).catch(e => e)) // Catch and return the error

const resolvedPromises = await Promise.all(promises) // this won't reject.

resolvedPromises should contain all the errors.

@Sowed
Copy link
Author

Sowed commented May 5, 2020

I have created a repro and you can test it out here.

I left some inline-comments based on your previous recommendations and removed the input fields for brevity, created two separate form data formats for comparison, expecting the invalid one to fail. However both pass and return the final should be sanitized and emailed data.

I am currently looking into running the validation in an imperative way using run()
but the approach is too repetitive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment