Node.js API Geliştirme Rehberi 2024

Modern Node.js teknolojileri kullanarak güçlü ve ölçeklenebilir RESTful API'ler nasıl geliştirilir? Express.js, MongoDB, JWT authentication ve production-ready best practices ile kapsamlı rehber.

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.