Skip to content
IRC-Coding IRC-Coding
REST API Fundamentals HTTP Methods Status Codes HATEOAS API Design Best Practices

REST API Fundamentals: HTTP Methods & Best Practices

Master REST API development with HTTP methods, status codes, HATEOAS, and best practices. Complete guide to API design and authentication.

S

schutzgeist

2 min read
REST API Fundamentals: HTTP Methods & Best Practices

REST API Development Fundamentals: HTTP Methods, Status Codes, HATEOAS & Best Practices

This article is a comprehensive introduction to REST API development fundamentals – including HTTP methods, status codes, HATEOAS, and best practices with practical examples.

In a Nutshell

REST (Representational State Transfer) is an architectural style for developing scalable web APIs that is based on HTTP protocol principles.

Compact Technical Description

REST API is a web API that follows REST architectural principles and provides stateless communication between client and server via HTTP methods, status codes, and HATEOAS.

Core Components:

HTTP Methods

  • GET: Retrieve resources (safe, idempotent)
  • POST: Create new resources (not safe, not idempotent)
  • PUT: Update/replace resources (not safe, idempotent)
  • PATCH: Partially update resources (not safe, not idempotent)
  • DELETE: Delete resources (not safe, idempotent)

HTTP Status Codes

  • 2xx Success: 200 OK, 201 Created, 204 No Content
  • 3xx Redirection: 301 Moved Permanently, 302 Found, 304 Not Modified
  • 4xx Client Error: 400 Bad Request, 401 Unauthorized, 404 Not Found
  • 5xx Server Error: 500 Internal Server Error, 502 Bad Gateway

HATEOAS (Hypermedia as the Engine of Application State)

  • Hypermedia Controls: Links for navigation
  • Self-Describing Messages: Resources describe themselves
  • State Transitions: Possible actions are provided
  • Discoverability: API is discoverable

API Design Principles

  • Resource-Oriented: URLs represent resources
  • Stateless: No client-side state
  • Cacheable: Responses can be cached
  • Uniform Interface: Consistent interface

Exam-Relevant Key Points

  • REST: Representational State Transfer, architectural style
  • HTTP Methods: GET, POST, PUT, PATCH, DELETE with semantics
  • Status Codes: 2xx Success, 3xx Redirection, 4xx Client Error, 5xx Server Error
  • HATEOAS: Hypermedia as the Engine of Application State
  • Resource-Oriented Design: URLs as resource identifiers
  • Stateless: No state on server side
  • Idempotency: Repeated calls have the same effect
  • API Versioning: Strategies for API versioning
  • Chamber of Commerce Relevant: Modern web API development and design

Core Components

  1. Resource Design: URL structure, naming conventions
  2. HTTP Methods: Semantically correct method usage
  3. Status Codes: Meaningful HTTP status codes
  4. Data Formats: JSON, XML, content negotiation
  5. Authentication: OAuth2, JWT, API keys
  6. Error Handling: Consistent error messages
  7. Documentation: OpenAPI/Swagger, API docs
  8. Testing: Unit tests, integration tests

Practical Examples

1. REST API with Node.js and Express

// server.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const { body, param, query, validationResult } = require('express-validator');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const mongoose = require('mongoose');
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

// Initialize Express app
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(helmet()); // Security headers
app.use(cors()); // Cross-origin resource sharing
app.use(morgan('combined')); // HTTP request logging
app.use(express.json({ limit: '10mb' })); // JSON parsing
app.use(express.urlencoded({ extended: true }));

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: {
    error: 'Too many requests from this IP, please try again later.',
    code: 'RATE_LIMIT_EXCEEDED'
  }
});
app.use('/api/', limiter);

// MongoDB Models
const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  firstName: String,
  lastName: String,
  role: { type: String, enum: ['user', 'admin'], default: 'user' },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const postSchema = new mongoose.Schema({
  title: { type: String, required: true },
  content: { type: String, required: true },
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  tags: [String],
  status: { type: String, enum: ['draft', 'published', 'archived'], default: 'draft' },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const commentSchema = new mongoose.Schema({
  content: { type: String, required: true },
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const User = mongoose.model('User', userSchema);
const Post = mongoose.model('Post', postSchema);
const Comment = mongoose.model('Comment', commentSchema);

// JWT Authentication Middleware
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({
      error: 'Access token is required',
      code: 'TOKEN_REQUIRED'
    });
  }

  jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key', (err, user) => {
    if (err) {
      return res.status(403).json({
        error: 'Invalid or expired token',
        code: 'TOKEN_INVALID'
      });
    }
    req.user = user;
    next();
  });
};

// Authorization Middleware
const authorize = (roles = []) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({
        error: 'Authentication required',
        code: 'AUTHENTICATION_REQUIRED'
      });
    }

    if (roles.length && !roles.includes(req.user.role)) {
      return res.status(403).json({
        error: 'Insufficient permissions',
        code: 'INSUFFICIENT_PERMISSIONS'
      });
    }

    next();
  };
};

// Validation Error Handler
const handleValidationErrors = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: 'Validation failed',
      code: 'VALIDATION_ERROR',
      details: errors.array()
    });
  }
  next();
};

// HATEOAS Helper Functions
const addHATEOASLinks = (resource, baseUrl, additionalLinks = {}) => {
  const links = {
    self: `${baseUrl}/${resource._id}`,
    ...additionalLinks
  };
  
  return {
    ...resource.toObject(),
    _links: links
  };
};

