Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ pnpm test

---

linked PR 5,4,6

## 📄 License

This project is licensed under the ISC License.
16 changes: 7 additions & 9 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import helmet from 'helmet';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
import { connectDatabase } from './config/database';
import logger from './config/logger';
import errorHandler from './middleware/errorHandler';
import requestLogger from './middleware/requestLogger';
import routes from './routes';
import env from './config/env';

const app = express();

Expand All @@ -16,15 +17,15 @@ app.use(helmet());
// CORS configuration
app.use(
cors({
origin: process.env.CORS_ORIGIN || '*',
origin: env.CORS_ORIGIN,
credentials: true,
}),
);

// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
windowMs: env.RATE_LIMIT_WINDOW_MS,
max: env.RATE_LIMIT_MAX_REQUESTS,
standardHeaders: true,
legacyHeaders: false,
});
Expand All @@ -37,11 +38,8 @@ app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Compression
app.use(compression());

// Logging middleware
app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`);
next();
});
// Request logging middleware
app.use(requestLogger);

// Health check endpoint
app.get('/health', (req, res) => {
Expand Down
35 changes: 35 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { z } from 'zod';
import dotenv from 'dotenv';

dotenv.config();

const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
MONGODB_URI: z.string().url(),
JWT_SECRET: z.string().min(16),
JWT_EXPIRES_IN: z.string().default('7d'),
BCRYPT_ROUNDS: z.coerce.number().int().min(8).max(31).default(10),
LOG_LEVEL: z.enum(['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly']).default('info'),
CORS_ORIGIN: z.string().url(),
RATE_LIMIT_WINDOW_MS: z.coerce.number().int().min(1000).default(900000),
RATE_LIMIT_MAX_REQUESTS: z.coerce.number().int().min(1).default(100),
});

let env: z.infer<typeof envSchema>;

try {
env = envSchema.parse(process.env);
} catch (error) {
if (error instanceof z.ZodError) {
console.error('❌ Invalid environment variables:');
error.issues.forEach((issue) => {
console.error(` - ${issue.path.join('.')}: ${issue.message}`);
});
} else {
console.error('❌ Failed to parse environment variables:', error);
}
process.exit(1);
}

export default env;
25 changes: 14 additions & 11 deletions src/config/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import winston from 'winston';
import env from './env';

const levels = {
error: 0,
Expand All @@ -8,12 +9,6 @@ const levels = {
debug: 4,
};

const level = (): string => {
const env = process.env.NODE_ENV || 'development';
const isDevelopment = env === 'development';
return process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'warn');
};

const colors = {
error: 'red',
warn: 'yellow',
Expand All @@ -24,25 +19,33 @@ const colors = {

winston.addColors(colors);

const format = winston.format.combine(
const devFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
winston.format.colorize({ all: true }),
winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`),
);

const prodFormat = winston.format.combine(winston.format.timestamp(), winston.format.json());

const transports = [
new winston.transports.Console(),
new winston.transports.Console({
format: env.NODE_ENV === 'development' ? devFormat : prodFormat,
}),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
format: prodFormat,
}),
new winston.transports.File({
filename: 'logs/all.log',
format: prodFormat,
}),
new winston.transports.File({ filename: 'logs/all.log' }),
];

const logger = winston.createLogger({
level: level(),
level: env.LOG_LEVEL,
levels,
format,
format: env.NODE_ENV === 'development' ? devFormat : prodFormat,
transports,
});

Expand Down
29 changes: 29 additions & 0 deletions src/middleware/requestLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Request, Response, NextFunction } from 'express';
import logger from '../config/logger';

const requestLogger = (req: Request, res: Response, next: NextFunction): void => {
const start = Date.now();

res.on('finish', () => {
const duration = Date.now() - start;
const logMessage = `${req.method} ${req.originalUrl} - ${res.statusCode} (${duration}ms)`;
const logData = {
method: req.method,
url: req.originalUrl,
statusCode: res.statusCode,
duration,
ip: req.ip,
userAgent: req.get('user-agent'),
};

if (res.statusCode >= 400) {
logger.warn(logMessage, logData);
} else {
logger.info(logMessage, logData);
}
});

next();
};

export default requestLogger;