Spring Boot Annotations in RESTful Web Services

Satyendra Jaiswal
9 min read3 days ago

--

Spring Boot is a powerful framework that streamlines the development of Java applications, particularly RESTful web services. One of its key strengths lies in its extensive use of annotations, which simplify configuration and enhance readability. In this article, we’ll explore some essential Spring Boot annotations used in developing RESTful web services and provide code examples to illustrate their use.

Understanding Spring Boot Annotations

Annotations in Spring Boot serve as metadata, providing information to the compiler about the behavior of code. They are used to configure Spring Beans, inject dependencies, and manage various aspects of the application. Here are some of the most commonly used annotations in RESTful web services.

1. @RestController

The @RestController annotation is a specialized version of the @Controller annotation. It indicates that the class is a web controller and that its return values should be bound to the web response body.

Example:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class HelloController {

@GetMapping("/hello")
public String sayHello() {
return "Hello, World!";
}
}

2. @RequestMapping

The @RequestMapping annotation is used to map web requests to specific handler methods or classes. It can be applied at both class and method levels.

Example:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class UserController {

@RequestMapping("/users")
public List<String> getUsers() {
return Arrays.asList("User1", "User2", "User3");
}
}

3. @GetMapping, @PostMapping, @PutMapping, @DeleteMapping

These annotations are shortcuts for @RequestMapping with specific HTTP methods. They enhance readability and are used for GET, POST, PUT, and DELETE requests, respectively.

Example:

import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api")
public class ProductController {

private List<String> products = new ArrayList<>();

@GetMapping("/products")
public List<String> getProducts() {
return products;
}

@PostMapping("/products")
public void addProduct(@RequestBody String product) {
products.add(product);
}

@PutMapping("/products/{index}")
public void updateProduct(@PathVariable int index, @RequestBody String product) {
products.set(index, product);
}

@DeleteMapping("/products/{index}")
public void deleteProduct(@PathVariable int index) {
products.remove(index);
}
}

4. @PathVariable

The @PathVariable annotation is used to extract values from the URI path. These are typically part of the URL itself and are defined within the URL structure.

Example:

If the URL is http://example.com/api/orders/123, the path variable id can be extracted using @PathVariable.

Example:

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class OrderController {

@GetMapping("/orders/{id}")
public String getOrder(@PathVariable String id) {
return "Order ID: " + id;
}
}

In this example, if a request is made to /api/orders/123, the method will extract the value of the id path variable and return "Order ID: 123".

5. @RequestParam

The @RequestParam annotation is used to extract query parameters from the URL.These are the parameters that come after the question mark (?) in the URL and are usually in the form of key-value pairs separated by an ampersand (&).

Example:

If the URL is http://example.com/api/customers?name=John, the query parameter name can be extracted using @RequestParam.

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class CustomerController {

@GetMapping("/customers")
public String getCustomer(@RequestParam String name) {
return "Customer Name: " + name;
}
}

In this example, if a request is made to /api/customers?name=John, the method will extract the value of the name query parameter and return "Customer Name: John".

6. @RequestBody

The @RequestBody annotation is used to map the HTTP request body directly to a Java object. This is typically used in POST and PUT requests where you need to send data to the server in the request body.

Example Scenario:

Suppose we have an API that allows users to create and update orders. The order details are sent as JSON in the request body.

Order Class:

First, let’s define a simple Order class to represent the order data.

public class Order {
private String id;
private String product;
private int quantity;

// Getters and Setters
public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getProduct() {
return product;
}

public void setProduct(String product) {
this.product = product;
}

public int getQuantity() {
return quantity;
}

public void setQuantity(int quantity) {
this.quantity = quantity;
}
}

OrderController:

Now, let’s create a controller that handles creating and updating orders using @RequestBody.

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

// POST endpoint to create an order
@PostMapping
public String createOrder(@RequestBody Order order) {
// Here, we would save the order to the database (not implemented in this example)
return "Order created: " + order.getProduct() + " x " + order.getQuantity();
}

// PUT endpoint to update an order
@PutMapping("/{id}")
public String updateOrder(@PathVariable String id, @RequestBody Order order) {
// Here, we would update the order in the database (not implemented in this example)
return "Order updated: ID " + id + ", Product " + order.getProduct() + " x " + order.getQuantity();
}
}

7. @ResponseStatus

The @ResponseStatus annotation is used to set the HTTP status code for a method’s response.

Example:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class ExceptionController {

@GetMapping("/error")
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleError() {
return "Resource not found";
}
}

8. @ExceptionHandler