const addCollectionLinks = (resources, baseUrl, page = 1, limit = 10, total = 0) => {
  const totalPages = Math.ceil(total / limit);
  
  return {
    _links: {
      self: `${baseUrl}?page=${page}&limit=${limit}`,
      first: page > 1 ? `${baseUrl}?page=1&limit=${limit}` : null,
      prev: page > 1 ? `${baseUrl}?page=${page - 1}&limit=${limit}` : null,
      next: page < totalPages ? `${baseUrl}?page=${page + 1}&limit=${limit}` : null,
      last: page < totalPages ? `${baseUrl}?page=${totalPages}&limit=${limit}` : null
    },
    _meta: {
      page,
      limit,
      total,
      totalPages
    },
    _embedded: resources
  };
};

// API Routes

// Health Check
app.get('/health', (req, res) => {
  res.status(200).json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    version: '1.0.0',
    _links: {
      self: '/health',
      docs: '/api-docs',
      users: '/api/users',
      posts: '/api/posts'
    }
  });
});

// User Registration
app.post('/api/auth/register', [
  body('username').isLength({ min: 3, max: 30 }).withMessage('Username must be 3-30 characters'),
  body('email').isEmail().withMessage('Valid email is required'),
  body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
  body('firstName').optional().isLength({ min: 1, max: 50 }),
  body('lastName').optional().isLength({ min: 1, max: 50 })
], handleValidationErrors, async (req, res) => {
  try {
    const { username, email, password, firstName, lastName } = req.body;

    // Check if user already exists
    const existingUser = await User.findOne({ $or: [{ username }, { email }] });
    if (existingUser) {
      return res.status(409).json({
        error: 'User already exists',
        code: 'USER_EXISTS'
      });
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(password, 10);

    // Create user
    const user = new User({
      username,
      email,
      password: hashedPassword,
      firstName,
      lastName
    });

    await user.save();

    // Generate JWT token
    const token = jwt.sign(
      { userId: user._id, username: user.username, role: user.role },
      process.env.JWT_SECRET || 'your-secret-key',
      { expiresIn: '24h' }
    );

    res.status(201).json({
      message: 'User registered successfully',
      token,
      user: addHATEOASLinks(user, '/api/users', {
        posts: `/api/users/${user._id}/posts`,
        comments: `/api/users/${user._id}/comments`
      })
    });
  } catch (error) {
    console.error('Registration error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// User Login
app.post('/api/auth/login', [
  body('username').notEmpty().withMessage('Username is required'),
  body('password').notEmpty().withMessage('Password is required')
], handleValidationErrors, async (req, res) => {
  try {
    const { username, password } = req.body;

    // Find user
    const user = await User.findOne({ username });
    if (!user) {
      return res.status(401).json({
        error: 'Invalid credentials',
        code: 'INVALID_CREDENTIALS'
      });
    }

    // Verify password
    const isValidPassword = await bcrypt.compare(password, user.password);
    if (!isValidPassword) {
      return res.status(401).json({
        error: 'Invalid credentials',
        code: 'INVALID_CREDENTIALS'
      });
    }

    // Generate JWT token
    const token = jwt.sign(
      { userId: user._id, username: user.username, role: user.role },
      process.env.JWT_SECRET || 'your-secret-key',
      { expiresIn: '24h' }
    );

    res.json({
      message: 'Login successful',
      token,
      user: addHATEOASLinks(user, '/api/users', {
        posts: `/api/users/${user._id}/posts`,
        comments: `/api/users/${user._id}/comments`
      })
    });
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// GET /api/users - Get all users (Admin only)
app.get('/api/users', authenticateToken, authorize(['admin']), [
  query('page').optional().isInt({ min: 1 }).withMessage('Page must be a positive integer'),
  query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100'),
  query('search').optional().isLength({ min: 1, max: 50 }).withMessage('Search term must be 1-50 characters')
], handleValidationErrors, async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const search = req.query.search;
    const skip = (page - 1) * limit;

    let query = {};
    if (search) {
      query = {
        $or: [
          { username: { $regex: search, $options: 'i' } },
          { email: { $regex: search, $options: 'i' } },
          { firstName: { $regex: search, $options: 'i' } },
          { lastName: { $regex: search, $options: 'i' } }
        ]
      };
    }

    const users = await User.find(query)
      .select('-password')
      .sort({ createdAt: -1 })
      .skip(skip)
      .limit(limit);

    const total = await User.countDocuments(query);

    const usersWithLinks = users.map(user => 
      addHATEOASLinks(user, '/api/users', {
        posts: `/api/users/${user._id}/posts`,
        comments: `/api/users/${user._id}/comments`
      })
    );

    const response = addCollectionLinks(usersWithLinks, '/api/users', page, limit, total);

    res.json(response);
  } catch (error) {
    console.error('Get users error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// GET /api/users/:id - Get user by ID
app.get('/api/users/:id', authenticateToken, [
  param('id').isMongoId().withMessage('Invalid user ID')
], handleValidationErrors, async (req, res) => {
  try {
    const { id } = req.params;

    // Users can only view their own profile unless they're admin
    if (req.user.userId !== id && req.user.role !== 'admin') {
      return res.status(403).json({
        error: 'Access denied',
        code: 'ACCESS_DENIED'
      });
    }

    const user = await User.findById(id).select('-password');
    if (!user) {
      return res.status(404).json({
        error: 'User not found',
        code: 'USER_NOT_FOUND'
      });
    }

    res.json(addHATEOASLinks(user, '/api/users', {
      posts: `/api/users/${user._id}/posts`,
      comments: `/api/users/${user._id}/comments`
    }));
  } catch (error) {
    console.error('Get user error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// PUT /api/users/:id - Update user
app.put('/api/users/:id', authenticateToken, [
  param('id').isMongoId().withMessage('Invalid user ID'),
  body('email').optional().isEmail().withMessage('Valid email is required'),
  body('firstName').optional().isLength({ min: 1, max: 50 }),
  body('lastName').optional().isLength({ min: 1, max: 50 })
], handleValidationErrors, async (req, res) => {
  try {
    const { id } = req.params;
    const updates = req.body;

    // Users can only update their own profile unless they're admin
    if (req.user.userId !== id && req.user.role !== 'admin') {
      return res.status(403).json({
        error: 'Access denied',
        code: 'ACCESS_DENIED'
      });
    }

    const user = await User.findById(id);
    if (!user) {
      return res.status(404).json({
        error: 'User not found',
        code: 'USER_NOT_FOUND'
      });
    }

    // Check if email is already taken by another user
    if (updates.email && updates.email !== user.email) {
      const existingUser = await User.findOne({ email: updates.email });
      if (existingUser) {
        return res.status(409).json({
          error: 'Email already exists',
          code: 'EMAIL_EXISTS'
        });
      }
    }

    // Update user
    Object.assign(user, updates, { updatedAt: new Date() });
    await user.save();

    res.json(addHATEOASLinks(user, '/api/users', {
      posts: `/api/users/${user._id}/posts`,
      comments: `/api/users/${user._id}/comments`
    }));
  } catch (error) {
    console.error('Update user error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// DELETE /api/users/:id - Delete user (Admin only)
app.delete('/api/users/:id', authenticateToken, authorize(['admin']), [
  param('id').isMongoId().withMessage('Invalid user ID')
], handleValidationErrors, async (req, res) => {
  try {
    const { id } = req.params;

    const user = await User.findById(id);
    if (!user) {
      return res.status(404).json({
        error: 'User not found',
        code: 'USER_NOT_FOUND'
      });
    }

    await User.findByIdAndDelete(id);

    res.status(204).send();
  } catch (error) {
    console.error('Delete user error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// GET /api/posts - Get all posts
app.get('/api/posts', [
  query('page').optional().isInt({ min: 1 }).withMessage('Page must be a positive integer'),
  query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100'),
  query('status').optional().isIn(['draft', 'published', 'archived']).withMessage('Invalid status'),
  query('author').optional().isMongoId().withMessage('Invalid author ID'),
  query('search').optional().isLength({ min: 1, max: 100 }).withMessage('Search term must be 1-100 characters')
], handleValidationErrors, async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const status = req.query.status;
    const author = req.query.author;
    const search = req.query.search;
    const skip = (page - 1) * limit;

    let query = {};
    
    if (status) query.status = status;
    if (author) query.author = author;
    
    if (search) {
      query = {
        ...query,
        $or: [
          { title: { $regex: search, $options: 'i' } },
          { content: { $regex: search, $options: 'i' } },
          { tags: { $in: [new RegExp(search, 'i')] } }
        ]
      };
    }

    const posts = await Post.find(query)
      .populate('author', 'username firstName lastName')
      .sort({ createdAt: -1 })
      .skip(skip)
      .limit(limit);

    const total = await Post.countDocuments(query);

    const postsWithLinks = posts.map(post => 
      addHATEOASLinks(post, '/api/posts', {
        author: `/api/users/${post.author._id}`,
        comments: `/api/posts/${post._id}/comments`
      })
    );

    const response = addCollectionLinks(postsWithLinks, '/api/posts', page, limit, total);

    res.json(response);
  } catch (error) {
    console.error('Get posts error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// POST /api/posts - Create new post
app.post('/api/posts', authenticateToken, [
  body('title').isLength({ min: 1, max: 200 }).withMessage('Title must be 1-200 characters'),
  body('content').isLength({ min: 1 }).withMessage('Content is required'),
  body('tags').optional().isArray().withMessage('Tags must be an array'),
  body('status').optional().isIn(['draft', 'published', 'archived']).withMessage('Invalid status')
], handleValidationErrors, async (req, res) => {
  try {
    const { title, content, tags, status } = req.body;

    const post = new Post({
      title,
      content,
      author: req.user.userId,
      tags: tags || [],
      status: status || 'draft'
    });

    await post.save();
    await post.populate('author', 'username firstName lastName');

    res.status(201).json(addHATEOASLinks(post, '/api/posts', {
      author: `/api/users/${post.author._id}`,
      comments: `/api/posts/${post._id}/comments`
    }));
  } catch (error) {
    console.error('Create post error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// GET /api/posts/:id - Get post by ID
app.get('/api/posts/:id', [
  param('id').isMongoId().withMessage('Invalid post ID')
], handleValidationErrors, async (req, res) => {
  try {
    const { id } = req.params;

    const post = await Post.findById(id).populate('author', 'username firstName lastName');
    if (!post) {
      return res.status(404).json({
        error: 'Post not found',
        code: 'POST_NOT_FOUND'
      });
    }

    res.json(addHATEOASLinks(post, '/api/posts', {
      author: `/api/users/${post.author._id}`,
      comments: `/api/posts/${post._id}/comments`
    }));
  } catch (error) {
    console.error('Get post error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// PUT /api/posts/:id - Update post
app.put('/api/posts/:id', authenticateToken, [
  param('id').isMongoId().withMessage('Invalid post ID'),
  body('title').optional().isLength({ min: 1, max: 200 }).withMessage('Title must be 1-200 characters'),
  body('content').optional().isLength({ min: 1 }).withMessage('Content is required'),
  body('tags').optional().isArray().withMessage('Tags must be an array'),
  body('status').optional().isIn(['draft', 'published', 'archived']).withMessage('Invalid status')
], handleValidationErrors, async (req, res) => {
  try {
    const { id } = req.params;
    const updates = req.body;

    const post = await Post.findById(id);
    if (!post) {
      return res.status(404).json({
        error: 'Post not found',
        code: 'POST_NOT_FOUND'
      });
    }

    // Check if user is the author or admin
    if (post.author.toString() !== req.user.userId && req.user.role !== 'admin') {
      return res.status(403).json({
        error: 'Access denied',
        code: 'ACCESS_DENIED'
      });
    }

    // Update post
    Object.assign(post, updates, { updatedAt: new Date() });
    await post.save();
    await post.populate('author', 'username firstName lastName');

    res.json(addHATEOASLinks(post, '/api/posts', {
      author: `/api/users/${post.author._id}`,
      comments: `/api/posts/${post._id}/comments`
    }));
  } catch (error) {
    console.error('Update post error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// DELETE /api/posts/:id - Delete post
app.delete('/api/posts/:id', authenticateToken, [
  param('id').isMongoId().withMessage('Invalid post ID')
], handleValidationErrors, async (req, res) => {
  try {
    const { id } = req.params;

    const post = await Post.findById(id);
    if (!post) {
      return res.status(404).json({
        error: 'Post not found',
        code: 'POST_NOT_FOUND'
      });
    }

    // Check if user is the author or admin
    if (post.author.toString() !== req.user.userId && req.user.role !== 'admin') {
      return res.status(403).json({
        error: 'Access denied',
        code: 'ACCESS_DENIED'
      });
    }

    await Post.findByIdAndDelete(id);

    res.status(204).send();
  } catch (error) {
    console.error('Delete post error:', error);
    res.status(500).json({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
});

// Swagger Documentation
const swaggerOptions = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'REST API Documentation',
      version: '1.0.0',
      description: 'A comprehensive REST API with HATEOAS',
    },
    servers: [
      {
        url: `http://localhost:${PORT}`,
        description: 'Development server',
      },
    ],
    components: {
      securitySchemes: {
        bearerAuth: {
          type: 'http',
          scheme: 'bearer',
          bearerFormat: 'JWT',
        },
      },
    },
  },
  apis: ['./server.js'],
};

const swaggerSpec = swaggerJsdoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));

// Error Handling Middleware
app.use((err, req, res, next) => {
  console.error('Unhandled error:', err);
  res.status(500).json({
    error: 'Internal server error',
    code: 'INTERNAL_ERROR'
  });
});

// 404 Handler
app.use('*', (req, res) => {
  res.status(404).json({
    error: 'Resource not found',
    code: 'NOT_FOUND',
    path: req.originalUrl
  });
});

// Start server
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/rest-api', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
})
.then(() => {
  app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
    console.log(`API Documentation: http://localhost:${PORT}/api-docs`);
  });
})
.catch(error => {
  console.error('Database connection error:', error);
  process.exit(1);
});

module.exports = app;

2. REST-API with Python and Flask

# app.py
from flask import Flask, request, jsonify, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_jwt_extended import (
    JWTManager, jwt_required, create_access_token,
    get_jwt_identity, get_jwt
)
from flask_bcrypt import Bcrypt
from flask_marshmallow import Marshmallow
from marshmallow import Schema, fields, validate, ValidationError
from flask_cors import CORS
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime, timedelta
import os
import re
from functools import wraps

# Initialize Flask app
app = Flask(__name__)

# Configuration
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL') or 'sqlite:///rest_api.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['JWT_SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY') or 'jwt-secret-key'
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=24)
app.config['JWT_BLACKLIST_ENABLED'] = True
app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access']

# Initialize extensions
db = SQLAlchemy(app)
migrate = Migrate(app, db)
jwt = JWTManager(app)
bcrypt = Bcrypt(app)
ma = Marshmallow(app)
CORS(app)
limiter = Limiter(app, key_func=get_remote_address)

# JWT Blacklist
blacklist = set()

@jwt.token_in_blocklist_loader
def check_if_token_revoked(jwt_header, jwt_payload):
    jti = jwt_payload['jti']
    return jti in blacklist

# Database Models
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)
    first_name = db.Column(db.String(50))
    last_name = db.Column(db.String(50))
    role = db.Column(db.Enum('user', 'admin'), default='user')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    posts = db.relationship('Post', backref='author', lazy='dynamic', cascade='all, delete-orphan')
    comments = db.relationship('Comment', backref='author', lazy='dynamic', cascade='all, delete-orphan')

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    status = db.Column(db.Enum('draft', 'published', 'archived'), default='draft')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    tags = db.relationship('Tag', secondary='post_tags', backref='posts')
    comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete-orphan')

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)

# Association table for many-to-many relationship
post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)

# Marshmallow Schemas
class UserSchema(Schema):
    id = fields.Int(dump_only=True)
    username = fields.Str(required=True, validate=validate.Length(min=3, max=80))
    email = fields.Email(required=True)
    first_name = fields.Str(validate=validate.Length(max=50))
    last_name = fields.Str(validate=validate.Length(max=50))
    role = fields.Str(dump_only=True)
    created_at = fields.DateTime(dump_only=True)
    updated_at = fields.DateTime(dump_only=True)
    
    _links = fields.Method('get_links')
    
    def get_links(self, obj):
        return {
            'self': url_for('get_user', id=obj.id, _external=True),
            'posts': url_for('get_user_posts', user_id=obj.id, _external=True),
            'comments': url_for('get_user_comments', user_id=obj.id, _external=True)
        }

class PostSchema(Schema):
    id = fields.Int(dump_only=True)
    title = fields.Str(required=True, validate=validate.Length(min=1, max=200))
    content = fields.Str(required=True)
    status = fields.Str(validate=validate.OneOf(['draft', 'published', 'archived']))
    created_at = fields.DateTime(dump_only=True)
    updated_at = fields.DateTime(dump_only=True)
    
    author = fields.Nested(UserSchema, only=('id', 'username', 'first_name', 'last_name'))
    tags = fields.List(fields.Nested(lambda: TagSchema(exclude=('posts',))))
    comments = fields.List(fields.Nested(lambda: CommentSchema(exclude=('post',))))
    
    _links = fields.Method('get_links')
    
    def get_links(self, obj):
        return {
            'self': url_for('get_post', id=obj.id, _external=True),
            'author': url_for('get_user', id=obj.author_id, _external=True),
            'comments': url_for('get_post_comments', post_id=obj.id, _external=True)
        }

class TagSchema(Schema):
    id = fields.Int(dump_only=True)
    name = fields.Str(required=True, validate=validate.Length(min=1, max=50))
    created_at = fields.DateTime(dump_only=True)
    
    _links = fields.Method('get_links')
    
    def get_links(self, obj):
        return {
            'self': url_for('get_tag', id=obj.id, _external=True),
            'posts': url_for('get_tag_posts', tag_id=obj.id, _external=True)
        }

class CommentSchema(Schema):
    id = fields.Int(dump_only=True)
    content = fields.Str(required=True)
    created_at = fields.DateTime(dump_only=True)
    updated_at = fields.DateTime(dump_only=True)
    
    author = fields.Nested(UserSchema, only=('id', 'username', 'first_name', 'last_name'))
    post = fields.Nested(PostSchema, only=('id', 'title'))
    
    _links = fields.Method('get_links')
    
    def get_links(self, obj):
        return {
            'self': url_for('get_comment', id=obj.id, _external=True),
            'author': url_for('get_user', id=obj.author_id, _external=True),
            'post': url_for('get_post', id=obj.post_id, _external=True)
        }

class CollectionSchema(Schema):
    _links = fields.Method('get_collection_links')
    _meta = fields.Method('get_collection_meta')
    _embedded = fields.Dict()

    def get_collection_links(self, obj):
        page = obj.get('page', 1)
        limit = obj.get('limit', 10)
        total = obj.get('total', 0)
        total_pages = (total + limit - 1) // limit
        base_url = obj.get('base_url', '')
        
        links = {
            'self': f"{base_url}?page={page}&limit={limit}"
        }
        
        if page > 1:
            links['first'] = f"{base_url}?page=1&limit={limit}"
            links['prev'] = f"{base_url}?page={page-1}&limit={limit}"
        
        if page < total_pages:
            links['next'] = f"{base_url}?page={page+1}&limit={limit}"
            links['last'] = f"{base_url}?page={total_pages}&limit={limit}"
        
        return links
    
    def get_collection_meta(self, obj):
        return {
            'page': obj.get('page', 1),
            'limit': obj.get('limit', 10),
            'total': obj.get('total', 0),
            'total_pages': (obj.get('total', 0) + obj.get('limit', 10) - 1) // obj.get('limit', 10)
        }

# Initialize schemas
user_schema = UserSchema()
users_schema = UserSchema(many=True)
post_schema = PostSchema()
posts_schema = PostSchema(many=True)
tag_schema = TagSchema()
tags_schema = TagSchema(many=True)
comment_schema = CommentSchema()
comments_schema = CommentSchema(many=True)
collection_schema = CollectionSchema()

# Helper Functions
def add_pagination_links(query, page, limit, base_url):
    """Add pagination links to response"""
    total = query.count()
    total_pages = (total + limit - 1) // limit
    
    items = query.offset((page - 1) * limit).limit(limit).all()
    
    collection_data = {
        'page': page,
        'limit': limit,
        'total': total,
        'base_url': base_url,
        '_embedded': {'items': items}
    }
    
    return collection_schema.dump(collection_data)

def admin_required(f):
    """Decorator to require admin role"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        current_user_id = get_jwt_identity()
        user = User.query.get(current_user_id)
        
        if not user or user.role != 'admin':
            return jsonify({
                'error': 'Admin access required',
                'code': 'ADMIN_REQUIRED'
            }), 403
        
        return f(*args, **kwargs)
    return decorated_function

def resource_owner_or_admin_required(f):
    """Decorator to require resource owner or admin role"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        current_user_id = get_jwt_identity()
        current_user = User.query.get(current_user_id)
        
        if not current_user:
            return jsonify({
                'error': 'Authentication required',
                'code': 'AUTHENTICATION_REQUIRED'
            }), 401
        
        # Check if user is admin or resource owner
        resource_id = kwargs.get('id') or kwargs.get('user_id') or kwargs.get('post_id')
        if resource_id:
            # For user resources
            if 'user_id' in kwargs or 'id' in kwargs:
                if current_user.role != 'admin' and str(current_user_id) != str(resource_id):
                    return jsonify({
                        'error': 'Access denied',
                        'code': 'ACCESS_DENIED'
                    }), 403
            # For post resources
            elif 'post_id' in kwargs:
                post = Post.query.get(resource_id)
                if not post or (current_user.role != 'admin' and str(post.author_id) != str(current_user_id)):
                    return jsonify({
                        'error': 'Access denied',
                        'code': 'ACCESS_DENIED'
                    }), 403
        
        return f(*args, **kwargs)
    return decorated_function

# API Routes

@app.route('/health', methods=['GET'])
def health_check():
    """Health check endpoint"""
    return jsonify({
        'status': 'healthy',
        'timestamp': datetime.utcnow().isoformat(),
        'version': '1.0.0',
        '_links': {
            'self': url_for('health_check', _external=True),
            'docs': url_for('api_docs', _external=True),
            'users': url_for('get_users', _external=True),
            'posts': url_for('get_posts', _external=True)
        }
    })

# Authentication Routes
@app.route('/api/auth/register', methods=['POST'])
@limiter.limit("5 per minute")
def register():
    """User registration"""
    json_data = request.get_json()
    
    if not json_data:
        return jsonify({
            'error': 'No input data provided',
            'code': 'NO_DATA'
        }), 400
    
    try:
        data = user_schema.load(json_data, partial=('first_name', 'last_name'))
    except ValidationError as err:
        return jsonify({
            'error': 'Validation failed',
            'code': 'VALIDATION_ERROR',
            'details': err.messages
        }), 400
    
    # Check if user already exists
    if User.query.filter_by(username=data['username']).first():
        return jsonify({
            'error': 'Username already exists',
            'code': 'USERNAME_EXISTS'
        }), 409
    
    if User.query.filter_by(email=data['email']).first():
        return jsonify({
            'error': 'Email already exists',
            'code': 'EMAIL_EXISTS'
        }), 409
    
    # Create user
    user = User(
        username=data['username'],
        email=data['email'],
        password_hash=generate_password_hash(data['password']),
        first_name=data.get('first_name'),
        last_name=data.get('last_name')
    )
    
    db.session.add(user)
    db.session.commit()
    
    # Generate access token
    access_token = create_access_token(identity=user.id)
    
    return jsonify({
        'message': 'User registered successfully',
        'token': access_token,
        'user': user_schema.dump(user)
    }), 201

@app.route('/api/auth/login', methods=['POST'])
@limiter.limit("10 per minute")
def login():
    """User login"""
    json_data = request.get_json()
    
    if not json_data:
        return jsonify({
            'error': 'No input data provided',
            'code': 'NO_DATA'
        }), 400
    
    username = json_data.get('username')
    password = json_data.get('password')
    
    if not username or not password:
        return jsonify({
            'error': 'Username and password are required',
            'code': 'MISSING_CREDENTIALS'
        }), 400
    
    # Find user
    user = User.query.filter_by(username=username).first()
    
    if not user or not check_password_hash(user.password_hash, password):
        return jsonify({
            'error': 'Invalid credentials',
            'code': 'INVALID_CREDENTIALS'
        }), 401
    
    # Generate access token
    access_token = create_access_token(identity=user.id)
    
    return jsonify({
        'message': 'Login successful',
        'token': access_token,
        'user': user_schema.dump(user)
    })

@app.route('/api/auth/logout', methods=['POST'])
@jwt_required()
def logout():
    """User logout"""
    jti = get_jwt()['jti']
    blacklist.add(jti)
    
    return jsonify({
        'message': 'Successfully logged out'
    })

# User Routes
@app.route('/api/users', methods=['GET'])
@jwt_required()
@admin_required
def get_users():
    """Get all users (admin only)"""
    page = request.args.get('page', 1, type=int)
    limit = min(request.args.get('limit', 10, type=int), 100)
    search = request.args.get('search', '')
    
    query = User.query
    
    if search:
        query = query.filter(
            db.or_(
                User.username.ilike(f'%{search}%'),
                User.email.ilike(f'%{search}%'),
                User.first_name.ilike(f'%{search}%'),
                User.last_name.ilike(f'%{search}%')
            )
        )
    
    return add_pagination_links(query, page, limit, url_for('get_users', _external=True))

@app.route('/api/users/<int:id>', methods=['GET'])
@jwt_required()
@resource_owner_or_admin_required
def get_user(id):
    """Get user by ID"""
    user = User.query.get_or_404(id)
    return user_schema.dump(user)

@app.route('/api/users/<int:id>', methods=['PUT'])
@jwt_required()
@resource_owner_or_admin_required
def update_user(id):
    """Update user"""
    user = User.query.get_or_404(id)
    json_data = request.get_json()
    
    if not json_data:
        return jsonify({
            'error': 'No input data provided',
            'code': 'NO_DATA'
        }), 400
    
    try:
        data = user_schema.load(json_data, partial=True)
    except ValidationError as err:
        return jsonify({
            'error': 'Validation failed',
            'code': 'VALIDATION_ERROR',
            'details': err.messages
        }), 400
    
    # Check if email is already taken by another user
    if 'email' in data and data['email'] != user.email:
        if User.query.filter_by(email=data['email']).first():
            return jsonify({
                'error': 'Email already exists',
                'code': 'EMAIL_EXISTS'
            }), 409
    
    # Update user
    for key, value in data.items():
        if key != 'password':  # Password update handled separately
            setattr(user, key, value)
    
    user.updated_at = datetime.utcnow()
    db.session.commit()
    
    return user_schema.dump(user)

@app.route('/api/users/<int:id>', methods=['DELETE'])
@jwt_required()
@admin_required
def delete_user(id):
    """Delete user (admin only)"""
    user = User.query.get_or_404(id)
    db.session.delete(user)
    db.session.commit()
    
    return '', 204

@app.route('/api/users/<int:user_id>/posts', methods=['GET'])
@jwt_required()
@resource_owner_or_admin_required
def get_user_posts(user_id):
    """Get posts by user"""
    page = request.args.get('page', 1, type=int)
    limit = min(request.args.get('limit', 10, type=int), 100)
    status = request.args.get('status')
    
    query = Post.query.filter_by(author_id=user_id)
    
    if status:
        query = query.filter_by(status=status)
    
    return add_pagination_links(query, page, limit, url_for('get_user_posts', user_id=user_id, _external=True))

@app.route('/api/users/<int:user_id>/comments', methods=['GET'])
@jwt_required()
@resource_owner_or_admin_required
def get_user_comments(user_id):
    """Get comments by user"""
    page = request.args.get('page', 1, type=int)
    limit = min(request.args.get('limit', 10, type=int), 100)
    
    query = Comment.query.filter_by(author_id=user_id)
    
    return add_pagination_links(query, page, limit, url_for('get_user_comments', user_id=user_id, _external=True))

# Post Routes
@app.route('/api/posts', methods=['GET'])
def get_posts():
    """Get all posts"""
    page = request.args.get('page', 1, type=int)
    limit = min(request.args.get('limit', 10, type=int), 100)
    status = request.args.get('status')
    author = request.args.get('author', type=int)
    search = request.args.get('search', '')
    tag = request.args.get('tag')
    
    query = Post.query
    
    if status:
        query = query.filter_by(status=status)
    
    if author:
        query = query.filter_by(author_id=author)
    
    if search:
        query = query.filter(
            db.or_(
                Post.title.ilike(f'%{search}%'),
                Post.content.ilike(f'%{search}%')
            )
        )
    
    if tag:
        query = query.filter(Post.tags.any(Tag.name == tag))
    
    return add_pagination_links(query, page, limit, url_for('get_posts', _external=True))

@app.route('/api/posts', methods=['POST'])
@jwt_required()
def create_post():
    """Create new post"""
    json_data = request.get_json()
    
    if not json_data:
        return jsonify({
            'error': 'No input data provided',
            'code': 'NO_DATA'
        }), 400
    
    try:
        data = post_schema.load(json_data, partial=('author', 'comments', 'created_at', 'updated_at'))
    except ValidationError as err:
        return jsonify({
            'error': 'Validation failed',
            'code': 'VALIDATION_ERROR',
            'details': err.messages
        }), 400
    
    # Create post
    post = Post(
        title=data['title'],
        content=data['content'],
        status=data.get('status', 'draft'),
        author_id=get_jwt_identity()
    )
    
    # Handle tags
    if 'tags' in data:
        for tag_name in data['tags']:
            tag = Tag.query.filter_by(name=tag_name).first()
            if not tag:
                tag = Tag(name=tag_name)
                db.session.add(tag)
            post.tags.append(tag)
    
    db.session.add(post)
    db.session.commit()
    
    return post_schema.dump(post), 201

@app.route('/api/posts/<int:id>', methods=['GET'])
def get_post(id):
    """Get post by ID"""
    post = Post.query.get_or_404(id)
    return post_schema.dump(post)

@app.route('/api/posts/<int:id>', methods=['PUT'])
@jwt_required()
@resource_owner_or_admin_required
def update_post(id):
    """Update post"""
    post = Post.query.get_or_404(id)
    json_data = request.get_json()
    
    if not json_data:
        return jsonify({
            'error': 'No input data provided',
            'code': 'NO_DATA'
        }), 400
    
    try:
        data = post_schema.load(json_data, partial=('author', 'comments', 'created_at', 'updated_at'))
    except ValidationError as err:
        return jsonify({
            'error': 'Validation failed',
            'code': 'VALIDATION_ERROR',
            'details': err.messages
        }), 400
    
    # Update post
    for key, value in data.items():
        if key != 'tags':  # Tags handled separately
            setattr(post, key, value)
    
    post.updated_at = datetime.utcnow()
    
    # Handle tags
    if 'tags' in data:
        post.tags.clear()
        for tag_name in data['tags']:
            tag = Tag.query.filter_by(name=tag_name).first()
            if not tag:
                tag = Tag(name=tag_name)
                db.session.add(tag)
            post.tags.append(tag)
    
    db.session.commit()
    
    return post_schema.dump(post)

@app.route('/api/posts/<int:id>', methods=['DELETE'])
@jwt_required()
@resource_owner_or_admin_required
def delete_post(id):
    """Delete post"""
    post = Post.query.get_or_404(id)
    db.session.delete(post)
    db.session.commit()
    
    return '', 204

@app.route('/api/posts/<int:post_id>/comments', methods=['GET'])
def get_post_comments(post_id):
    """Get comments for a post"""
    page = request.args.get('page', 1, type=int)
    limit = min(request.args.get('limit', 10, type=int), 100)
    
    query = Comment.query.filter_by(post_id=post_id)
    
    return add_pagination_links(query, page, limit, url_for('get_post_comments', post_id=post_id, _external=True))

# Tag Routes
@app.route('/api/tags', methods=['GET'])
def get_tags():
    """Get all tags"""
    page = request.args.get('page', 1, type=int)
    limit = min(request.args.get('limit', 10, type=int), 100)
    search = request.args.get('search', '')
    
    query = Tag.query
    
    if search:
        query = query.filter(Tag.name.ilike(f'%{search}%'))
    
    return add_pagination_links(query, page, limit, url_for('get_tags', _external=True))

@app.route('/api/tags/<int:id>', methods=['GET'])
def get_tag(id):
    """Get tag by ID"""
    tag = Tag.query.get_or_404(id)
    return tag_schema.dump(tag)

@app.route('/api/tags/<int:tag_id>/posts', methods=['GET'])
def get_tag_posts(tag_id):
    """Get posts by tag"""
    page = request.args.get('page', 1, type=int)
    limit = min(request.args.get('limit', 10, type=int), 100)
    
    query = Post.query.filter(Post.tags.any(Tag.id == tag_id))
    
    return add_pagination_links(query, page, limit, url_for('get_tag_posts', tag_id=tag_id, _external=True))

# Comment Routes
@app.route('/api/comments', methods=['POST'])
@jwt_required()
def create_comment():
    """Create new comment"""
    json_data = request.get_json()
    
    if not json_data:
        return jsonify({
            'error': 'No input data provided',
            'code': 'NO_DATA'
        }), 400
    
    try:
        data = comment_schema.load(json_data, partial=('author', 'post', 'created_at', 'updated_at'))
    except ValidationError as err:
        return jsonify({
            'error': 'Validation failed',
            'code': 'VALIDATION_ERROR',
            'details': err.messages
        }), 400
    
    # Check if post exists
    post = Post.query.get(data['post_id'])
    if not post:
        return jsonify({
            'error': 'Post not found',
            'code': 'POST_NOT_FOUND'
        }), 404
    
    # Create comment
    comment = Comment(
        content=data['content'],
        author_id=get_jwt_identity(),
        post_id=data['post_id']
    )
    
    db.session.add(comment)
    db.session.commit()
    
    return comment_schema.dump(comment), 201

@app.route('/api/comments/<int:id>', methods=['GET'])
def get_comment(id):
    """Get comment by ID"""
    comment = Comment.query.get_or_404(id)
    return comment_schema.dump(comment)

@app.route('/api/comments/<int:id>', methods=['PUT'])
@jwt_required()
@resource_owner_or_admin_required
def update_comment(id):
    """Update comment"""
    comment = Comment.query.get_or_404(id)
    json_data = request.get_json()
    
    if not json_data:
        return jsonify({
            'error': 'No input data provided',
            'code': 'NO_DATA'
        }), 400
    
    try:
        data = comment_schema.load(json_data, partial=('author', 'post', 'created_at', 'updated_at'))
    except ValidationError as err:
        return jsonify({
            'error': 'Validation failed',
            'code': 'VALIDATION_ERROR',
            'details': err.messages
        }), 400
    
    # Update comment
    comment.content = data['content']
    comment.updated_at = datetime.utcnow()
    
    db.session.commit()
    
    return comment
## REST-API Architecture

### HTTP Methods and Their Semantics
```mermaid
graph TD
    A[HTTP Method] --> B{Operation Type}
    
    B -->|Safe & Idempotent| C[GET]
    B -->|Unsafe & Non-Idempotent| D[POST]
    B -->|Unsafe & Idempotent| E[PUT]
    B -->|Unsafe & Non-Idempotent| F[PATCH]
    B -->|Unsafe & Idempotent| G[DELETE]
    
    C --> C1[Read Resource]
    D --> D1[Create Resource]
    E --> E1[Replace Resource]
    F --> F1[Update Resource]
    G --> G1[Delete Resource]
    
    C1 --> C2[/users]
    D1 --> D2[/users]
    E1 --> E2[/users/123]
    F1 --> F2[/users/123]
    G1 --> G2[/users/123]

HTTP Status Codes Overview

Success Codes (2xx)

CodeMeaningUsage
200 OKRequest successfulGET, PUT, DELETE
201 CreatedResource createdPOST
204 No ContentNo contentDELETE, PUT

Client Error Codes (4xx)

CodeMeaningUsage
400 Bad RequestInvalid requestValidation errors
401 UnauthorizedAuthentication missingLogin required
403 ForbiddenAccess deniedPermission error
404 Not FoundResource not foundWrong URL
409 ConflictConflict with stateDuplicates

Server Error Codes (5xx)

CodeMeaningUsage
500 Internal Server ErrorServer errorGeneral error
502 Bad GatewayGateway errorProxy problem
503 Service UnavailableService unavailableMaintenance

HATEOAS Principles

Hypermedia Controls

{
  "id": 123,
  "title": "REST API Guide",
  "status": "published",
  "_links": {
    "self": {
      "href": "/api/posts/123"
    },
    "author": {
      "href": "/api/users/456"
    },
    "comments": {
      "href": "/api/posts/123/comments"
    },
    "update": {
      "href": "/api/posts/123",
      "method": "PUT"
    },
    "delete": {
      "href": "/api/posts/123",
      "method": "DELETE"
    }
  },
  "_embedded": {
    "author": {
      "id": 456,
      "username": "john_doe"
    }
  }
}

API Design Best Practices

URL Design

PracticeExampleExplanation
Plural Nouns/api/usersResources as plural
Hierarchical/api/users/123/postsLogical structure
Consistent/api/posts/456/commentsUniform pattern
Versioning/api/v1/usersAPI versions

Response Format

{
  "data": {
    "id": 123,
    "title": "Example Post"
  },
  "_links": {
    "self": "/api/posts/123"
  },
  "_meta": {
    "timestamp": "2024-01-01T12:00:00Z",
    "version": "1.0"
  }
}

Advantages and Disadvantages

Advantages of REST APIs

  • Scalability: Stateless architecture
  • Flexibility: Platform-independent
  • Simplicity: Based on HTTP standard
  • Cacheability: HTTP caching possible
  • Testability: Easy to test

Disadvantages

  • Overhead: HTTP header overhead
  • Complexity: Many endpoints in large APIs
  • Versioning: API versioning complex
  • Documentation: Manual maintenance required
  • State Management: Client-side state

Common Exam Questions

  1. What is the difference between PUT and PATCH? PUT replaces an entire resource, PATCH updates only specific parts of a resource.

  2. Explain HATEOAS! HATEOAS means that API responses contain hypermedia links that enable the client to navigate through the API.

  3. When do you use which HTTP status code? 200 for successful GET requests, 201 for POST creation, 404 for resources not found, 500 for server errors.

  4. What makes an API “RESTful”? Resource-oriented URLs, correct HTTP methods, stateless communication, HATEOAS, uniform interface.

Key Sources

  1. https://restfulapi.net/
  2. https://www.ietf.org/rfc/rfc7231.txt
  3. https://jsonapi.org/
  4. https://swagger.io/specification/
Back to Blog
Share:

Related Posts