--- description: This rule provides comprehensive guidance on best practices for developing robust, maintainable, and performant Express.js applications, covering aspects from code organization and security to testing and deployment. globs: *.js --- - # Express.js Best Practices This document outlines best practices for developing Express.js applications to ensure code quality, maintainability, performance, and security. ## 1. Code Organization and Structure - ### Directory Structure Best Practices - **Modular Structure:** Organize your application into logical modules based on functionality (e.g., `controllers`, `models`, `routes`, `middleware`, `services`). - **Configuration:** Separate configuration files for different environments (development, production, testing). - **Public Assets:** Keep static assets (CSS, JavaScript, images) in a dedicated `public` directory. - **Views:** Store template files in a `views` directory. Use a template engine like EJS or Pug. - **Example Structure:** my-express-app/ ├── controllers/ │ ├── userController.js │ └── productController.js ├── models/ │ ├── user.js │ └── product.js ├── routes/ │ ├── userRoutes.js │ └── productRoutes.js ├── middleware/ │ ├── authMiddleware.js │ └── errorMiddleware.js ├── services/ │ ├── userService.js │ └── productService.js ├── config/ │ ├── config.js │ └── db.js ├── views/ │ ├── index.ejs │ └── user.ejs ├── public/ │ ├── css/ │ │ └── style.css │ ├── js/ │ │ └── script.js │ └── images/ ├── app.js # Main application file ├── package.json └── .env # Environment variables - ### File Naming Conventions - **Descriptive Names:** Use clear and descriptive names for files and directories. - **Case Convention:** Use camelCase for JavaScript files and directories. For components consider PascalCase. - **Route Files:** Name route files according to the resource they handle (e.g., `userRoutes.js`, `productRoutes.js`). - **Controller Files:** Name controller files according to the resource they handle (e.g., `userController.js`, `productController.js`). - **Model Files:** Name model files after the data model they represent (e.g., `user.js`, `product.js`). - ### Module Organization - **ES Modules:** Use ES modules (`import`/`export`) for modularity. - **Single Responsibility Principle:** Each module should have a single, well-defined responsibility. - **Loose Coupling:** Minimize dependencies between modules to improve reusability and testability. - ### Component Architecture - **Reusable Components:** Break down the application into reusable components (e.g., UI components, service components). - **Separation of Concerns:** Separate presentation logic (views) from business logic (controllers/services). - **Component Composition:** Compose complex components from simpler ones. - ### Code Splitting Strategies - **Route-Based Splitting:** Split code based on application routes to reduce initial load time. Use dynamic imports (`import()`) to load modules on demand. - **Component-Based Splitting:** Split code based on components, loading components only when they are needed. ## 2. Common Patterns and Anti-patterns - ### Design Patterns Specific to Express - **Middleware Pattern:** Use middleware functions to handle request processing, authentication, logging, etc. - **MVC Pattern:** Implement the Model-View-Controller (MVC) pattern to separate concerns and improve code organization. - **Observer Pattern:** Implement the observer pattern when you need to notify multiple objects about state changes. - ### Recommended Approaches for Common Tasks - **Route Handling:** Use Express Router to define routes in separate modules. - **Error Handling:** Use custom error handling middleware to catch and handle errors gracefully. See section 6 for more details. - **Data Validation:** Use middleware for validating request data before processing it. - **Asynchronous Operations:** Use `async/await` or Promises to handle asynchronous operations. - ### Anti-patterns and Code Smells to Avoid - **God Object:** Avoid creating a single, massive object that handles too many responsibilities. - **Callback Hell:** Avoid deeply nested callbacks; use Promises or `async/await` instead. - **Ignoring Errors:** Always handle errors properly instead of ignoring them. - **Global Variables:** Minimize the use of global variables to avoid naming conflicts and unexpected behavior. - **Hardcoding Secrets:** Never hardcode sensitive information (API keys, passwords) in your code. Use environment variables instead. - ### State Management Best Practices - **Stateless Controllers:** Keep controllers stateless to improve scalability and testability. - **Session Management:** Use session management middleware (e.g., `express-session`) to manage user sessions. - **Caching:** Implement caching strategies (e.g., Redis, Memcached) to improve performance. - ### Error Handling Patterns - **Centralized Error Handling:** Create a custom error handling middleware to catch and handle errors from different parts of the application. - **Error Logging:** Log errors to a file or a monitoring service for debugging and analysis. - **Custom Error Objects:** Create custom error objects with specific error codes and messages. - **Graceful Error Messages:** Return user-friendly error messages instead of exposing internal errors. - **Example Error Handler Middleware:** javascript // middleware/errorMiddleware.js const errorHandler = (err, req, res, next) => { console.error(err.stack); const statusCode = res.statusCode === 200 ? 500 : res.statusCode; res.status(statusCode); res.json({ message: err.message, stack: process.env.NODE_ENV === 'production' ? null : err.stack, }); }; module.exports = errorHandler; ## 3. Performance Considerations - ### Optimization Techniques - **Gzip Compression:** Use Gzip compression to reduce the size of responses. - **Caching:** Implement caching at different levels (e.g., browser caching, server-side caching) to reduce server load. - **Connection Pooling:** Use connection pooling for database connections to improve performance. - **Load Balancing:** Distribute traffic across multiple servers using a load balancer. - ### Memory Management - **Avoid Memory Leaks:** Be mindful of memory leaks, especially when working with large datasets or long-running processes. Use tools like `memwatch` to profile. - **Garbage Collection:** Understand how garbage collection works in Node.js and optimize your code accordingly. - **Streams:** Use streams for handling large files or data streams to avoid loading the entire data into memory. - ### Rendering Optimization - **Template Caching:** Enable template caching in your template engine to improve rendering performance. - **Minify Assets:** Minify CSS and JavaScript files to reduce their size. - **Lazy Loading Images:** Lazy load images to improve initial page load time. - ### Bundle Size Optimization - **Tree Shaking:** Use tree shaking to remove unused code from your bundles. - **Code Splitting:** Split your code into smaller chunks to reduce the size of initial bundles. - **Dependency Analysis:** Analyze your dependencies to identify and remove unnecessary packages. - ### Lazy Loading Strategies - **Lazy Load Modules:** Load modules only when they are needed using dynamic imports (`import()`). - **Lazy Load Images and Other Assets:** Use lazy loading for images and other assets that are not immediately visible on the page. ## 4. Security Best Practices - ### Common Vulnerabilities and How to Prevent Them - **Cross-Site Scripting (XSS):** Prevent XSS attacks by sanitizing user input and encoding output. - **Cross-Site Request Forgery (CSRF):** Protect against CSRF attacks by using CSRF tokens. - **SQL Injection:** Use parameterized queries or an ORM to prevent SQL injection attacks. - **NoSQL Injection:** Sanitize user input and avoid constructing queries from strings to prevent NoSQL injection attacks. - **Command Injection:** Avoid executing shell commands based on user input. If necessary, sanitize the input and use appropriate escaping. - **Denial of Service (DoS):** Implement rate limiting and other measures to prevent DoS attacks. - **Man-in-the-Middle (MitM):** Use HTTPS to encrypt communication between the client and server and protect against MitM attacks. - **HTTP Parameter Pollution (HPP):** Avoid using the same parameter multiple times in a request to prevent HPP attacks. - ### Input Validation - **Server-Side Validation:** Always validate user input on the server-side, even if you have client-side validation. - **Data Sanitization:** Sanitize user input to remove potentially harmful characters or code. - **Schema Validation:** Use schema validation libraries (e.g., Joi, express-validator) to validate request data against a predefined schema. - ### Authentication and Authorization Patterns - **Authentication:** Use a secure authentication mechanism (e.g., JWT, OAuth) to verify user identities. - **Authorization:** Implement role-based access control (RBAC) or attribute-based access control (ABAC) to control access to resources. - **Secure Password Storage:** Use a strong hashing algorithm (e.g., bcrypt) to store user passwords securely. - **Multi-Factor Authentication (MFA):** Implement MFA to add an extra layer of security. - ### Data Protection Strategies - **Encryption:** Encrypt sensitive data at rest and in transit. - **Data Masking:** Mask sensitive data in logs and other outputs. - **Access Control:** Restrict access to sensitive data to authorized users and processes. - ### Secure API Communication - **HTTPS:** Use HTTPS for all API communication. - **API Keys:** Use API keys to authenticate clients. - **Rate Limiting:** Implement rate limiting to prevent abuse and DoS attacks. - **Input Validation:** Validate all input data to prevent injection attacks. - **Output Encoding:** Encode all output data to prevent XSS attacks. ## 5. Testing Approaches - ### Unit Testing Strategies - **Test-Driven Development (TDD):** Write unit tests before writing the actual code. - **Test Individual Modules:** Test individual modules in isolation to ensure they work correctly. - **Mock Dependencies:** Mock external dependencies (e.g., databases, APIs) to isolate the module being tested. - ### Integration Testing - **Test Interactions Between Modules:** Test the interactions between different modules to ensure they work together correctly. - **Test API Endpoints:** Test API endpoints to ensure they return the expected results. - ### End-to-End Testing - **Test the Entire Application Flow:** Test the entire application flow from start to finish. - **Simulate User Interactions:** Simulate user interactions to ensure the application behaves as expected. - ### Test Organization - **Separate Test Files:** Create separate test files for each module or component. - **Descriptive Test Names:** Use clear and descriptive names for test cases. - **Arrange-Act-Assert Pattern:** Follow the Arrange-Act-Assert pattern in your tests. - ### Mocking and Stubbing - **Use Mocking Libraries:** Use mocking libraries (e.g., Jest, Sinon) to create mocks and stubs. - **Mock External Dependencies:** Mock external dependencies to isolate the module being tested. - **Stub Function Calls:** Stub function calls to control the behavior of dependencies. ## 6. Common Pitfalls and Gotchas - ### Frequent Mistakes Developers Make - **Not Handling Errors Properly:** Always handle errors properly to prevent unexpected behavior. - **Ignoring Security Vulnerabilities:** Be aware of common security vulnerabilities and take steps to prevent them. - **Not Using Middleware Wisely:** Use middleware wisely to handle common tasks such as authentication, logging, and error handling. - **Over-Engineering:** Avoid over-engineering your code by keeping it simple and focused. - ### Edge Cases to Be Aware Of - **Handling Large File Uploads:** Use streams or middleware libraries for handling large file uploads to prevent memory issues. - **Dealing with Timezones:** Be aware of timezone issues when working with dates and times. - **Handling Unicode Characters:** Properly handle Unicode characters to prevent encoding issues. - **Dealing with Concurrent Requests:** Implement concurrency control mechanisms to handle concurrent requests safely. - ### Version-Specific Issues - **Deprecated Features:** Be aware of deprecated features and use the recommended alternatives. - **Breaking Changes:** Be aware of breaking changes when upgrading to a new version of Express.js. - ### Compatibility Concerns - **Browser Compatibility:** Test your application in different browsers to ensure it works correctly. - **Operating System Compatibility:** Test your application on different operating systems to ensure it works correctly. - **Node.js Version Compatibility:** Ensure your application is compatible with the supported versions of Node.js. - ### Debugging Strategies - **Use Debugging Tools:** Use debugging tools (e.g., Node.js Inspector, Chrome DevTools) to debug your code. - **Log Statements:** Use log statements to track the flow of execution and identify issues. - **Error Messages:** Read error messages carefully to understand the cause of the error. ## 7. Tooling and Environment - ### Recommended Development Tools - **IDE:** Use a good IDE such as VS Code, WebStorm, or Sublime Text. - **Debugger:** Use a debugger to step through your code and identify issues. - **Linter:** Use a linter (e.g., ESLint) to enforce coding standards. - **Formatter:** Use a formatter (e.g., Prettier) to format your code automatically. - ### Build Configuration - **Use a Build Tool:** Use a build tool (e.g., Webpack, Parcel) to bundle and optimize your code. - **Configure Build Scripts:** Configure build scripts in your `package.json` file to automate the build process. - **Use Environment Variables:** Use environment variables to configure your application for different environments. - ### Linting and Formatting - **Use ESLint:** Use ESLint to enforce coding standards and identify potential issues. - **Use Prettier:** Use Prettier to format your code automatically. - **Configure Editor Integration:** Configure your editor to automatically run ESLint and Prettier on save. - ### Deployment Best Practices - **Use a Process Manager:** Use a process manager (e.g., PM2, Forever) to keep your application running in production. - **Use a Reverse Proxy:** Use a reverse proxy (e.g., Nginx, Apache) to handle incoming requests and forward them to your application. - **Use a Load Balancer:** Use a load balancer to distribute traffic across multiple servers. - **Use HTTPS:** Use HTTPS to encrypt communication between the client and server. - **Monitor Your Application:** Monitor your application to identify and resolve issues. - ### CI/CD Integration - **Use a CI/CD Pipeline:** Use a CI/CD pipeline (e.g., Jenkins, Travis CI, CircleCI, GitHub Actions) to automate the build, test, and deployment process. - **Run Tests Automatically:** Configure your CI/CD pipeline to run tests automatically on every commit. - **Automate Deployment:** Automate the deployment process to reduce the risk of errors.