Table of Contents
- REST API Basics
- HTTP Methods & Status Codes
- API Design Principles
- Implementation Examples
- Authentication & Security
- Testing & Documentation
- Best Practices
- Tools & Resources
What is REST? REST (Representational State Transfer) is an architectural style for designing networked applications. It uses standard HTTP methods and is stateless, meaning each request contains all information needed to process it.
Core REST Principles ⦁ Stateless: Each request is independent ⦁ Client-Server: Separation of concerns ⦁ Cacheable: Responses can be cached ⦁ Uniform Interface: Consistent API structure ⦁ Layered System: Architecture can have multiple layers
HTTP Methods
| Method | Purpose | Example |
|---|---|---|
GET |
Retrieve data | GET /api/users |
POST |
Create new resource | POST /api/users |
PUT |
Update entire resource | PUT /api/users/123 |
PATCH |
Partial update | PATCH /api/users/123 |
DELETE |
Remove resource | DELETE /api/users/123 |
Common Status Codes
| Code | Meaning | When to Use |
|---|---|---|
200 |
OK | Successful GET, PUT, PATCH |
201 |
Created | Successful POST |
204 |
No Content | Successful DELETE |
400 |
Bad Request | Invalid request data |
401 |
Unauthorized | Authentication required |
403 |
Forbidden | Access denied |
404 |
Not Found | Resource doesn't exist |
500 |
Internal Server Error | Server-side error |
- URL Structure
// Good URLs
GET /api/v1/users // Get all users
GET /api/v1/users/123 // Get specific user
GET /api/v1/users/123/posts // Get user's posts
POST /api/v1/users/123/posts // Create new post for user
// Bad URLs
GET /api/v1/getUsers // Don't use verbs
GET /api/v1/user/123/posts/get // Redundant verb
- Consistent Naming
⦁ Use nouns for resources, not verbs
⦁ Use plural forms (
/users, not/user) ⦁ Use kebab-case for multi-word resources (/user-profiles) ⦁ Be consistent with naming conventions - Query Parameters
GET /api/v1/users?page=2&limit=10&sort=name&order=asc&filter=active
Node.js + Express
const express = require('express');
const app = express();
app.use(express.json());
// In-memory data (use database in production)
let users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
// GET all users
app.get('/api/users', (req, res) => {
res.status(200).json({
success: true,
data: users,
count: users.length
});
});
// GET single user
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.status(200).json({
success: true,
data: user
});
});
// CREATE user
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
// Validation
if (!name || !email) {
return res.status(400).json({
success: false,
message: 'Name and email are required'
});
}
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json({
success: true,
data: newUser
});
});
// UPDATE user
app.put('/api/users/:id', (req, res) => {
const userId = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
const { name, email } = req.body;
users[userIndex] = { id: userId, name, email };
res.status(200).json({
success: true,
data: users[userIndex]
});
});
// DELETE user
app.delete('/api/users/:id', (req, res) => {
const userId = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
users.splice(userIndex, 1);
res.status(204).send();
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});Python + Flask
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(100), nullable=False, unique=True)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'email': self.email
}
@app.route('/api/users', methods=['GET'])
def get_users():
users = User.query.all()
return jsonify({
'success': True,
'data': [user.to_dict() for user in users],
'count': len(users)
})
@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
user = User.query.get(user_id)
if not user:
return jsonify({'success': False, 'message': 'User not found'}), 404
return jsonify({'success': True, 'data': user.to_dict()})
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
if not data.get('name') or not data.get('email'):
return jsonify({
'success': False,
'message': 'Name and email are required'
}), 400
user = User(name=data['name'], email=data['email'])
db.session.add(user)
db.session.commit()
return jsonify({'success': True, 'data': user.to_dict()}), 201
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)Java + Spring Boot
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public ResponseEntity<ApiResponse<List<User>>> getAllUsers() {
List<User> users = userService.findAll();
return ResponseEntity.ok(new ApiResponse<>(true, users, users.size()));
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUserById(@PathVariable Long id) {
Optional<User> user = userService.findById(id);
if (user.isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ApiResponse<>(false, "User not found"));
}
return ResponseEntity.ok(new ApiResponse<>(true, user.get()));
}
@PostMapping
public ResponseEntity<ApiResponse<User>> createUser(@RequestBody @Valid User user) {
User savedUser = userService.save(user);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new ApiResponse<>(true, savedUser));
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<User>> updateUser(
@PathVariable Long id,
@RequestBody @Valid User user) {
if (!userService.existsById(id)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ApiResponse<>(false, "User not found"));
}
user.setId(id);
User updatedUser = userService.save(user);
return ResponseEntity.ok(new ApiResponse<>(true, updatedUser));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (!userService.existsById(id)) {
return ResponseEntity.notFound().build();
}
userService.deleteById(id);
return ResponseEntity.noContent().build();
}
}- JWT Authentication
const jwt = require('jsonwebtoken');
// Middleware for JWT verification
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid token' });
}
req.user = user;
next();
});
};
// Login endpoint
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
// Verify user credentials (implement your logic)
const user = await verifyUserCredentials(email, password);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token, user: { id: user.id, email: user.email } });
});
// Protected route
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({ message: 'This is protected', user: req.user });
});- API Key Authentication
const authenticateApiKey = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || !isValidApiKey(apiKey)) {
return res.status(401).json({ message: 'Valid API key required' });
}
next();
};
app.use('/api', authenticateApiKey);- Security Best Practices ⦁ Use HTTPS in production ⦁ Implement rate limiting ⦁ Validate and sanitize input ⦁ Use parameterized queries (prevent SQL injection) ⦁ Implement CORS properly ⦁ Log security events
Testing with Jest (Node.js)
const request = require('supertest');
const app = require('./app');
describe('Users API', () => {
test('GET /api/users should return all users', async () => {
const response = await request(app)
.get('/api/users')
.expect(200);
expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data)).toBe(true);
});
test('POST /api/users should create new user', async () => {
const newUser = {
name: 'Test User',
email: 'test@example.com'
};
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.name).toBe(newUser.name);
});
});API Documentation (OpenAPI/Swagger)
openapi: 3.0.0
info:
title: User Management API
version: 1.0.0
description: REST API for managing users
paths:
/api/users:
get:
summary: Get all users
responses:
'200':
description: List of users
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
type: array
items:
$ref: '#/components/schemas/User'
count:
type: integer
post:
summary: Create new user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserInput'
responses:
'201':
description: User created successfully
'400':
description: Invalid input
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
UserInput:
type: object
required:
- name
- email
properties:
name:
type: string
email:
type: string- Error Handling
// Consistent error response format
const errorHandler = (err, req, res, next) => {
console.error(err.stack);
const error = {
success: false,
message: err.message || 'Internal Server Error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
};
const statusCode = err.statusCode || 500;
res.status(statusCode).json(error);
};
app.use(errorHandler);- Validation Middleware
const { body, validationResult } = require('express-validator');
const validateUser = [
body('name').trim().isLength({ min: 1 }).withMessage('Name is required'),
body('email').isEmail().withMessage('Valid email is required'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
}
];
app.post('/api/users', validateUser, createUser);- Pagination
app.get('/api/users', (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const offset = (page - 1) * limit;
const users = getUsersPaginated(offset, limit);
const total = getTotalUsers();
res.json({
success: true,
data: users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
});- Logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Log API requests
app.use((req, res, next) => {
logger.info(`${req.method} ${req.path} - ${req.ip}`);
next();
});Development Tools ⦁ Postman - API testing and documentation ⦁ Insomnia - REST client ⦁ Thunder Client - VS Code extension ⦁ Swagger/OpenAPI - API documentation ⦁ Newman - Command-line Postman
Testing Tools ⦁ Jest - JavaScript testing ⦁ Mocha/Chai - Node.js testing ⦁ pytest - Python testing ⦁ JUnit - Java testing
// MongoDB with Mongoose
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
createdAt: { type: Date, default: Date.now }
});
const User = mongoose.model('User', userSchema);
// PostgreSQL with pg
const { Pool } = require('pg');
const pool = new Pool({
user: 'username',
host: 'localhost',
database: 'mydb',
password: 'password',
port: 5432,
});Rate Limiting
const rateLimit = require('express-rate-limit');
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);CORS Configuration
const cors = require('cors');
const corsOptions = {
origin: ['http://localhost:3000', 'https://yourapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));- Caching with Redis
const redis = require('redis');
const client = redis.createClient();
const cacheMiddleware = (duration = 300) => {
return async (req, res, next) => {
const key = req.originalUrl;
const cached = await client.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
res.sendResponse = res.json;
res.json = (data) => {
client.setex(key, duration, JSON.stringify(data));
res.sendResponse(data);
};
next();
};
};
app.get('/api/users', cacheMiddleware(600), getUsers);- API Versioning
// URL versioning
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);
// Header versioning
app.use((req, res, next) => {
const version = req.headers['api-version'] || 'v1';
req.apiVersion = version;
next();
});- File Upload
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
app.post('/api/users/:id/avatar', upload.single('avatar'), (req, res) => {
// Handle file upload
res.json({
success: true,
message: 'Avatar uploaded successfully',
filename: req.file.filename
});
});This comprehensive guide covers everything from basic REST principles to advanced implementation patterns. Start with the basics and gradually implement more advanced features as your API grows!