Express Configuration
When building a web application with Express.js, proper configuration is crucial for making your application maintainable, secure, and scalable. This guide will walk you through various aspects of Express configuration, from basic setup to environment-specific configurations.
Introduction to Express Configuration
Configuration in Express involves setting up various aspects of your application:
- Server settings (port, host)
- Middleware configuration
- Environment variables
- Application behavior across different environments (development, production, testing)
Proper configuration not only helps keep your code organized but also makes your application more secure and easier to deploy across different environments.
Basic Express Configuration
Let's start with a simple Express server configuration:
const express = require('express');
const app = express();
// Basic configuration
const PORT = 3000;
// Start the server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
While this works for simple applications, real-world applications need more robust configuration strategies.
Configuration Using Environment Variables
Environment variables allow your application to behave differently based on where it's running. This is a key practice for production-ready applications.
Using the dotenv Package
First, install the dotenv
package:
npm install dotenv
Create a .env
file in your project root:
PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=mysupersecretkey
Now, modify your Express app to use these variables:
const express = require('express');
// Load environment variables early in your application
require('dotenv').config();
const app = express();
// Use environment variables with fallbacks
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';
app.listen(PORT, () => {
console.log(`Server running in ${NODE_ENV} mode on port ${PORT}`);
});
Never commit your .env
file to version control. Add it to .gitignore
to keep sensitive information secure.
Configuration Object Pattern
For more complex applications, using a dedicated configuration object helps organize settings:
// config.js
const path = require('path');
require('dotenv').config();
const config = {
env: process.env.NODE_ENV || 'development',
port: process.env.PORT || 3000,
db: {
uri: process.env.DATABASE_URL || 'mongodb://localhost:27017/myapp',
},
jwtSecret: process.env.JWT_SECRET || 'devSecret',
staticFolder: path.join(__dirname, 'public'),
sessionCookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
};
module.exports = config;
Then, in your main application file:
const express = require('express');
const config = require('./config');
const app = express();
// Use configuration values throughout your app
app.listen(config.port, () => {
console.log(`Server running in ${config.env} mode on port ${config.port}`);
});
Environment-Specific Configuration
Different environments often require different configurations. Here's how to manage them:
// config/index.js
const development = require('./env/development');
const production = require('./production');
const testing = require('./testing');
const defaultConfig = {
port: 3000,
staticFolder: 'public',
logLevel: 'info'
};
const environment = process.env.NODE_ENV || 'development';
let environmentConfig = {};
switch(environment) {
case 'development':
environmentConfig = development;
break;
case 'production':
environmentConfig = production;
break;
case 'testing':
environmentConfig = testing;
break;
default:
environmentConfig = development;
}
// Merge default config with environment-specific config
const finalConfig = { ...defaultConfig, ...environmentConfig };
module.exports = finalConfig;
With separate files for each environment:
// config/env/development.js
module.exports = {
db: 'mongodb://localhost:27017/myapp_dev',
logLevel: 'debug',
sessionSecret: 'dev-session-secret',
corsOptions: {
origin: 'http://localhost:3000'
}
};
// config/env/production.js
module.exports = {
db: process.env.DATABASE_URL,
logLevel: 'error',
sessionSecret: process.env.SESSION_SECRET,
corsOptions: {
origin: process.env.CLIENT_ORIGIN,
credentials: true
}
};
Configuring Express Middleware
Proper middleware configuration is essential for Express applications. Here's how to configure some common middleware:
Body Parser
const express = require('express');
const app = express();
const config = require('./config');
// Configure body parser
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
CORS Configuration
const express = require('express');
const cors = require('cors');
const app = express();
const config = require('./config');
// Configure CORS middleware based on environment
app.use(cors(config.corsOptions));
Static Files
const express = require('express');
const path = require('path');
const app = express();
const config = require('./config');
// Serve static files
app.use(express.static(path.join(__dirname, config.staticFolder)));
Security Middleware
const express = require('express');
const helmet = require('helmet');
const app = express();
// Add security headers
app.use(helmet());
// Configure Content Security Policy if needed
if (process.env.NODE_ENV === 'production') {
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", 'trusted-cdn.com'],
styleSrc: ["'self'", "'unsafe-inline'", 'trusted-cdn.com'],
imgSrc: ["'self'", 'data:', 'trusted-cdn.com'],
connectSrc: ["'self'", 'api.example.com']
}
}));
}
Real-World Example: Complete Express App Configuration
Let's put everything together in a complete Express application with proper configuration:
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const path = require('path');
const config = require('./config');
// Initialize express app
const app = express();
// Security middleware
app.use(helmet());
// CORS configuration
app.use(cors(config.corsOptions));
// Request body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Logging configuration
if (config.env !== 'test') {
app.use(morgan(config.env === 'development' ? 'dev' : 'combined'));
}
// Static files
app.use(express.static(path.join(__dirname, config.staticFolder)));
// Set application-wide variables
app.set('port', config.port);
app.set('trust proxy', config.trustProxy);
// Error handling for development vs production
if (config.env === 'development') {
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: err.message,
stack: err.stack
});
});
} else {
app.use((err, req, res, next) => {
// Log error to monitoring service in production
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
}
// Routes (would typically be imported from separate files)
app.get('/', (req, res) => {
res.send(`Hello from ${config.env} environment!`);
});
// Start server
app.listen(config.port, () => {
console.log(`Server running in ${config.env} mode on port ${config.port}`);
});
module.exports = app; // Export for testing
Best Practices for Express Configuration
- Never hardcode sensitive information - Always use environment variables for secrets, API keys, etc.
- Use configuration hierarchy - Default values → Environment file → Command line arguments
- Keep environment-specific config separate - Separate development, production, and test configs
- Validate configurations on startup - Check if all required configs are present
- Document your configuration - Include comments explaining each configuration option
- Use sensible defaults - Make your app work reasonably well without extensive configuration
Configuration Validation
For critical applications, validate your configuration on startup:
const Joi = require('joi');
const config = require('./config');
// Define validation schema
const schema = Joi.object({
port: Joi.number().default(3000),
env: Joi.string().valid('development', 'production', 'test').default('development'),
db: Joi.object({
uri: Joi.string().required(),
options: Joi.object()
}).required(),
jwtSecret: Joi.string().min(8).required()
}).unknown();
// Validate config
const { error, value } = schema.validate(config);
if (error) {
console.error('Configuration validation error:', error.message);
process.exit(1);
}
// Continue with the validated config
console.log('Configuration validated successfully');
Summary
Proper Express configuration is fundamental to building robust, secure, and maintainable Node.js applications. In this guide, we've covered:
- Basic Express configuration setup
- Using environment variables with dotenv
- Creating configuration objects and hierarchies
- Environment-specific configurations
- Configuring common middleware
- Best practices for secure and maintainable configuration
- Configuration validation techniques
By implementing these practices, you'll have a solid foundation for your Express applications that can easily scale and adapt to different environments.
Additional Resources
- Express.js Official Documentation
- dotenv Documentation
- 12-Factor App Methodology - See especially the Config section
- Node.js Best Practices
Exercises
- Create a basic Express application that reads configuration from a
.env
file and displays different messages based on the environment. - Implement environment-specific configuration for development, testing, and production environments.
- Create a configuration validation system that checks required values on startup.
- Extend the configuration to include database settings and implement a connection function that uses these settings.
- Implement proper error handling middleware based on the current environment.
If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)