What would be a good project structure for a Node.js application?
Small applications don't matter that much, but for larger apps with more complex logic, it's worth thinking about the project structure. A person diving fresh into the project should get quickly on their feet. Or, it can be you 6 months from now trying to wrap your head around what's going on in the codebase. Separating concerns is a great way to enforce structure, and it also provides useful qualities when thinking about testing.
One way to organize a project is to use three-layer architecture.
The project is structured into three layers: API, Service and Integration layers.
Each layer has a specific set of responsibilities that are clearly defined and easy to grasp. Each layer accesses the layer below it, never above it. Serving a request touches each layer starting from the top, traveling all the way down, and then resurfacing back to the topmost layer.
The API Layer is responsible for receiving the HTTP request and parsing the payload from it. This layer would then forward the payload removed of any HTTP-specific items to the following Service layer. Structural input validation, e.g. received string represents a number, is performed at this level and translating a result from the Service layer to HTTP status code and JSON response.
Express.js lives only on this level. You'd have your main app.js that set up the server and individual route files. Route files define validations and call service layers, leaving all req and res objects behind.
Services perform business logic. They validate inputs against business rules and call other services in the Service layer. If they need to talk to outside systems, they use the Integration layer to do that.
The code in the Integration Layer is responsible for performing I/O outside the process boundaries. It talks to databases and makes HTTP requests to 3rd party web APIs.
Most of the contents in this layer would be higher abstraction clients. For example, a client handling the communication to a web API would accept vanilla objects as arguments and would hide the complexity of formulating an HTTP request payload and making the call.
This division achieves separation of concerns. Each layer has a specific responsibility, and it translates well into a directory structure.
You could translate this architecture into a project structure by having a separate directory for each layer.