Entry Point
Where your application starts - server setup, graceful shutdown, and the startup sequence.
The Starting Line#
Every application needs a starting point - the file that runs when you type node src/index.js. This file is your entry point, and it has one critical job: start your application correctly.
Think of it like starting a car. You don't just turn the key - you check mirrors, fasten seatbelt, then start. Your server startup is similar: connect to databases, set up error handlers, then listen for requests.
The Simplest Entry Point#
// src/index.js
import express from 'express';
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
This works for learning, but production apps need more.
The Production Entry Point#
Here's what a real entry point looks like:
// src/index.js
import 'dotenv/config';
import { app } from './app.js';
import { connectDB } from './config/database.js';
import { connectRedis } from './config/redis.js';
import { logger } from './utils/logger.js';
import { config } from './config/index.js';
async function start() {
try {
// 1. Connect to databases
await connectDB();
logger.info('MongoDB connected');
await connectRedis();
logger.info('Redis connected');
// 2. Start HTTP server
const server = app.listen(config.port, () => {
logger.info(`Server running on port ${config.port}`);
});
// 3. Handle shutdown gracefully
setupGracefulShutdown(server);
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
start();
Let's break down each part.
The Startup Sequence#
Order matters. You can't handle requests if the database isn't connected:
async function start() {
// Step 1: Load environment variables (already done via import)
// Step 2: Connect to external services
await connectDB();
await connectRedis();
// Step 3: Only NOW start accepting requests
app.listen(config.port);
}
Never Skip This Order
If you start listening before databases connect, requests will fail. Users get errors. Your monitoring goes crazy. Always: connect first, listen second.
Separating App from Server#
Notice we import app from another file:
// src/app.js - Express configuration
import express from 'express';
const app = express();
app.use(express.json());
// ... routes, middleware ...
export { app };
// src/index.js - Server startup
import { app } from './app.js';
app.listen(3000);
Why separate them?
- Testing - You can import
appin tests without starting a server - Clarity -
app.jsis about Express config,index.jsis about startup - Flexibility - Same app, different startup (tests vs production vs serverless)
Graceful Shutdown#
When your server stops (deploy, crash, restart), you want it to:
- Stop accepting new requests
- Finish handling current requests
- Close database connections
- Then exit
Without graceful shutdown, requests get dropped mid-flight. Users see errors. Data gets corrupted.
function setupGracefulShutdown(server) {
// These signals tell your app to stop
const signals = ['SIGTERM', 'SIGINT'];
signals.forEach(signal => {
process.on(signal, async () => {
logger.info(`${signal} received, shutting down gracefully`);
// Stop accepting new connections
server.close(async () => {
logger.info('HTTP server closed');
// Close database connections
await mongoose.connection.close();
logger.info('MongoDB connection closed');
await redis.quit();
logger.info('Redis connection closed');
process.exit(0);
});
// Force exit after timeout
setTimeout(() => {
logger.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
});
});
}
When Do These Signals Fire?#
| Signal | When |
|---|---|
SIGTERM | Docker/Kubernetes stopping container |
SIGINT | You press Ctrl+C |
SIGQUIT | Terminal quit signal |
Error Handling at Startup#
Startup failures need special handling. If the database is down, should you:
- Keep retrying?
- Exit immediately?
- Start anyway and hope?
Usually: exit immediately. Let your orchestrator (Docker, Kubernetes, PM2) restart you.
async function start() {
try {
await connectDB();
app.listen(config.port);
} catch (error) {
logger.error('Startup failed:', error);
process.exit(1); // Non-zero = error
}
}
Unhandled Errors#
Catch errors that slip through:
// Unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection:', reason);
// Don't exit - let the error handler deal with it
});
// Uncaught exceptions - these are serious
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error);
process.exit(1); // Exit - app is in unknown state
});
Uncaught vs Unhandled
Uncaught Exception: Synchronous error with no try/catch. App is in unknown state - exit.
Unhandled Rejection: Promise rejected without .catch(). Usually recoverable - log and continue.
Complete Entry Point#
Here's everything together:
// src/index.js
import 'dotenv/config';
import mongoose from 'mongoose';
import { app } from './app.js';
import { connectDB } from './config/database.js';
import { redis, connectRedis } from './config/redis.js';
import { logger } from './utils/logger.js';
import { config } from './config/index.js';
async function start() {
try {
// Connect to services
await connectDB();
logger.info('MongoDB connected');
await connectRedis();
logger.info('Redis connected');
// Start server
const server = app.listen(config.port, () => {
logger.info(`Server running on port ${config.port} in ${config.env} mode`);
});
// Graceful shutdown
const shutdown = async (signal) => {
logger.info(`${signal} received, starting graceful shutdown`);
server.close(async () => {
logger.info('HTTP server closed');
try {
await mongoose.connection.close();
logger.info('MongoDB disconnected');
await redis.quit();
logger.info('Redis disconnected');
process.exit(0);
} catch (err) {
logger.error('Error during shutdown:', err);
process.exit(1);
}
});
// Force exit after 10 seconds
setTimeout(() => {
logger.error('Shutdown timeout, forcing exit');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
} catch (error) {
logger.error('Failed to start:', error);
process.exit(1);
}
}
// Global error handlers
process.on('unhandledRejection', (reason) => {
logger.error('Unhandled Rejection:', reason);
});
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error);
process.exit(1);
});
start();
The app.js File#
Your Express configuration lives here:
// src/app.js
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import routes from './routes/index.js';
import { errorHandler, notFoundHandler } from './middleware/error.js';
const app = express();
// Security
app.use(helmet());
app.use(cors());
// Parse JSON bodies
app.use(express.json());
// Routes
app.use('/api', routes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Error handling (must be last)
app.use(notFoundHandler);
app.use(errorHandler);
export { app };
Development vs Production#
Your entry point can behave differently based on environment:
// Development: more logging, auto-reload handled by --watch
// Production: less logging, graceful shutdown matters more
if (config.env === 'development') {
logger.level = 'debug';
}
if (config.env === 'production') {
// Production-only setup
app.set('trust proxy', 1); // Trust load balancer
}
Key Takeaways#
- Startup order matters - Connect to databases before listening for requests
- Separate app from server - Makes testing easier
- Graceful shutdown is essential - Don't drop requests mid-flight
- Handle startup failures - Exit fast, let orchestrators restart you
- Catch global errors - unhandledRejection and uncaughtException
The Pattern
Connect → Listen → Handle shutdown. This three-step pattern works for any Node.js server. Master it once, use it everywhere.
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.