CSE 154 Code Quality Guide (Node.js/Express) {
NOTE: All JavaScript Code should lint. Check to see if your code lints by looking for a green check mark in your repo. The linter will automatically run and email you a report if your code does not lint. Please refer to the JS section of this Code Quality Guide for JS-specific language guidelines. This page is specific to writing Node.js and Express programs.
Table of Contents
- Project Directory Structure
- Overall API Design
- Handling HTTP Requests
- Sending HTTP Responses
- Asynchronous Programming
- File Processing
- Using Databases
- Comments
Project Directory Structure
-
1.1 All static forward facing “view” files (HTML/CSS/Client-side JS/any images) should be inside of a
public
folder. All other files (e.g. your Node.js/Express web service) should be at the root.example-node-project/ .gitignore APIDOC.md app.js node_modules/ ... package.json public/ img/ ... index.html index.js styles.css
- 1.2 You should always submit your assignment with a complete
package.json
file generated usingnpm init
. You should never push thenode_modules
directory in a Git project. Thenpm init
command should only be run after your project directory is as described above to ensure the default entry point is correct.
- 1.3 In your
package.json
file, make sure the value associated with the fieldname"main"
matches the filename of the file that has all your endpoints (e.g.app.js
).
Overall API Design
-
2.1 In Node, we have hundreds of thousands of modules we can use with
npm
. When choosing a module, prefer core modules over community modules unless there are motivating benefits of a community module (e.g.express
to greatly simplify thehttp-server
core module). When choosing modules and their functions, keep stability in mind. In Node.js Core Module documentation, most functions that are in deprecated or experimental stages will be labeled as so (e.g. the deprecatedfs.exists
from thefs
module, which should now never be used).// best - both modules are core to Node const fs = require('fs').promises; async function processFile() { ... let contents = await fs.readFile('file.txt', 'utf8'); ... }
// also great if we don't need async/await although for this class you should prefer the promise version unless instructed otherwise const fs = require('fs'); function processFile() { fs.readFile('file.txt', 'utf8', (err, contents) => { ... }); }
-
2.2 Define all of your modules at the top of the file, and only define those that you use in your program. Modules should be saved as
const
variables usingcamelCasing
naming conventions. Module global constants should still follow theUPPER_CASE
naming convention. For example, you should always define thePORT
constant at the bottom of your file usingconst PORT = process.env.PORT || 8000
(8000 is a common localhost port number convention, though you may also see 3000 or 8080). Additionally, you should includeapp.use(express.static('public'))
andapp.listen(PORT)
at the bottom of your file as well.// bad const EXPRESS = require('express'); const FS = require('fs').promises; app.use(express.static('public')); let port = process.env.PORT || 8000; app.listen(port);
// good const express = require('express'); const fs = require('fs').promises; // at the bottom of the file app.use(express.static('public')); const PORT = process.env.PORT || 8000; app.listen(PORT);
-
2.3 Use helper functions to keep your middleware functions short and to factor out shared code, and define any helper functions at the bottom of your file (below your request-handling functions), never in the middle of the program.
// bad app.get('/path/here', (req, res) => { ... }); function helperFunction() { ... } app.get('another/path/here', (req, res) => { ... });
// good app.get('/path/here', (req, res) => { ... }); app.get('another/path/here', (req, res) => { ... }); // bottom of the file function helperFunction() { ... }
-
2.3 Avoid passing the
req
orres
objects to helper functions. This will help reduce the possibility of sending a response multiple times or unintentially modifying the object.// bad app.get('/path/:name', (req, res) => { helperFunction(req) ... }); function helperFunction(req) { let name = req.params.name; ... }
// good app.get('/path/:name', (req, res) => { helperFunction(req.params.name) ... }); function helperFunction(name) { ... }
Handling HTTP Requests
-
3.1 You may use path parameters or query parameters in Express, but it is more common in Node APIs to use named path parameters. Query parameters are usually used for optional parameters (e.g. type=’text’, type=’json’, limit=10, etc.).
// required 'name' path parameter and optional 'limit' query parameter // valid api calls include both /path/myname and /path/myname?limit=3 app.get('/path/:name', (req, res) => { let name = req.params.name; let limit = DEFAULT_COUNT; if (req.query.limit) { limit = req.query.limit; } ... });
-
3.2 Always consider possible invalid requests, such as passing invalid parameters or not passing required query parameters. Responses should be sent with 400 (Bad Request) status codes. Do not attempt to handle missing path parameters, as the error is not possible in a callback function specific to a matched route for the path string. See Sending HTTP Responses for more details on sending errors.
// bad - 400 status code not set app.get('/path', (req, res) => { if (!req.query.category) { res.type("text").send("missing category parameter"); } else { ... } }); // bad - path parameters can't be missing since they are a part of the path app.get('/path/:category', (req, res) => { if (!req.params.category) { res.type("text").status(400).send("missing category parameter"); } else { ... } });
// good - query parameters have to be checked for since they are optional app.get('/path', (req, res) => { if (!req.query.category) { res.type("text").status(400).send("missing category parameter"); } else { ... } }); // good - path parameters could be invalid app.get('/path/:category', (req, res) => { let category = req.query.category; if (category !== "trivia" && category !== "jokes") { res.type("text").status(400).send("invalid category parameter"); } else { ... } });
Sending HTTP Responses
-
4.1 Make sure to send a response for every situation to avoid a hanging connection.
// bad - no response sent if name was not given app.get('/path', (req, res) => { if (req.query.name) { // send response based on given name } });
// good app.get('/path', (req, res) => { if (req.query.name) { // send response based on given name } else { // send error } });
-
4.2 Always set a header type before sending a response (including when sending an error response). Remember that the default is HTML, which is not the same as plain text.
// bad app.get('/path', (req, res) => { res.send('Hello world'); });
// good app.get('/path', (req, res) => { res.type('json'); res.send({ 'msg' : ['Hello', 'world'] }); }); // also good (res.json will automatically set the content type) app.get('/path', (req, res) => { res.json({ 'msg' : ['Hello', 'world'] }); }); // also good app.get('/path', (req, res) => { res.type('text'); res.send('Hello world'); });
- 4.3 Handle errors that are specific to invalid requests with the 400 status code with a descriptive error message (e.g. inputted filename was invalid). Remember to set both the type and status before sending. See Handling HTTP Requests for more details about sending errors for invalid parameters.
-
4.4 Handle errors that are specific to the server and independent of any client input (e.g. file-processing errors, database errors) by setting a 500 status code. It’s best to keep these status messages brief to avoid sharing internal details with clients, but to let them know that something went wrong on the server. Remember to set both the type and status before sending.
// good app.get('/path', (req, res) => { try { ... } catch(err) { res.type("text").status(500).send("server error"); } });
-
4.5 Never overwrite content header types, whether it is the same content type or a different content type.
// bad app.get('/path', (req, res) => { res.type('text'); let respType = req.query.type; if (respType && respType === 'json') { res.type('json'); // overwrites to a different type ... } else { ... } }); // also bad app.get('/path', (req, res) => { res.type('text'); let name = req.query.name; if (name) { res.type('text').send("Hello " + name ); // overwrites to the same type } else { ... } });
// good app.get('/:param', (req, res) => { let respType = req.query.type; if (respType) { if (respType === 'text') { res.type('text'); ... } else if (respType === 'json') { res.type('json'); ... } else { ... // handle invalid type parameter value } } ... });
-
4.6 After sending, the code continues to execute. Structure your code so the endpoint sends exactly one response in each situation. To achieve this, never
return res.send(...)
and never use emptyreturn
s - instead consider using anif-else
structure where applicable.// bad - code continues executing app.get('/path', (req, res) => { if (!req.query.category) { res.type("text").status(400).send("missing category parameter"); } ... // code will keep execute even after the error was sent }); // bad - returns while sending app.get('/path', (req, res) => { if (!req.query.category) { return res.type("text").status(400).send("missing category parameter"); } ... }); // bad - empty return app.get('/path', (req, res) => { if (!req.query.category) { res.type("text").status(400).send("missing category parameter"); return; } ... });
// good app.get('/path', (req, res) => { if (!req.query.category) { res.type("text").status(400).send("missing category parameter"); } else if (req.query.category !== "trivia" && req.query.category !== "jokes") { res.type("text").status(400).send("invalid category parameter"); } else { ... } });
Asynchronous Programming
- 5.1 Avoid nested “callback pyramids”, preferring
async
/await
to simplify asynchronous code.
- 5.2 Do not mark a function as
async
unless you need to useawait
(e.g. for asynchronous file i/o)
-
5.3 Always use a
try
/catch
block when handling errors withasync
/await
.// bad app.get('/dogs/:breed', async (req, res) => { let breedContents = await fs.readFile('breeds.txt', 'utf8'); // do stuff with req.params.breed and breedContents });
// good app.get('/dogs/:breed', async (req, res) => { try { let breedContents = await fs.readFile('breeds.txt', 'utf8'); // do stuff with req.params.breed and breedContents } catch (err) { res.type('text'); res.status(500).send('Something went wrong on the server. Please try again later.'); } });
File Processing
-
6.1 Only read/write files when necessary to limit computation on the server.
// bad - file contents may not be used if there is an invalid parameter app.get('/path/:category', async (req, res) => { try { let category = req.query.category; let contents = await fs.readFile('file.txt', 'utf8'); if (category !== "trivia" && category !== "jokes") { // handle invalid parameter error } else { // do stuff with contents } } catch (err) { // handle error } });
// good - file contents will definitely be used once obtained app.get('/path/:category', async (req, res) => { try { let category = req.query.category; if (category !== "trivia" && category !== "jokes") { // handle invalid parameter error } else { let contents = await fs.readFile('file.txt', 'utf8'); // do stuff with contents } } catch (err) { // handle error } });
-
6.2 Never use synchronous versions (e.g.
readFileSync
) offs
module functions.// bad let contents = fs.readFileSync('file.txt', 'utf8'); // do stuff with contents // bad fs.readFile('file.txt', 'utf8', (err, contents) => { // do stuff with contents });
// good let contents = await fs.readFile('file.txt', 'utf8'); // do stuff with contents
-
6.3 Always handle possible errors (e.g. with
try
/catch
andasync
/await
).try { let contents = await fs.readFile('file.txt', 'utf8'); // do stuff with contents } catch (err) { // handle error if something goes wrong }
Using Databases
-
7.1 To avoid the possibility of SQL injection, always use placeholders for any client input used within SQL statments.
// bad db.all("SELECT * FROM table WHERE name = " + nameParam)
// good db.all("SELECT * FROM table WHERE name = ?", nameParam)
- 7.2 Any database connection that is opened should be closed before a response is sent.
let db = await getDBConnection(); let rows = db.all("SELECT * FROM table WHERE name = " + nameParam); await db.close(); // close db after using it res.json(rows);
-
7.3 Always handle possible errors (e.g. with
try
/catch
).try { // connect to database and run queries } catch (err) { // handle error if something goes wrong }
Comments
-
8.1 Document any request-handling functions (e.g. app.get, app.post, etc.) with comments describing the endpoint.
// Returns the names of dogs that are the given breed app.get('/dogs/:breed', async (req, res) => { ... });
- 8.2 Document any other helper functions using the same JSDoc requirements as your client-side JS file. Visit the “Comments” section of the “JavaScript” tab for more details.
- 8.3 Include a descriptive public documentation of your API (e.g in an APIDOC.md file). For each endpoint, include the following information: Request Format, Request Type, Returned Data Format, Description, Example Request, Example Response, and Error Handling.