Giriş
Node.js, modern web uygulamalarının backend'i için en popüler teknolojilerden biri haline geldi. Bu rehberde, 2024 yılının en güncel teknikleri ve best practice'leri ile production-ready API'ler nasıl geliştirileceğini öğreneceğiz.
1. Proje Kurulumu ve Temel Yapı
💡 Modern Proje Yapısı
2024'te Node.js projelerinde MVC pattern ve clean architecture principles kullanmak industry standard haline geldi.
Package.json Kurulumu
{
"name": "nodejs-api-2024",
"version": "1.0.0",
"description": "Modern Node.js API with Express and MongoDB",
"main": "src/server.js",
"type": "module",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix"
},
"dependencies": {
"express": "^4.18.2",
"mongoose": "^8.0.3",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"express-rate-limit": "^7.1.5",
"dotenv": "^16.3.1",
"joi": "^17.11.0",
"winston": "^3.11.0"
},
"devDependencies": {
"nodemon": "^3.0.2",
"jest": "^29.7.0",
"supertest": "^6.3.3",
"eslint": "^8.56.0"
}
}
Proje Dizin Yapısı
src/
├── controllers/
│ ├── auth.controller.js
│ ├── user.controller.js
│ └── product.controller.js
├── middleware/
│ ├── auth.middleware.js
│ ├── error.middleware.js
│ └── validation.middleware.js
├── models/
│ ├── User.js
│ └── Product.js
├── routes/
│ ├── auth.routes.js
│ ├── user.routes.js
│ └── product.routes.js
├── services/
│ ├── auth.service.js
│ └── user.service.js
├── utils/
│ ├── logger.js
│ ├── database.js
│ └── response.js
├── config/
│ └── database.js
└── server.js
2. Express.js Server Kurulumu
Modern Server Konfigürasyonu
// src/server.js
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import dotenv from 'dotenv';
import connectDB from './config/database.js';
import errorHandler from './middleware/error.middleware.js';
import logger from './utils/logger.js';
// Routes
import authRoutes from './routes/auth.routes.js';
import userRoutes from './routes/user.routes.js';
import productRoutes from './routes/product.routes.js';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
// Database Connection
await connectDB();
// Security Middleware
app.use(helmet());
app.use(cors({
origin: process.env.FRONTEND_URL || 'https://efekrbs.github.io',
credentials: true
}));
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
// Body Parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
// Health Check
app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// Error Handler
app.use(errorHandler);
// 404 Handler
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: 'Route not found'
});
});
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
export default app;
⚡ Performance Tip
Rate limiting ve helmet middleware kullanarak API güvenliğini artırın. Production'da nginx veya cloudflare gibi reverse proxy'ler de kullanın.
3. MongoDB ve Mongoose Modelleri
Database Connection
// src/config/database.js
import mongoose from 'mongoose';
import logger from '../utils/logger.js';
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
logger.info(`MongoDB Connected: ${conn.connection.host}`);
// Connection events
mongoose.connection.on('error', (err) => {
logger.error('MongoDB connection error:', err);
});
mongoose.connection.on('disconnected', () => {
logger.warn('MongoDB disconnected');
});
process.on('SIGINT', async () => {
await mongoose.connection.close();
logger.info('MongoDB connection closed');
process.exit(0);
});
} catch (error) {
logger.error('Database connection failed:', error);
process.exit(1);
}
};
export default connectDB;
User Model with Advanced Features
// src/models/User.js
import mongoose from 'mongoose';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
maxlength: [50, 'Name cannot exceed 50 characters']
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
match: [
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
'Please enter a valid email'
]
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters'],
select: false
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
avatar: {
type: String,
default: null
},
isEmailVerified: {
type: Boolean,
default: false
},
refreshTokens: [{
token: String,
createdAt: {
type: Date,
default: Date.now,
expires: 604800 // 7 days
}
}],
lastLogin: {
type: Date,
default: null
},
loginAttempts: {
type: Number,
default: 0
},
lockUntil: Date
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Indexes
userSchema.index({ email: 1 });
userSchema.index({ createdAt: -1 });
// Virtual for account lock status
userSchema.virtual('isLocked').get(function() {
return !!(this.lockUntil && this.lockUntil > Date.now());
});
// Pre-save middleware to hash password
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword) {
if (!this.password) return false;
return await bcrypt.compare(candidatePassword, this.password);
};
// Generate JWT tokens
userSchema.methods.generateAccessToken = function() {
return jwt.sign(
{
id: this._id,
email: this.email,
role: this.role
},
process.env.JWT_ACCESS_SECRET,
{ expiresIn: process.env.JWT_ACCESS_EXPIRE || '15m' }
);
};
userSchema.methods.generateRefreshToken = function() {
return jwt.sign(
{ id: this._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRE || '7d' }
);
};
// Handle failed login attempts
userSchema.methods.incLoginAttempts = function() {
if (this.lockUntil && this.lockUntil < Date.now()) {
return this.updateOne({
$unset: { lockUntil: 1 },
$set: { loginAttempts: 1 }
});
}
const updates = { $inc: { loginAttempts: 1 } };
if (this.loginAttempts + 1 >= 5 && !this.isLocked) {
updates.$set = {
lockUntil: Date.now() + 2 * 60 * 60 * 1000 // 2 hours
};
}
return this.updateOne(updates);
};
export default mongoose.model('User', userSchema);
4. JWT Authentication System
🔐 Modern Authentication
2024'te access token + refresh token pattern kullanmak, güvenlik ve user experience açısından en iyi yaklaşım.
Authentication Controller
// src/controllers/auth.controller.js
import User from '../models/User.js';
import jwt from 'jsonwebtoken';
import { asyncHandler } from '../utils/asyncHandler.js';
import { ApiResponse } from '../utils/response.js';
import { ApiError } from '../utils/error.js';
export const register = asyncHandler(async (req, res) => {
const { name, email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
throw new ApiError(400, 'User already exists with this email');
}
// Create user
const user = await User.create({
name,
email,
password
});
// Generate tokens
const accessToken = user.generateAccessToken();
const refreshToken = user.generateRefreshToken();
// Save refresh token
user.refreshTokens.push({ token: refreshToken });
await user.save();
// Remove password from response
user.password = undefined;
res.status(201).json(
new ApiResponse(201, {
user,
accessToken,
refreshToken
}, 'User registered successfully')
);
});
export const login = asyncHandler(async (req, res) => {
const { email, password } = req.body;
// Find user with password
const user = await User.findOne({ email }).select('+password');
if (!user) {
throw new ApiError(401, 'Invalid credentials');
}
// Check if account is locked
if (user.isLocked) {
throw new ApiError(423, 'Account temporarily locked due to too many failed login attempts');
}
// Verify password
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
await user.incLoginAttempts();
throw new ApiError(401, 'Invalid credentials');
}
// Reset login attempts on successful login
if (user.loginAttempts > 0) {
await user.updateOne({
$unset: {
loginAttempts: 1,
lockUntil: 1
}
});
}
// Update last login
user.lastLogin = new Date();
// Generate tokens
const accessToken = user.generateAccessToken();
const refreshToken = user.generateRefreshToken();
// Save refresh token
user.refreshTokens.push({ token: refreshToken });
await user.save();
// Remove password from response
user.password = undefined;
res.json(
new ApiResponse(200, {
user,
accessToken,
refreshToken
}, 'Login successful')
);
});
export const refreshToken = asyncHandler(async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
throw new ApiError(401, 'Refresh token is required');
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
throw new ApiError(401, 'Invalid refresh token');
}
// Check if refresh token exists in user's tokens
const tokenExists = user.refreshTokens.some(
tokenObj => tokenObj.token === refreshToken
);
if (!tokenExists) {
throw new ApiError(401, 'Invalid refresh token');
}
// Generate new tokens
const newAccessToken = user.generateAccessToken();
const newRefreshToken = user.generateRefreshToken();
// Remove old refresh token and add new one
user.refreshTokens = user.refreshTokens.filter(
tokenObj => tokenObj.token !== refreshToken
);
user.refreshTokens.push({ token: newRefreshToken });
await user.save();
res.json(
new ApiResponse(200, {
accessToken: newAccessToken,
refreshToken: newRefreshToken
}, 'Token refreshed successfully')
);
} catch (error) {
throw new ApiError(401, 'Invalid refresh token');
}
});
export const logout = asyncHandler(async (req, res) => {
const { refreshToken } = req.body;
const user = req.user;
if (refreshToken) {
// Remove specific refresh token
user.refreshTokens = user.refreshTokens.filter(
tokenObj => tokenObj.token !== refreshToken
);
} else {
// Remove all refresh tokens (logout from all devices)
user.refreshTokens = [];
}
await user.save();
res.json(
new ApiResponse(200, null, 'Logout successful')
);
});
Authentication Middleware
// src/middleware/auth.middleware.js
import jwt from 'jsonwebtoken';
import User from '../models/User.js';
import { asyncHandler } from '../utils/asyncHandler.js';
import { ApiError } from '../utils/error.js';
export const authenticate = asyncHandler(async (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
throw new ApiError(401, 'Access token is required');
}
try {
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
throw new ApiError(401, 'Invalid token - user not found');
}
req.user = user;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new ApiError(401, 'Token expired');
}
throw new ApiError(401, 'Invalid token');
}
});
export const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
throw new ApiError(403, 'Access denied');
}
next();
};
};
5. Error Handling ve Validation
⚠️ Error Handling
Production'da asla sensitive bilgileri client'a göndermeyin. Centralized error handling kullanın.
Global Error Handler
// src/middleware/error.middleware.js
import logger from '../utils/logger.js';
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error
logger.error(err);
// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = 'Resource not found';
error = { statusCode: 404, message };
}
// Mongoose duplicate key
if (err.code === 11000) {
const message = 'Duplicate field value entered';
error = { statusCode: 400, message };
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message);
error = { statusCode: 400, message };
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
const message = 'Invalid token';
error = { statusCode: 401, message };
}
if (err.name === 'TokenExpiredError') {
const message = 'Token expired';
error = { statusCode: 401, message };
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
export default errorHandler;
Request Validation with Joi
// src/middleware/validation.middleware.js
import Joi from 'joi';
import { ApiError } from '../utils/error.js';
export const validate = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
const message = error.details.map(detail => detail.message).join(', ');
throw new ApiError(400, message);
}
next();
};
};
// Validation schemas
export const registerSchema = Joi.object({
name: Joi.string().trim().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
});
export const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
});
6. Logging ve Monitoring
Winston Logger Konfigürasyonu
// src/utils/logger.js
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'nodejs-api' },
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
export default logger;
7. Testing Strategy
API Testing with Jest ve Supertest
// tests/auth.test.js
import request from 'supertest';
import app from '../src/server.js';
import User from '../src/models/User.js';
describe('Auth Endpoints', () => {
beforeEach(async () => {
await User.deleteMany({});
});
describe('POST /api/auth/register', () => {
it('should register a new user', async () => {
const userData = {
name: 'Test User',
email: 'test@example.com',
password: 'password123'
};
const response = await request(app)
.post('/api/auth/register')
.send(userData)
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.user.email).toBe(userData.email);
expect(response.body.data.accessToken).toBeDefined();
expect(response.body.data.refreshToken).toBeDefined();
});
it('should not register user with invalid email', async () => {
const userData = {
name: 'Test User',
email: 'invalid-email',
password: 'password123'
};
const response = await request(app)
.post('/api/auth/register')
.send(userData)
.expect(400);
expect(response.body.success).toBe(false);
});
});
describe('POST /api/auth/login', () => {
beforeEach(async () => {
const user = new User({
name: 'Test User',
email: 'test@example.com',
password: 'password123'
});
await user.save();
});
it('should login with valid credentials', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.accessToken).toBeDefined();
});
it('should not login with invalid credentials', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
})
.expect(401);
expect(response.body.success).toBe(false);
});
});
});
8. Production Deployment
🚀 Production Tips
PM2 ile process management, nginx reverse proxy, SSL certificates ve environment variables ile güvenli deployment yapın.
PM2 Ecosystem File
// ecosystem.config.js
module.exports = {
apps: [{
name: 'nodejs-api',
script: 'src/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/pm2-error.log',
out_file: './logs/pm2-out.log',
log_file: './logs/pm2-combined.log',
time: true
}]
};
Docker Configuration
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY src/ ./src/
# Create logs directory
RUN mkdir -p logs
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# Change ownership
RUN chown -R nodejs:nodejs /app
USER nodejs
EXPOSE 3000
CMD ["node", "src/server.js"]
Sonuç
Bu rehberde modern Node.js API geliştirme sürecinin tüm aşamalarını ele aldık. 2024 yılının best practice'lerini kullanarak güvenli, ölçeklenebilir ve maintainable API'ler geliştirebilirsiniz.
🎯 Sonraki Adımlar
- GraphQL implementasyonu
- Microservices architecture
- Redis caching stratejileri
- WebSocket real-time features
- Advanced monitoring ve alerting
Başarılı projeler dilerim! Sorularınız için benimle iletişime geçebilirsiniz.