Table of Contents
Building a scalable and maintainable Node.js application using Express and TypeScript requires thoughtful structuring. Proper organization enhances code readability, facilitates testing, and simplifies future development efforts. This article explores best practices for structuring such applications effectively.
Project Directory Structure
A clear directory structure is fundamental. Consider organizing your project as follows:
- src/: Contains all source code.
- src/controllers/: Handles request logic.
- src/routes/: Defines API routes.
- src/middlewares/: Custom middleware functions.
- src/services/: Business logic and external integrations.
- src/models/: Data models and interfaces.
- src/utils/: Utility functions.
- tests/: Test cases and test setup.
TypeScript Configuration
Configure TypeScript for strict type checking and better development experience. Use tsconfig.json with recommended settings:
tsconfig.json
{ "compilerOptions": { "target": "ES6", "module": "CommonJS", "outDir": "./dist", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }
Express Application Setup
Initialize your Express app with TypeScript. Create an app.ts file to set up middleware, routes, and error handling.
Example:
import express from 'express';
const app = express();
app.use(express.json());
app.use('/api', require('./routes/api'));
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
export default app;
Routing and Controllers
Separate route definitions from controller logic. This promotes modularity and testability.
Example route (routes/api.ts):
import express from 'express';
import { getUsers } from '../controllers/userController';
const router = express.Router();
router.get('/users', getUsers);
export default router;
Controller example (controllers/userController.ts):
import { Request, Response } from 'express';
export const getUsers = (req: Request, res: Response) => {
res.json({ message: 'List of users' });
};
Middleware and Error Handling
Implement middleware for authentication, logging, and validation. Use centralized error handling for consistency.
Example middleware:
import { Request, Response, NextFunction } from 'express';
export const logger = (req: Request, res: Response, next: NextFunction) => {
console.log(`${req.method} ${req.url}`);
next();
};
Testing and Development
Use testing frameworks like Jest or Mocha to write unit and integration tests. Maintain a separate tests directory.
Sample test file (tests/user.test.ts):
import request from 'supertest';
import app from '../src/app';
describe('GET /api/users', () => {
it('should return list of users', async () => {
const res = await request(app).get('/api/users');
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('message');
});
});
Conclusion
Organizing your Node.js application with Express and TypeScript following these best practices improves maintainability and scalability. Modular structure, strict typing, and clear separation of concerns are key to building robust backend services.