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?