The @ExceptionHandler annotation is used to define methods that handle specific exceptions thrown by controller methods.By using @ExceptionHandler, you can define a method to handle specific exceptions and return custom responses to the client.

Scenario: Building an Order Management System

Let’s create an example of an order management system where we handle exceptions such as OrderNotFoundException and InvalidOrderException.

Step 1: Define Custom Exceptions

First, we define our custom exceptions:

// OrderNotFoundException.java
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(String message) {
super(message);
}
}

// InvalidOrderException.java
public class InvalidOrderException extends RuntimeException {
public InvalidOrderException(String message) {
super(message);
}
}

Step 2: Define the Order Entity

Next, we define a simple Order entity:

// Order.java
public class Order {
private String id;
private String product;
private int quantity;

// Getters and Setters
public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getProduct() {
return product;
}

public void setProduct(String product) {
this.product = product;
}

public int getQuantity() {
return quantity;
}

public void setQuantity(int quantity) {
this.quantity = quantity;
}
}

Step 3: Define the Order Controller

Now, we create a controller with endpoints for handling orders and a method for handling exceptions.

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

// Mock database
private Map<String, Order> orderDatabase = new HashMap<>();

@GetMapping("/{id}")
public Order getOrder(@PathVariable String id) {
Order order = orderDatabase.get(id);
if (order == null) {
throw new OrderNotFoundException("Order not found with ID: " + id);
}
return order;
}

@PostMapping
public String createOrder(@RequestBody Order order) {
if (order.getProduct() == null || order.getProduct().isEmpty()) {
throw new InvalidOrderException("Product name is required.");
}
orderDatabase.put(order.getId(), order);
return "Order created: " + order.getProduct() + " x " + order.getQuantity();
}

