🎯 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.
1GET /users/1232GET /users?status=active&page=13GET /users/123/ordersCharacteristics:
POST is for creating new resources. It’s not idempotent (calling twice creates two resources).
1POST /users2Content-Type: application/json3
4{5 "name": "John Doe",6 "email": "john@example.com"7}Characteristics:
Response:
1HTTP/1.1 201 Created2Location: /users/4563Content-Type: application/json4
5{6 "id": 456,7 "name": "John Doe",8 "email": "john@example.com"9}PUT replaces an entire resource. It’s idempotent (calling twice has same effect as once).
1PUT /users/1232Content-Type: application/json3
4{5 "name": "Jane Doe",6 "email": "jane@example.com",7 "status": "active"8}Characteristics:
PATCH updates part of a resource. Should be idempotent if designed correctly.
1PATCH /users/1232Content-Type: application/json3
4{5 "name": "Jane Doe"6}Characteristics:
DELETE removes a resource. It’s idempotent (deleting twice = same as once).
1DELETE /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:
1/api/v1/users2/api/v2/usersPros:
Cons:
Version in HTTP headers:
1GET /users2Accept: application/vnd.api+json;version=1Pros:
Cons:
Version as query parameter:
1/api/users?version=1Pros:
Cons:
❌ Bad:
1POST /users/123/delete2GET /users/create?name=John3POST /users/123/update✅ Good:
1DELETE /users/1232POST /users (with body)3PUT /users/123 (with body)❌ Bad:
1/getUsers2/createUser3/updateUser4/deleteUser✅ Good:
1GET /users2POST /users3PUT /users/1234DELETE /users/123❌ Bad:
1/user2/order✅ Good:
1/users2/orders❌ Bad:
1/orders?userId=123✅ Good:
1/users/123/orders❌ Bad:
1/users2/customers3/clients✅ Good:
1/users (consistent across API)❌ Bad:
1// Always returns 200, even for errors2{3 "success": false,4 "error": "User not found"5}✅ Good:
1HTTP/1.1 404 Not Found2Content-Type: application/json3
4{5 "error": "User not found",6 "code": "USER_NOT_FOUND"7}❌ Bad:
1GET /users // Returns 10,000 users✅ Good:
1GET /users?page=1&limit=202GET /users?offset=0&limit=203GET /users?cursor=abc123&limit=20Response:
1{2 "data": [...],3 "pagination": {4 "page": 1,5 "limit": 20,6 "total": 1000,7 "hasNext": true8 }9}1GET /users?status=active&role=admin&sort=name&order=asc2GET /users?search=john&limit=10Include links to related resources:
1{2 "id": 123,3 "name": "John Doe",4 "links": {5 "self": "/users/123",6 "orders": "/users/123/orders",7 "profile": "/users/123/profile"8 }9}At the code level, REST APIs translate to controllers, services, and DTOs.
1from flask import Flask, request, jsonify2from typing import Optional3
4app = Flask(__name__)5
6class UserController:7 def __init__(self, user_service):8 self.user_service = user_service9
10 def get_user(self, user_id: int):11 """GET /users/:id"""12 user = self.user_service.get_user(user_id)13
14 if not user:15 return jsonify({"error": "User not found"}), 40416
17 return jsonify(user), 20018
19 def create_user(self):20 """POST /users"""21 data = request.get_json()22
23 # Validate input24 if not data or 'email' not in data:25 return jsonify({"error": "Email required"}), 40026
27 user = self.user_service.create_user(data)28 return jsonify(user), 201, {'Location': f'/users/{user["id"]}'}29
30 def update_user(self, user_id: int):31 """PUT /users/:id"""32 data = request.get_json()33
34 if not data:35 return jsonify({"error": "Request body required"}), 40036
37 user = self.user_service.update_user(user_id, data)38
39 if not user:40 return jsonify({"error": "User not found"}), 40441
42 return jsonify(user), 20043
44 def delete_user(self, user_id: int):45 """DELETE /users/:id"""46 success = self.user_service.delete_user(user_id)47
48 if not success:49 return jsonify({"error": "User not found"}), 40450
51 return '', 20452
53# Routes54@app.route('/users/<int:user_id>', methods=['GET', 'PUT', 'DELETE'])55def user_detail(user_id):56 controller = UserController(user_service)57
58 if request.method == 'GET':59 return controller.get_user(user_id)60 elif request.method == 'PUT':61 return controller.update_user(user_id)62 elif request.method == 'DELETE':63 return controller.delete_user(user_id)64
65@app.route('/users', methods=['POST'])66def user_create():67 controller = UserController(user_service)68 return controller.create_user()1import org.springframework.http.HttpStatus;2import org.springframework.http.ResponseEntity;3import org.springframework.web.bind.annotation.*;4
5@RestController6@RequestMapping("/users")7public class UserController {8 private final UserService userService;9
10 public UserController(UserService userService) {11 this.userService = userService;12 }13
14 @GetMapping("/{id}")15 public ResponseEntity<UserDTO> getUser(@PathVariable int id) {16 // GET /users/:id17 Optional<UserDTO> user = userService.getUser(id);18
19 if (user.isEmpty()) {20 return ResponseEntity.notFound().build();21 }22
23 return ResponseEntity.ok(user.get());24 }25
26 @PostMapping27 public ResponseEntity<UserDTO> createUser(@RequestBody CreateUserRequest request) {28 // POST /users29 UserDTO user = userService.createUser(request);30
31 return ResponseEntity32 .status(HttpStatus.CREATED)33 .header("Location", "/users/" + user.getId())34 .body(user);35 }36
37 @PutMapping("/{id}")38 public ResponseEntity<UserDTO> updateUser(39 @PathVariable int id,40 @RequestBody UpdateUserRequest request) {41 // PUT /users/:id42 Optional<UserDTO> user = userService.updateUser(id, request);43
44 if (user.isEmpty()) {45 return ResponseEntity.notFound().build();46 }47
48 return ResponseEntity.ok(user.get());49 }50
51 @DeleteMapping("/{id}")52 public ResponseEntity<Void> deleteUser(@PathVariable int id) {53 // DELETE /users/:id54 boolean deleted = userService.deleteUser(id);55
56 if (!deleted) {57 return ResponseEntity.notFound().build();58 }59
60 return ResponseEntity.noContent().build();61 }62}1from typing import Optional, Dict, Any2
3class UserService:4 def __init__(self, user_repository):5 self.user_repository = user_repository6
7 def get_user(self, user_id: int) -> Optional[Dict[str, Any]]:8 """Business logic for getting user"""9 user = self.user_repository.find_by_id(user_id)10
11 if not user:12 return None13
14 # Transform to DTO15 return {16 "id": user.id,17 "name": user.name,18 "email": user.email,19 "status": user.status20 }21
22 def create_user(self, data: Dict[str, Any]) -> Dict[str, Any]:23 """Business logic for creating user"""24 # Validate business rules25 if self.user_repository.find_by_email(data['email']):26 raise ValueError("Email already exists")27
28 # Create user29 user = self.user_repository.create(data)30
31 return {32 "id": user.id,33 "name": user.name,34 "email": user.email,35 "status": user.status36 }37
38 def update_user(self, user_id: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:39 """Business logic for updating user"""40 user = self.user_repository.find_by_id(user_id)41
42 if not user:43 return None44
45 # Update user46 updated_user = self.user_repository.update(user_id, data)47
48 return {49 "id": updated_user.id,50 "name": updated_user.name,51 "email": updated_user.email,52 "status": updated_user.status53 }54
55 def delete_user(self, user_id: int) -> bool:56 """Business logic for deleting user"""57 user = self.user_repository.find_by_id(user_id)58
59 if not user:60 return False61
62 self.user_repository.delete(user_id)63 return True1import java.util.Optional;2
3@Service4public class UserService {5 private final UserRepository userRepository;6
7 public UserService(UserRepository userRepository) {8 this.userRepository = userRepository;9 }10
11 public Optional<UserDTO> getUser(int id) {12 // Business logic for getting user13 return userRepository.findById(id)14 .map(this::toDTO);15 }16
17 public UserDTO createUser(CreateUserRequest request) {18 // Validate business rules19 if (userRepository.findByEmail(request.getEmail()).isPresent()) {20 throw new ConflictException("Email already exists");21 }22
23 // Create user24 User user = userRepository.save(toEntity(request));25 return toDTO(user);26 }27
28 public Optional<UserDTO> updateUser(int id, UpdateUserRequest request) {29 // Business logic for updating user30 return userRepository.findById(id)31 .map(user -> {32 user.update(request);33 return toDTO(userRepository.save(user));34 });35 }36
37 public boolean deleteUser(int id) {38 // Business logic for deleting user39 if (!userRepository.existsById(id)) {40 return false;41 }42
43 userRepository.deleteById(id);44 return true;45 }46
47 private UserDTO toDTO(User user) {48 return new UserDTO(49 user.getId(),50 user.getName(),51 user.getEmail(),52 user.getStatus()53 );54 }55}1from flask import jsonify2from werkzeug.exceptions import HTTPException3
4class APIError(Exception):5 def __init__(self, message, status_code=400):6 self.message = message7 self.status_code = status_code8
9@app.errorhandler(APIError)10def handle_api_error(error):11 return jsonify({12 "error": error.message,13 "status": error.status_code14 }), error.status_code15
16@app.errorhandler(404)17def handle_not_found(error):18 return jsonify({19 "error": "Resource not found",20 "status": 40421 }), 40422
23@app.errorhandler(500)24def handle_server_error(error):25 return jsonify({26 "error": "Internal server error",27 "status": 50028 }), 5001import org.springframework.http.ResponseEntity;2import org.springframework.web.bind.annotation.ExceptionHandler;3import org.springframework.web.bind.annotation.RestControllerAdvice;4
5@RestControllerAdvice6public class ErrorHandler {7
8 @ExceptionHandler(NotFoundException.class)9 public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) {10 return ResponseEntity11 .status(404)12 .body(new ErrorResponse("Resource not found", 404));13 }14
15 @ExceptionHandler(BadRequestException.class)16 public ResponseEntity<ErrorResponse> handleBadRequest(BadRequestException e) {17 return ResponseEntity18 .status(400)19 .body(new ErrorResponse(e.getMessage(), 400));20 }21
22 @ExceptionHandler(Exception.class)23 public ResponseEntity<ErrorResponse> handleGeneric(Exception e) {24 return ResponseEntity25 .status(500)26 .body(new ErrorResponse("Internal server error", 500));27 }28}Make POST requests idempotent using idempotency keys:
1POST /orders2Idempotency-Key: abc123-xyz7893Content-Type: application/json4
5{6 "productId": 456,7 "quantity": 28}Server behavior:
Support multiple formats:
1GET /users/1232Accept: application/json3
4GET /users/1235Accept: application/xmlLet clients choose fields:
1GET /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.