🎯 Resources are Nouns
Use nouns for resources, HTTP methods for actions. /users with GET, not /getUsers.
REST (Representational State Transfer) is an architectural style for designing web APIs. Think of it as a set of rules for how systems should communicate over HTTP.
Resources are the core of REST. A resource is anything that can be identified and manipulated.
Good resources are:
/users, not /getUsers)/users/123/orders)/users, not /user)| ✅ Good | ❌ Bad | Why? |
|---|---|---|
/users | /getUsers | Verbs in URL |
/users/123 | /user/123 | Inconsistent plural |
/users/123/orders | /orders?userId=123 | Better hierarchy |
/products/456 | /product?id=456 | Resource in path, not query |
HTTP methods define what action to perform on a resource.
| Operation | HTTP Method | Idempotent? | Safe? | Use Case |
|---|---|---|---|---|
| Create | POST | ❌ No | ❌ No | Create new resource |
| Read | GET | ✅ Yes | ✅ Yes | Retrieve resource |
| Update (Full) | PUT | ✅ Yes | ❌ No | Replace entire resource |
| Update (Partial) | PATCH | ❌ No* | ❌ No | Update part of resource |
| Delete | DELETE | ✅ Yes | ❌ No | Remove resource |
*PATCH can be idempotent if designed correctly
GET is for reading data. It’s safe (no side effects) and idempotent.
GET /users/123GET /users?status=active&page=1GET /users/123/ordersCharacteristics:
POST is for creating new resources. It’s not idempotent (calling twice creates two resources).
POST /usersContent-Type: application/json
{ "name": "John Doe",}Characteristics:
Response:
HTTP/1.1 201 CreatedLocation: /users/456Content-Type: application/json
{ "id": 456, "name": "John Doe",}PUT replaces an entire resource. It’s idempotent (calling twice has same effect as once).
PUT /users/123Content-Type: application/json
{ "name": "Jane Doe", "status": "active"}Characteristics:
PATCH updates part of a resource. Should be idempotent if designed correctly.
PATCH /users/123Content-Type: application/json
{ "name": "Jane Doe"}Characteristics:
DELETE removes a resource. It’s idempotent (deleting twice = same as once).
DELETE /users/123Characteristics:
Status codes communicate the result of the request. Use them correctly!
| Code | Meaning | Use Case |
|---|---|---|
| 200 OK | Request succeeded | GET, PUT, PATCH |
| 201 Created | Resource created | POST (with Location header) |
| 204 No Content | Success, no body | DELETE, PUT (sometimes) |
| Code | Meaning | Use Case |
|---|---|---|
| 400 Bad Request | Invalid request | Malformed JSON, missing fields |
| 401 Unauthorized | Not authenticated | Missing/invalid token |
| 403 Forbidden | Not authorized | Valid token, but no permission |
| 404 Not Found | Resource doesn’t exist | Invalid ID, wrong URL |
| 409 Conflict | Resource conflict | Duplicate email, version conflict |
| 429 Too Many Requests | Rate limited | Too many requests |
| Code | Meaning | Use Case |
|---|---|---|
| 500 Internal Server Error | Server error | Unexpected exception |
| 502 Bad Gateway | Upstream error | Downstream service failed |
| 503 Service Unavailable | Service down | Maintenance, overloaded |
✅ Do:
❌ Don’t:
Versioning allows you to evolve your API without breaking existing clients.
Version in the URL path:
/api/v1/users/api/v2/usersPros:
Cons:
Version in HTTP headers:
GET /usersAccept: application/vnd.api+json;version=1Pros:
Cons:
Version as query parameter:
/api/users?version=1Pros:
Cons:
❌ Bad:
POST /users/123/deleteGET /users/create?name=JohnPOST /users/123/update✅ Good:
DELETE /users/123POST /users (with body)PUT /users/123 (with body)❌ Bad:
/getUsers/createUser/updateUser/deleteUser✅ Good:
GET /usersPOST /usersPUT /users/123DELETE /users/123❌ Bad:
/user/order✅ Good:
/users/orders❌ Bad:
/orders?userId=123✅ Good:
/users/123/orders❌ Bad:
/users/customers/clients✅ Good:
/users (consistent across API)❌ Bad:
// Always returns 200, even for errors{ "success": false, "error": "User not found"}✅ Good:
HTTP/1.1 404 Not FoundContent-Type: application/json
{ "error": "User not found", "code": "USER_NOT_FOUND"}❌ Bad:
GET /users // Returns 10,000 users✅ Good:
GET /users?page=1&limit=20GET /users?offset=0&limit=20GET /users?cursor=abc123&limit=20Response:
{ "data": [...], "pagination": { "page": 1, "limit": 20, "total": 1000, "hasNext": true }}GET /users?status=active&role=admin&sort=name&order=ascGET /users?search=john&limit=10Include links to related resources:
{ "id": 123, "name": "John Doe", "links": { "self": "/users/123", "orders": "/users/123/orders", "profile": "/users/123/profile" }}At the code level, REST APIs translate to controllers, services, and DTOs.
from flask import Flask, request, jsonifyfrom typing import Optional
app = Flask(__name__)
class UserController: def __init__(self, user_service): self.user_service = user_service
def get_user(self, user_id: int): """GET /users/:id""" user = self.user_service.get_user(user_id)
if not user: return jsonify({"error": "User not found"}), 404
return jsonify(user), 200
def create_user(self): """POST /users""" data = request.get_json()
# Validate input if not data or 'email' not in data: return jsonify({"error": "Email required"}), 400
user = self.user_service.create_user(data) return jsonify(user), 201, {'Location': f'/users/{user["id"]}'}
def update_user(self, user_id: int): """PUT /users/:id""" data = request.get_json()
if not data: return jsonify({"error": "Request body required"}), 400
user = self.user_service.update_user(user_id, data)
if not user: return jsonify({"error": "User not found"}), 404
return jsonify(user), 200
def delete_user(self, user_id: int): """DELETE /users/:id""" success = self.user_service.delete_user(user_id)
if not success: return jsonify({"error": "User not found"}), 404
return '', 204
# Routes@app.route('/users/<int:user_id>', methods=['GET', 'PUT', 'DELETE'])def user_detail(user_id): controller = UserController(user_service)
if request.method == 'GET': return controller.get_user(user_id) elif request.method == 'PUT': return controller.update_user(user_id) elif request.method == 'DELETE': return controller.delete_user(user_id)
@app.route('/users', methods=['POST'])def user_create(): controller = UserController(user_service) return controller.create_user()import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;
@RestController@RequestMapping("/users")public class UserController { private final UserService userService;
public UserController(UserService userService) { this.userService = userService; }
@GetMapping("/{id}") public ResponseEntity<UserDTO> getUser(@PathVariable int id) { // GET /users/:id Optional<UserDTO> user = userService.getUser(id);
if (user.isEmpty()) { return ResponseEntity.notFound().build(); }
return ResponseEntity.ok(user.get()); }
@PostMapping public ResponseEntity<UserDTO> createUser(@RequestBody CreateUserRequest request) { // POST /users UserDTO user = userService.createUser(request);
return ResponseEntity .status(HttpStatus.CREATED) .header("Location", "/users/" + user.getId()) .body(user); }
@PutMapping("/{id}") public ResponseEntity<UserDTO> updateUser( @PathVariable int id, @RequestBody UpdateUserRequest request) { // PUT /users/:id Optional<UserDTO> user = userService.updateUser(id, request);
if (user.isEmpty()) { return ResponseEntity.notFound().build(); }
return ResponseEntity.ok(user.get()); }
@DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable int id) { // DELETE /users/:id boolean deleted = userService.deleteUser(id);
if (!deleted) { return ResponseEntity.notFound().build(); }
return ResponseEntity.noContent().build(); }}from typing import Optional, Dict, Any
class UserService: def __init__(self, user_repository): self.user_repository = user_repository
def get_user(self, user_id: int) -> Optional[Dict[str, Any]]: """Business logic for getting user""" user = self.user_repository.find_by_id(user_id)
if not user: return None
# Transform to DTO return { "id": user.id, "name": user.name, "email": user.email, "status": user.status }
def create_user(self, data: Dict[str, Any]) -> Dict[str, Any]: """Business logic for creating user""" # Validate business rules if self.user_repository.find_by_email(data['email']): raise ValueError("Email already exists")
# Create user user = self.user_repository.create(data)
return { "id": user.id, "name": user.name, "email": user.email, "status": user.status }
def update_user(self, user_id: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: """Business logic for updating user""" user = self.user_repository.find_by_id(user_id)
if not user: return None
# Update user updated_user = self.user_repository.update(user_id, data)
return { "id": updated_user.id, "name": updated_user.name, "email": updated_user.email, "status": updated_user.status }
def delete_user(self, user_id: int) -> bool: """Business logic for deleting user""" user = self.user_repository.find_by_id(user_id)
if not user: return False
self.user_repository.delete(user_id) return Trueimport java.util.Optional;
@Servicepublic class UserService { private final UserRepository userRepository;
public UserService(UserRepository userRepository) { this.userRepository = userRepository; }
public Optional<UserDTO> getUser(int id) { // Business logic for getting user return userRepository.findById(id) .map(this::toDTO); }
public UserDTO createUser(CreateUserRequest request) { // Validate business rules if (userRepository.findByEmail(request.getEmail()).isPresent()) { throw new ConflictException("Email already exists"); }
// Create user User user = userRepository.save(toEntity(request)); return toDTO(user); }
public Optional<UserDTO> updateUser(int id, UpdateUserRequest request) { // Business logic for updating user return userRepository.findById(id) .map(user -> { user.update(request); return toDTO(userRepository.save(user)); }); }
public boolean deleteUser(int id) { // Business logic for deleting user if (!userRepository.existsById(id)) { return false; }
userRepository.deleteById(id); return true; }
private UserDTO toDTO(User user) { return new UserDTO( user.getId(), user.getName(), user.getEmail(), user.getStatus() ); }}from flask import jsonifyfrom werkzeug.exceptions import HTTPException
class APIError(Exception): def __init__(self, message, status_code=400): self.message = message self.status_code = status_code
@app.errorhandler(APIError)def handle_api_error(error): return jsonify({ "error": error.message, "status": error.status_code }), error.status_code
@app.errorhandler(404)def handle_not_found(error): return jsonify({ "error": "Resource not found", "status": 404 }), 404
@app.errorhandler(500)def handle_server_error(error): return jsonify({ "error": "Internal server error", "status": 500 }), 500import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvicepublic class ErrorHandler {
@ExceptionHandler(NotFoundException.class) public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) { return ResponseEntity .status(404) .body(new ErrorResponse("Resource not found", 404)); }
@ExceptionHandler(BadRequestException.class) public ResponseEntity<ErrorResponse> handleBadRequest(BadRequestException e) { return ResponseEntity .status(400) .body(new ErrorResponse(e.getMessage(), 400)); }
@ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleGeneric(Exception e) { return ResponseEntity .status(500) .body(new ErrorResponse("Internal server error", 500)); }}Make POST requests idempotent using idempotency keys:
POST /ordersIdempotency-Key: abc123-xyz789Content-Type: application/json
{ "productId": 456, "quantity": 2}Server behavior:
Support multiple formats:
GET /users/123Accept: application/json
GET /users/123Accept: application/xmlLet clients choose fields:
GET /users/123?fields=id,name,email🎯 Resources are Nouns
Use nouns for resources, HTTP methods for actions. /users with GET, not /getUsers.
📊 Proper Status Codes
Use appropriate status codes. 201 for creation, 404 for not found, 400 for bad requests.
🔄 Idempotency Matters
GET, PUT, DELETE are idempotent. Design POST/PATCH to be idempotent when possible.
🏗️ Layered Architecture
Controllers handle HTTP, Services handle business logic, Repositories handle data access.