// Exception handler for OrderNotFoundException
@ExceptionHandler(OrderNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, String> handleOrderNotFoundException(OrderNotFoundException ex) {
Map<String, String> response = new HashMap<>();
response.put("error", ex.getMessage());
return response;
}

// Exception handler for InvalidOrderException
@ExceptionHandler(InvalidOrderException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleInvalidOrderException(InvalidOrderException ex) {
Map<String, String> response = new HashMap<>();
response.put("error", ex.getMessage());
return response;
}
}

Step 4: Example Requests and Responses

Example 1: Retrieve an Order (GET request)

Request URL:

GET http://example.com/api/orders/123

Response (if order not found):

{
"error": "Order not found with ID: 123"
}

In this case, the OrderNotFoundException is thrown, and the handleOrderNotFoundException method handles it, returning a 404 Not Found status with a custom error message.

Example 2: Create an Order (POST request)

Request URL:

POST http://example.com/api/orders

Request Body:

{
"id": "1",
"product": "",
"quantity": 5
}

Response (if product name is missing):

{
"error": "Product name is required."
}

In this case, the InvalidOrderException is thrown, and the handleInvalidOrderException method handles it, returning a 400 Bad Request status with a custom error message.

9. @CrossOrigin

The @CrossOrigin annotation is used to handle Cross-Origin Resource Sharing (CORS) at the controller level. It enables the server to indicate which origins are permitted to access resources.

Cross-Origin Resource Sharing (CORS) Flow

CORS is a security feature implemented by web browsers to restrict web pages from making requests to a different domain than the one that served the web page. This is done to prevent malicious websites from making unauthorized requests to your server.

CORS Flow with @CrossOrigin

  1. Preflight Request:
  • When a web page from one domain (http://example.com) tries to make an API call to another domain (http://api.example.com), the browser first sends an HTTP OPTIONS request to the target server.
  • This preflight request checks if the server allows the actual request method (e.g., GET, POST) and headers that will be sent.

2. Server Response:

  • If the server is configured to allow cross-origin requests (using @CrossOrigin in Spring Boot), it responds to the preflight request with the appropriate headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, etc.).
  • The browser then sends the actual request if the preflight request is successful.

3. Actual Request:

  • The browser sends the actual request (e.g., GET, POST) to the server.
  • The server processes the request and sends back the response with the necessary CORS headers.

Example Controller with @CrossOrigin:

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/orders")
@CrossOrigin(origins = "http://example.com") // Enable CORS for this domain
public class OrderController {

@GetMapping("/{id}")
public Order getOrder(@PathVariable String id) {
// Mock order for demonstration
Order order = new Order();
order.setId(id);
order.setProduct("Laptop");
order.setQuantity(2);
return order;
}

@PostMapping
public String createOrder(@RequestBody Order order) {
return "Order created: " + order.getProduct() + " x " + order.getQuantity();
}
}

Client Example Using JavaScript (e.g., in a web browser)

HTML and JavaScript Client

Here’s a simple HTML file with embedded JavaScript that makes a cross-origin request to the OrderController:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Order Client</title>
<script>
async function getOrder() {
const orderId = document.getElementById('orderId').value;
const response = await fetch(`http://api.example.com/api/orders/${orderId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const order = await response.json();
document.getElementById('orderDetails').innerText = JSON.stringify(order, null, 2);
}

async function createOrder() {
const order = {
id: document.getElementById('newOrderId').value,
product: document.getElementById('product').value,
quantity: parseInt(document.getElementById('quantity').value)
};
const response = await fetch('http://api.example.com/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(order)
});
const result = await response.text();
document.getElementById('creationResult').innerText = result;
}
</script>
</head>
<body>
<h1>Order Client</h1>
<h2>Get Order</h2>
<input type="text" id="orderId" placeholder="Order ID">
<button onclick="getOrder()">Get Order</button>
<pre id="orderDetails"></pre>

<h2>Create Order</h2>
<input type="text" id="newOrderId" placeholder="Order ID">
<input type="text" id="product" placeholder="Product">
<input type="number" id="quantity" placeholder="Quantity">
<button onclick="createOrder()">Create Order</button>
<pre id="creationResult"></pre>
</body>
</html>

Explanation

  1. HTML Structure:
  • The HTML contains two sections: one for retrieving an order and one for creating an order.
  • Each section has input fields and buttons to trigger the respective functions.

2. JavaScript Functions:

  • getOrder: This function reads the order ID from an input field and sends a GET request to the API endpoint to retrieve the order details. It then displays the response in a <pre> element.
  • createOrder: This function reads the order details from input fields and sends a POST request to the API endpoint to create a new order. It then displays the server's response in a <pre> element.

3. CORS Configuration:

  • The OrderController has @CrossOrigin(origins = "http://example.com"), which allows requests from http://example.com. In this example, assume that http://example.com is where the client HTML is served from.

Running the Client

  1. Serve the HTML File:
  • Host the HTML file on a server running at http://example.com. This could be a simple local server for testing, such as using http-server for Node.js or any other web server.

2. Run the Spring Boot Application:

3. Access the Client in a Browser:

  • Open the HTML file served from http://example.com in a web browser.
  • Enter an order ID in the “Get Order” section and click the “Get Order” button to fetch the order details.
  • Fill in the order details in the “Create Order” section and click the “Create Order” button to create a new order.

Issues and Benefits

Without @CrossOrigin

Issues:

  1. Blocked Requests:
  • Browsers will block cross-origin requests by default. This means that if you try to make a request from a web application hosted on a different domain, it will fail.
  • This can be a significant issue if your frontend and backend are hosted on different domains, which is common in microservices architectures.

2. Development Friction:

  • Developers may face difficulties when trying to test or develop features that require cross-origin requests.
  • It can lead to increased complexity in development environments.

Benefits:

  1. Enhanced Security:
  • Blocking cross-origin requests by default can prevent unauthorized access and reduce the risk of CSRF (Cross-Site Request Forgery) attacks.
  • This ensures that only the same-origin requests are processed, maintaining a higher level of security.

With @CrossOrigin

Issues:

  1. Potential Security Risks:
  • Allowing cross-origin requests can expose your server to potential security vulnerabilities if not properly managed.
  • It’s crucial to restrict allowed origins to trusted domains to mitigate risks.

Benefits:

  1. Improved Flexibility:
  • Enabling CORS allows your web application to interact with resources from different domains, facilitating the integration of microservices and external APIs.
  • This is especially useful in modern web development where frontend and backend services are often decoupled and hosted separately.

2. Enhanced User Experience:

  • Users can seamlessly access resources from different domains, improving the overall functionality and usability of your application.
  • For instance, a single-page application (SPA) can make AJAX requests to a backend API hosted on a different domain without issues.

3. Simplified Development:

  • Developers can easily test and develop features that require cross-origin requests, reducing the friction and complexity in the development process.
  • It allows for a smoother workflow when working with frontend-backend integration.

Conclusion

Spring Boot’s annotations are powerful tools that greatly simplify the development of RESTful web services. By using annotations such as @RestController, @RequestMapping, @PathVariable, @RequestBody, and @CrossOrigin, developers can create clean, readable, and maintainable code. Understanding and effectively using these annotations can significantly enhance your productivity and the quality of your Spring Boot applications.

Feel free to explore more annotations and experiment with different configurations to see how Spring Boot can best meet your application needs. Happy coding!

--

--