I'm building a Node.js backend using plain SQL (no ORM, just mysql2 wrapped in a small helper). However, as my project grows, my route handlers start looking messy — full of raw SQL strings embedded in JavaScript.
For example, this route fetches configuration data by joining two tables (function and permission):
router.get("/config", async (req, res) => {
const sql = `
SELECT
\`function\`.\`function_key\` AS \`key\`,
GROUP_CONCAT(DISTINCT \`permission\`.\`role_id\` ORDER BY \`permission\`.\`role_id\` ASC) AS permission
FROM \`function\`
LEFT JOIN \`permission\` ON \`function\`.\`function_id\` = \`permission\`.\`function_id\`
GROUP BY \`function\`.\`function_id\`
`;
const { err, rows } = await db.async.all(sql, []);
if (err) {
console.error(err);
return res.status(500).json({
code: 500,
msg: "Database query failed"
});
}
const config = rows.map(row => ({
key: row.key,
permission: row.permission
? row.permission.split(',').map(id => Number(id))
: []
}));
return res.status(200).json({
code: 200,
config
});
});
While this works fine, I feel the backend looks like a bunch of SQL statements glued together with JavaScript. I want to keep using plain SQL (no Sequelize, Prisma, etc.), but I also want my code to look structured, maintainable, and testable.
What are some best practices or architectural patterns to organize raw SQL queries in a Node.js project?