This post introduces REST APIs and their fundamental concepts. If you need a refresher on HTTP basics or web fundamentals, understanding those concepts will help you get the most out of this guide!
What is a REST API?
REST (Representational State Transfer) is an architectural style for designing networked applications. A REST API is a web service that follows REST principles, allowing different systems to communicate over HTTP in a standardized way.
Think of REST APIs as the universal language of the web. Just like how people from different countries can communicate through English as a common language, different applications, regardless of their programming language or platform, can communicate through REST APIs.
Why REST APIs Matter
In today’s interconnected world, applications rarely work in isolation. Consider these everyday scenarios:
- Your mobile banking app fetches account balance from a server
- A weather app displays real-time forecasts from a weather service
- An e-commerce site processes payments through a payment gateway
- A social media dashboard aggregates data from multiple platforms
All these interactions happen through REST APIs. As a software engineer, you’ll either be consuming APIs (using them in your application) or building APIs (creating them for others to use), or most likely, both!
Why REST Won: While there are other API styles (SOAP, GraphQL, gRPC), REST’s simplicity, scalability, and use of standard HTTP make it the most popular choice for web services.
The Six REST Principles
REST isn’t just about making HTTP requests, it’s a well-defined architectural style with specific principles. Understanding these will help you design better APIs.
1. Client-Server Architecture - Separation of concerns between UI and data.
The client (frontend) and server (backend) are independent and communicate through requests and responses. This separation allows them to evolve independently.
Example: A React frontend can be completely redesigned while the REST API remains unchanged. Similarly, the backend can be rewritten in a different language without affecting the frontend.
2. Stateless - Each request contains all necessary information.
The server doesn't store any client context between requests. Each request must contain all the information needed to understand and process it.
Why it matters: Statelessness makes APIs more scalable. Any server can handle any request without needing to know about previous requests from that client.
Example:
❌ Bad (Stateful):
Request 1: "Get user 123"
Request 2: "Update that user's email" // Which user?
✅ Good (Stateless):
Request 1: "Get user 123"
Request 2: "Update user 123's email" // Complete information
3. Cacheable - Responses must define themselves as cacheable or not.
REST APIs should explicitly indicate whether responses can be cached by the client. This improves performance by reducing unnecessary server requests.
Implementation: Using HTTP headers like Cache-Control, ETag, and Last-Modified.
4. Uniform Interface - Standardized way to interact with resources.
REST APIs follow consistent patterns for accessing resources:
- Resources are identified by URIs (e.g.,
/users/123) - Resources are manipulated through representations (JSON, XML)
- Messages are self-descriptive
- Hypermedia as the engine of application state (HATEOAS)
This uniformity makes APIs predictable and easy to understand.
5. Layered System - Client doesn't know if connected directly to server.
A REST API can have multiple layers (load balancers, caches, security layers) between client and server. The client doesn't need to know about these intermediaries.
Benefit: Enables scalability through load balancing and improves security through intermediary servers.
6. Code on Demand (Optional) - Server can extend client functionality.
Servers can send executable code to clients (e.g., JavaScript). This is the only optional constraint in REST.
Understanding HTTP Methods
REST APIs leverage standard HTTP methods to perform operations on resources. Each method has a specific purpose and expected behavior.
flowchart LR
subgraph Operations["CRUD Operations"]
C[Create]
R[Read]
U[Update]
D[Delete]
end
subgraph HTTP["HTTP Methods"]
POST[POST]
GET[GET]
PUT[PUT / PATCH]
DELETE[DELETE]
end
C --> POST
R --> GET
U --> PUT
D --> DELETE
style POST fill:#90EE90
style GET fill:#87CEEB
style PUT fill:#FFD700
style DELETE fill:#FFB6C1
HTTP Methods Comparison
| Method | Purpose | Request Body | Response Body | Idempotent | Safe |
|---|---|---|---|---|---|
| GET | Retrieve resource | ❌ No | ✅ Yes | ✅ Yes | ✅ Yes |
| POST | Create new resource | ✅ Yes | ✅ Yes | ❌ No | ❌ No |
| PUT | Replace entire resource | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No |
| PATCH | Partially update resource | ✅ Yes | ✅ Yes | ❌ No* | ❌ No |
| DELETE | Remove resource | ❌ No* | ✅ Yes | ✅ Yes | ❌ No |
Idempotent - Making the same request multiple times produces the same result.
An idempotent operation can be repeated without changing the outcome after the first application.
Examples:
GET /users/123- Always returns the same user (safe and idempotent)PUT /users/123- Setting the same data multiple times yields the same resultDELETE /users/123- First call deletes the user, subsequent calls still result in the user being deleted (404 or 204)POST /users- Creates a new user each time (NOT idempotent)
Safe - Operation doesn't modify resources on the server.
A safe method only retrieves information without causing side effects. Only GET, HEAD, and OPTIONS are considered safe methods.
Detailed Method Descriptions
GET - Retrieve Resources
The most common HTTP method. Used to read data without modifying it.
Examples:
GET /users- Get all usersGET /users/123- Get user with ID 123GET /users/123/orders- Get orders for user 123GET /products?category=electronics&sort=price- Get products with filters
Never use GET to modify data! Search engines and browsers can pre-fetch GET requests, potentially causing unintended modifications.
POST - Create New Resources
Used to create new resources. The server typically generates the resource ID.
Example: Creating a new user
1
2
3
4
5
6
7
POST /users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}
Response:
1
2
3
4
5
6
7
8
9
201 Created
Location: /users/124
{
"id": 124,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2026-01-04T10:30:00Z"
}
PUT - Replace Entire Resource
Used to completely replace a resource. If the resource doesn’t exist, it may create it.
Example: Replacing a user’s complete information
1
2
3
4
5
6
7
8
PUT /users/123
Content-Type: application/json
{
"name": "John Smith",
"email": "john.smith@example.com",
"phone": "+1234567890"
}
PUT requires the complete resource: Missing fields will be removed or set to default values. Use PATCH for partial updates.
PATCH - Partially Update Resource
Used to partially update a resource, modifying only specified fields.
Example: Updating only the email
1
2
3
4
5
6
PATCH /users/123
Content-Type: application/json
{
"email": "newemail@example.com"
}
PUT vs PATCH:
1
2
3
4
5
6
7
Current state: { "name": "John", "email": "john@example.com", "phone": "123" }
PUT /users/123 {"email": "new@example.com"}
Result: { "email": "new@example.com" } // name and phone removed!
PATCH /users/123 {"email": "new@example.com"}
Result: { "name": "John", "email": "new@example.com", "phone": "123" } // only email changed
DELETE - Remove Resources
Used to delete resources.
Examples:
1
2
DELETE /users/123 // Delete user 123
DELETE /users/123/avatar // Delete user's avatar
HTTP Status Codes
Status codes tell clients whether their request succeeded or failed, and why. They’re grouped into five categories:
| Category | Range | Meaning |
|---|---|---|
| 1xx | 100-199 | Informational (rarely used in REST) |
| 2xx | 200-299 | Success |
| 3xx | 300-399 | Redirection |
| 4xx | 400-499 | Client Error |
| 5xx | 500-599 | Server Error |
Essential Status Codes for REST APIs
2xx Success Codes - The request was successful.
| Code | Name | Usage |
|---|---|---|
| 200 | OK | Successful GET, PUT, or PATCH |
| 201 | Created | Successful POST that created a resource |
| 204 | No Content | Successful DELETE or update with no response body |
Example scenarios:
200 - Book data retrieved successfully or updated with new values
201 - New book created and assigned a generated ID
204 - Book deleted successfully, no content returned
4xx Client Error Codes - The client made an invalid request.
| Code | Name | Usage |
|---|---|---|
| 400 | Bad Request | Invalid request format or validation error |
| 401 | Unauthorized | Authentication required or failed |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Request conflicts with current state (e.g., duplicate) |
| 422 | Unprocessable Entity | Validation failed |
Common scenarios:
400 - Client sent invalid JSON or missing required fields for book
401 - No authentication token provided or token expired
403 - User authenticated but doesn't have permission to access books
404 - /api/books/999 when book 999 doesn't exist
409 - Creating book with ISBN that already exists
422 - Book validation failed (invalid publication year, empty title)
5xx Server Error Codes - Something went wrong on the server.
| Code | Name | Usage |
|---|---|---|
| 500 | Internal Server Error | Unhandled server exception |
| 502 | Bad Gateway | Invalid response from upstream server |
| 503 | Service Unavailable | Server temporarily unavailable |
Example scenarios:
500 - Database connection failed while fetching book
502 - Upstream book recommendation service returned invalid response
503 - Book service temporarily down for maintenance
Never expose error details in 5xx responses! They can reveal sensitive information about your system. Log the details server-side and return generic messages to clients.
Request and Response Structure
Understanding the anatomy of HTTP requests and responses is crucial for working with REST APIs.
sequenceDiagram
participant Client
participant Server
Note over Client: Prepare Request
Client->>Server: HTTP Request<br/>(Method, Headers, Body)
Note over Server: Process Request<br/>Validate, Execute, Format
Server->>Client: HTTP Response<br/>(Status Code, Headers, Body)
Note over Client: Process Response<br/>Handle Success/Error
Anatomy of an HTTP Request
1
2
3
4
5
6
7
8
9
10
11
POST /api/users HTTP/1.1 // Request Line: Method + URL + Version
Host: api.example.com // Headers
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: application/json
Content-Length: 62
{ // Request Body (for POST, PUT, PATCH)
"name": "John Doe",
"email": "john@example.com"
}
Key Components:
- Request Line: HTTP method, URI path, and HTTP version
- Headers: Metadata about the request (format, authentication, etc.)
- Body: Data being sent (optional, depends on method)
Anatomy of an HTTP Response
1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 201 Created // Status Line: Version + Status Code + Reason
Content-Type: application/json
Location: /api/users/124
Date: Fri, 04 Jan 2026 10:30:00 GMT
Content-Length: 134
{ // Response Body
"id": 124,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2026-01-04T10:30:00Z"
}
Key Components:
- Status Line: HTTP version, status code, and reason phrase
- Headers: Metadata about the response (content type, caching, etc.)
- Body: The actual data returned (optional for some status codes)
Common HTTP Headers
| Header | Type | Purpose | Example |
|---|---|---|---|
Content-Type | Both | Format of the body | application/json |
Accept | Request | Formats client accepts | application/json |
Authorization | Request | Authentication credentials | Bearer token123 |
Location | Response | URL of created/moved resource | /users/124 |
Cache-Control | Response | Caching directives | max-age=3600 |
Practical Java Examples with Spring Boot
Let’s build a complete REST API for managing a book library. We’ll cover all CRUD operations with proper status codes and error handling.
Project Setup
First, you’ll need a Spring Boot project with these dependencies:
1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
Pro Tip: Use Spring Initializr to generate your project with all necessary dependencies.
Domain Model
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.example.library.model;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;
import java.time.LocalDateTime;
public class Book {
private Long id;
@NotBlank(message = "Title is required")
private String title;
@NotBlank(message = "Author is required")
private String author;
@NotBlank(message = "ISBN is required")
private String isbn;
@NotNull(message = "Publication year is required")
@Min(value = 1000, message = "Invalid publication year")
private Integer publicationYear;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Book() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public Book(Long id, String title, String author, String isbn, Integer publicationYear) {
this();
this.id = id;
this.title = title;
this.author = author;
this.isbn = isbn;
this.publicationYear = publicationYear;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) {
this.title = title;
this.updatedAt = LocalDateTime.now();
}
public String getAuthor() { return author; }
public void setAuthor(String author) {
this.author = author;
this.updatedAt = LocalDateTime.now();
}
public String getIsbn() { return isbn; }
public void setIsbn(String isbn) {
this.isbn = isbn;
this.updatedAt = LocalDateTime.now();
}
public Integer getPublicationYear() { return publicationYear; }
public void setPublicationYear(Integer publicationYear) {
this.publicationYear = publicationYear;
this.updatedAt = LocalDateTime.now();
}
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}
REST Controller - Complete CRUD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package com.example.library.controller;
import com.example.library.model.Book;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@RestController
@RequestMapping("/api/books")
public class BookController {
// In-memory storage (use a database in production!)
private final Map<Long, Book> books = new ConcurrentHashMap<>();
private final AtomicLong idCounter = new AtomicLong();
@GetMapping
public ResponseEntity<List<Book>> getAllBooks(
@RequestParam(required = false) String author,
@RequestParam(required = false) Integer year) {
List<Book> result = books.values().stream()
.filter(book -> author == null || book.getAuthor().equalsIgnoreCase(author))
.filter(book -> year == null || book.getPublicationYear().equals(year))
.toList();
return ResponseEntity.ok(result); // 200 OK
}
@GetMapping("/{id}")
public ResponseEntity<Book> getBookById(@PathVariable Long id) {
Book book = books.get(id);
if (book == null) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
return ResponseEntity.ok(book); // 200 OK
}
@PostMapping
public ResponseEntity<Book> createBook(@Valid @RequestBody Book book) {
// Check for duplicate ISBN
boolean isDuplicate = books.values().stream()
.anyMatch(b -> b.getIsbn().equals(book.getIsbn()));
if (isDuplicate) {
return ResponseEntity.status(HttpStatus.CONFLICT).build(); // 409 Conflict
}
// Assign ID and save
Long id = idCounter.incrementAndGet();
book.setId(id);
books.put(id, book);
// Build Location header with URI of created resource
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(id)
.toUri();
return ResponseEntity.created(location).body(book); // 201 Created with Location header
}
@PutMapping("/{id}")
public ResponseEntity<Book> replaceBook(
@PathVariable Long id,
@Valid @RequestBody Book book) {
if (!books.containsKey(id)) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
// Set the ID and save (replacing the entire resource)
book.setId(id);
books.put(id, book);
return ResponseEntity.ok(book); // 200 OK
}
@PatchMapping("/{id}")
public ResponseEntity<Book> updateBook(
@PathVariable Long id,
@RequestBody Map<String, Object> updates) {
Book book = books.get(id);
if (book == null) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
// Apply partial updates
updates.forEach((key, value) -> {
switch (key) {
case "title" -> book.setTitle((String) value);
case "author" -> book.setAuthor((String) value);
case "isbn" -> book.setIsbn((String) value);
case "publicationYear" -> book.setPublicationYear((Integer) value);
}
});
return ResponseEntity.ok(book); // 200 OK
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
if (!books.containsKey(id)) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
books.remove(id);
return ResponseEntity.noContent().build(); // 204 No Content
}
}
Error Handling
Proper error handling is crucial for a good API. Here’s a global exception handler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.library.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice // Global exception handler for all controllers
public class GlobalExceptionHandler {
// Handle validation errors (from @Valid annotation)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationErrors(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
Map<String, Object> response = new HashMap<>();
response.put("timestamp", LocalDateTime.now());
response.put("status", HttpStatus.BAD_REQUEST.value());
response.put("errors", errors);
return ResponseEntity.badRequest().body(response); // 400 Bad Request
}
// Handle unexpected errors
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGenericError(Exception ex) {
Map<String, Object> response = new HashMap<>();
response.put("timestamp", LocalDateTime.now());
response.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
response.put("message", "An unexpected error occurred");
// Log the actual error for debugging (never expose to client!)
System.err.println("Error: " + ex.getMessage());
ex.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response); // 500 Internal Server Error
}
}
Testing the API
You can test your API using curl, Postman, or any HTTP client:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Create a book
curl -X POST http://localhost:8080/api/books \
-H "Content-Type: application/json" \
-d '{
"title": "Clean Code",
"author": "Robert C. Martin",
"isbn": "978-0132350884",
"publicationYear": 2008
}'
# Get all books
curl http://localhost:8080/api/books
# Get a specific book
curl http://localhost:8080/api/books/1
# Partially update a book
curl -X PATCH http://localhost:8080/api/books/1 \
-H "Content-Type: application/json" \
-d '{"title": "Clean Code: A Handbook of Agile Software Craftsmanship"}'
# Delete a book
curl -X DELETE http://localhost:8080/api/books/1
API Design Best Practices
Following these best practices will make your APIs more intuitive, maintainable, and developer-friendly.
1. Use Nouns for Resources, Not Verbs
The HTTP method already indicates the action, keep URLs focused on resources.
1
2
3
4
5
6
7
8
9
10
11
12
✅ Good:
GET /users // Get all users
POST /users // Create a user
GET /users/123 // Get user 123
PUT /users/123 // Update user 123
DELETE /users/123 // Delete user 123
❌ Bad:
GET /getUsers
POST /createUser
GET /user/get/123
POST /deleteUser/123
2. Use Plural Nouns for Collections
1
2
3
4
5
6
7
8
9
✅ Good:
/users
/books
/orders
❌ Bad:
/user
/book
/order
3. Use Hierarchical Structure for Relationships
1
2
3
4
5
6
7
8
✅ Good:
GET /users/123/orders // Get orders for user 123
GET /users/123/orders/456 // Get order 456 for user 123
POST /users/123/orders // Create order for user 123
❌ Bad:
GET /getUserOrders?userId=123
GET /orders?userId=123&orderId=456
4. Version Your API
Always version your API from the start to allow backwards-compatible changes.
1
2
3
4
5
@RestController
@RequestMapping("/api/v1/books") // Version in URL
public class BookController {
// ...
}
Common versioning strategies:
- URL path:
/api/v1/books - Query parameter:
/api/books?version=1 - Header:
Accept: application/vnd.myapi.v1+json
Start with v1: Even if you think your API won’t change, version it from day one. Future you will be grateful!
5. Use Consistent Naming Conventions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
✅ Good (camelCase in JSON):
{
"firstName": "John",
"lastName": "Doe",
"emailAddress": "john@example.com"
}
✅ Also acceptable (snake_case):
{
"first_name": "John",
"last_name": "Doe",
"email_address": "john@example.com"
}
❌ Inconsistent:
{
"firstName": "John",
"last_name": "Doe",
"email-address": "john@example.com"
}
6. Provide Meaningful Error Messages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ Bad: Generic error
{
"error": "Bad request"
}
// ✅ Good: Specific, actionable error
{
"timestamp": "2026-01-04T10:30:00Z",
"status": 400,
"error": "Validation Failed",
"message": "Invalid input data",
"errors": {
"email": "Email address is not valid",
"age": "Must be at least 18"
}
}
7. Use Query Parameters for Filtering, Sorting, and Pagination
1
2
3
4
GET /books?author=Martin&year=2008 // Filtering
GET /books?sort=title&order=asc // Sorting
GET /books?page=2&size=20 // Pagination
GET /books?author=Martin&sort=year&page=1 // Combined
8. Support HATEOAS (Hypermedia)
Include links to related resources in responses (advanced topic, but good to know):
1
2
3
4
5
6
7
8
9
10
{
"id": 123,
"title": "Clean Code",
"author": "Robert C. Martin",
"_links": {
"self": {"href": "/api/books/123"},
"author": {"href": "/api/authors/456"},
"reviews": {"href": "/api/books/123/reviews"}
}
}
Common Pitfalls for Beginners
1. Using Wrong HTTP Methods
1
2
3
4
5
❌ Using GET for operations that modify data:
GET /deleteUser/123
✅ Use DELETE:
DELETE /users/123
Remember: GET requests should be safe and idempotent. They should never modify data!
2. Not Using Proper Status Codes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ❌ Always returning 200, even for errors
@PostMapping("/users")
public ResponseEntity<Map<String, Object>> createUser(@RequestBody User user) {
Map<String, Object> response = new HashMap<>();
if (userExists(user.getEmail())) {
response.put("error", "User already exists");
return ResponseEntity.ok(response); // Wrong! This is 200
}
// ...
}
// ✅ Use appropriate status codes
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
if (userExists(user.getEmail())) {
return ResponseEntity.status(HttpStatus.CONFLICT).build(); // 409
}
// ...
return ResponseEntity.status(HttpStatus.CREATED).body(newUser); // 201
}
3. Ignoring Validation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ No validation
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
// What if user is null? What if email is invalid?
userRepository.save(user);
return ResponseEntity.ok(user);
}
// ✅ Proper validation
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
// @Valid triggers validation, exception handler catches errors
User newUser = userRepository.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body(newUser);
}
4. Exposing Internal Details in Errors
1
2
3
4
5
6
7
8
9
10
11
12
// ❌ Exposing stack traces and internal details
catch (Exception e) {
return ResponseEntity.status(500)
.body("Error: " + e.getMessage() + "\n" + e.getStackTrace());
}
// ✅ Generic error message, log details internally
catch (Exception e) {
logger.error("Error creating user", e);
return ResponseEntity.status(500)
.body(Map.of("error", "An unexpected error occurred"));
}
Never expose error details in production! They can reveal sensitive information about your database, file structure, or dependencies.
5. Not Handling Content Negotiation
1
2
3
4
5
6
// ✅ Always specify Content-Type in responses
@GetMapping(value = "/users/{id}", produces = "application/json")
public ResponseEntity<User> getUser(@PathVariable Long id) {
// Spring Boot handles this automatically, but be aware
return ResponseEntity.ok(user);
}
6. Forgetting About CORS for Frontend Apps
If your frontend runs on a different domain/port, you’ll need CORS configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000") // Frontend URL
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowedHeaders("*");
}
};
}
}
Complete API Flow Visualization
Let’s visualize a complete request-response cycle for creating a book:
sequenceDiagram
participant Client as Client Application
participant Controller as BookController
participant Validator as Validation Layer
participant Service as Business Logic
participant Storage as Data Storage
Client->>Controller: POST /api/books<br/>{title, author, isbn, year}
Controller->>Validator: @Valid validates request
alt Validation Fails
Validator-->>Controller: MethodArgumentNotValidException
Controller-->>Client: 400 Bad Request<br/>{field errors}
else Validation Succeeds
Validator-->>Controller: OK
Controller->>Service: Check for duplicate ISBN
Service->>Storage: Query by ISBN
Storage-->>Service: Results
alt ISBN Already Exists
Service-->>Controller: Duplicate found
Controller-->>Client: 409 Conflict
else ISBN is Unique
Service-->>Controller: No duplicate
Controller->>Storage: Save new book
Storage-->>Controller: Book saved (with ID)
Controller->>Controller: Build Location header
Controller-->>Client: 201 Created<br/>Location: /api/books/123<br/>{book with id}
end
end
Summary and Next Steps
Congratulations! You’ve learned the fundamentals of REST APIs:
✅ REST Principles: Client-server, stateless, cacheable, uniform interface, layered system
✅ HTTP Methods: GET, POST, PUT, PATCH, DELETE and when to use each
✅ Status Codes: 2xx success, 4xx client errors, 5xx server errors
✅ Request/Response Structure: Headers, body, and proper formatting
✅ Practical Implementation: Building REST APIs with Spring Boot
✅ Best Practices: Resource naming, versioning, error handling
✅ Common Pitfalls: What to avoid as a beginner
Your Next Steps
Build Your Own API: Create a simple REST API for a domain you’re interested in (e.g., task manager, recipe book, movie database)
Add Authentication: Learn about JWT tokens and secure your endpoints
Connect to a Database: Replace in-memory storage with JPA and a real database
Write Tests: Learn to write unit and integration tests for your API
Explore API Documentation: Learn about OpenAPI/Swagger for automatic API documentation
Study Real APIs: Examine popular APIs (GitHub API, Twitter API) to see professional implementations
Practice Makes Perfect: The best way to learn REST APIs is by building them. Start small, make mistakes, and iterate!
Additional Resources
- RESTful API Design Best Practices
- HTTP Status Codes Reference
- Spring Boot REST API Tutorial
- Postman Learning Center - Great tool for testing APIs
Keep building, keep learning, and remember: every expert was once a beginner who didn’t give up! 🚀
