-
-
Save Sowed/246614abb27f71f668b6e9a314ae6acd to your computer and use it in GitHub Desktop.
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); | |
} | |
})(); |
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
@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)
?
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);
``
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
);
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.
@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 ]
}
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.
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.
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
:This is sort of like Approach #2.
Basically
runMiddleware
"promisfy" the callback function (which is what Express middleware is written as). The parterr => err ? reject(err) : resolve(err)
is actually thenext
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.