Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 27 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,78 +1,36 @@
![logo_ironhack_blue 7](https://user-images.githubusercontent.com/23629340/40541063-a07a0a8a-601a-11e8-91b5-2f13e4e6b441.png)
I am currently learning Backend course through IronHack and this was one of my projects:

# LAB | SpringBoot REST API
Built a fully functional Spring Boot REST API from scratch!

### Instructions
I focused on applying real-world backend development practices rather than just making it “work”. Aiming for clean architecture, validation, and proper API design.

1. Fork this repo.
2. Clone your fork to your local machine.
3. Solve the challenges.
🔧 Tech Stack:


## Deliverables
Java + Spring Boot,
Spring Web,
Spring Validation,
and Dev-Tools.

- Upon completion, add your solution to git.
- Then commit to git and push to your repo on GitHub.
- Make a pull request and paste the pull request link in the submission field in the Student Portal.
💡 Key Features Implemented:

## Tasks
Designed a Product & Customer management system
Applied input validation using annotations (e.g., constraints on name, price, email, age)
Built a service layer architecture with clean separation of concerns
Used constructor injection (no field injection) for better design
Implemented custom filtering logic (by category & price range)
Secured endpoints with a required API-Key header
Created a global exception handler to manage:
Validation errors
Missing API key
Resource not found
Invalid input scenarios
Returned proper HTTP status codes for all operations
Tested all endpoints using Postman

1. Create a Spring Boot application using Spring Initializr with the following dependencies:
- Spring Web
- Spring Boot DevTools
- Spring Boot Starter Validation
📌 Endpoints include:

2. Create a `Product` class with the following validated properties:
- name (not blank, min length 3)
- price (positive number)
- category (not blank)
- quantity (positive number)
Full CRUD operations for Products & Customers
Search by name/email
Filter by category and price range

3. Create a `ProductService` class that manages a List of Products and has methods to:
- Add a new product
- Get all products
- Get product by name
- Update product
- Delete product
- Get products by category
- Get products by price range

4. Create a `ProductController` class that:
- Uses constructor injection for the ProductService
- Requires an "API-Key" header for all requests (value: "123456")
- Has the following endpoints:
* POST `/products` - Create new product
* GET `/products` - Get all products
* GET `/products/{name}` - Get product by name
* PUT `/products/{name}` - Update product
* DELETE `/products/{name}` - Delete product
* GET `/products/category/{category}` - Get products by category
* GET `/products/price?min={min}&max={max}` - Get products by price range

5. Create a global exception handler that handles:
- Validation errors (return proper error messages)
- Missing API-Key header
- Product not found
- Invalid price range

6. Create a `Customer` class with the following validated properties:
- name (not blank)
- email (valid email format)
- age (minimum 18)
- address (not blank)

7. Create a `CustomerController` with endpoints to:
- Create new customer (with validation)
- Get all customers
- Get customer by email
- Update customer
- Delete customer

**Remember**:
- Use proper package structure
- Use constructor injection instead of @Autowired
- Test all endpoints using Postman
- Include appropriate error handling
- Use meaningful variable and method names
- Return appropriate HTTP status codes
- Include validation messages in responses
This project helped reinforce how important structured code, validation, and error handling are in building production-ready APIs.
104 changes: 104 additions & 0 deletions src/main/java/com/SpringRestAPI/Controllers/CustomerController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.SpringRestAPI.Controllers;

import com.SpringRestAPI.Exceptions.MissingApiKeyException;
import com.SpringRestAPI.Models.Customer;
import com.SpringRestAPI.Services.CustomerService;
import jakarta.validation.Valid;
// @Valid Annotations provide a consistent approach to validation, which improves readability and maintainability.
// It gives you automatic error feedback to the client, indicating exactly which fields are incorrect and why.
// @Valid is used for incoming data validation. Like 'Post' and 'Put' requests. It's not needed for others.
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.logging.Logger;

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

CustomerService customerService;

public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}

// create new customer
@PostMapping("/customer")
public Customer addNewCustomer(
@RequestHeader("API-Key") String apiKey,
@Valid @RequestBody Customer customer ) {
if (!"123456".equals(apiKey)) {
throw new MissingApiKeyException("API-Key header is missing or invalid");
}
Logger myLogger = Logger.getLogger("CustomerController new customer");
myLogger.info("Adding new customer ");

return customerService.addNewCustomer(
customer.getCustomerName(),
customer.getCustomerEmail(),
customer.getCustomerAge(),
customer.getCustomerAddress()
);
}

// get all customer
@GetMapping("/customer")
public List<Customer> getCustomers(
@RequestHeader("API-Key") String apiKey) {
if (!"123456".equals(apiKey)) {
throw new RuntimeException("Invalid API Key");
}

Logger myLogger = Logger.getLogger("CustomerService get all customers");
myLogger.info("Getting all customers");

return customerService.getCustomers();
}

// get customer by email
@GetMapping("/customer/{customerEmail}")
public List<Customer> getCustomers(
@RequestHeader("API-Key") String apiKey,
@PathVariable String customerEmail) {
if (!"123456".equals(apiKey)) {
throw new RuntimeException("Invalid API Key");
}

Logger myLogger = Logger.getLogger("CustomerService getting customer by email");
myLogger.info("Getting customer by email");

return customerService.getCustomerByEmail(customerEmail);
}

// update customer name
@PutMapping("/customer/{currentName}/{newName}")
public Customer updateCustomerName(
@RequestHeader("API-Key") String apiKey,
@Valid @PathVariable String currentName,
@PathVariable String newName) {
if (!"123456".equals(apiKey)) {
throw new RuntimeException("Invalid API Key");
}

Logger myLogger = Logger.getLogger("CustomerController update customer name");
myLogger.info("Updating customer name");

return customerService.updateCustomerName(currentName, newName);
}

// delete customer
@DeleteMapping("/customer/{customerName}")
public Customer deleteCustomer(
@RequestHeader("API-Key") String apiKey,
@PathVariable String customerName) {
if (!"123456".equals(apiKey)) {
throw new RuntimeException("Invalid API Key");
}

Logger myLogger = Logger.getLogger("CustomerController delete customer name");
myLogger.info("Deleting customer name");

return customerService.deleteCustomer(customerName)
.orElseThrow(() -> new RuntimeException("Customer not found"));
}

}
172 changes: 172 additions & 0 deletions src/main/java/com/SpringRestAPI/Controllers/ProductController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package com.SpringRestAPI.Controllers;

import com.SpringRestAPI.Exceptions.InvalidPriceRangeException;
import com.SpringRestAPI.Exceptions.MissingApiKeyException;
import com.SpringRestAPI.Exceptions.ProductNotFoundException;
import com.SpringRestAPI.Models.Product;
import com.SpringRestAPI.Models.ProductCategories;
import com.SpringRestAPI.Services.ProductService;
import jakarta.validation.Valid;
// @Valid Annotations provide a consistent approach to validation, which improves readability and maintainability.
// It gives you automatic error feedback to the client, indicating exactly which fields are incorrect and why.
// @Valid is used for incoming data validation. Like 'Post' and 'Put' requests. It's not needed for others.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.logging.Logger;

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


ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}

// Create new product
@PostMapping("/products") // http://localhost:8080/api/products/
public Product addNewProduct(
@RequestHeader("API-Key") String apiKey,
@Valid @RequestBody Product product ) {
if (!"123456".equals(apiKey)) {
throw new MissingApiKeyException("API-Key header is missing or invalid");
}
Logger myLogger = Logger.getLogger("ProductController");
myLogger.info("Create a new Product");

return productService.addNewProduct(
product.getProductName(),
product.getProductPrice(),
product.getProductCategory(),
product.getProductQuantity()
);
}

// get all products
@GetMapping("/products") // http://localhost:8080/api/products/
public List<Product> getAllProducts(@RequestHeader("API-Key") String apiKey) {
if (!"123456".equals(apiKey)) {
throw new MissingApiKeyException("API-Key header is missing or invalid");
}
Logger myLogger = Logger.getLogger("ProductList");
myLogger.info("Get all the Product list");

return productService.getProductList();
}

// Get product by name
@GetMapping("/products/{productName}")
public Product getProductByName(
@RequestHeader(value="API-Key") String apiKey,
@PathVariable String productName) {
if (!"123456".equals(apiKey)) {
throw new MissingApiKeyException("API-Key header is missing or invalid");
}
return productService.getProductByName(productName)
.orElseThrow(() -> new ProductNotFoundException("Product not found"));
}

// change product name
@PutMapping("/products/{productName}/{newName}") // http://localhost:8080/api/products/{name}/{newName}
public Product updateProductName(
@Valid @PathVariable String productName,
@PathVariable String newName,
@RequestHeader("API-Key") String apiKey) {
if (!"123456".equals(apiKey)) {
throw new MissingApiKeyException("API-Key header is missing or invalid");
}
Logger myLogger = Logger.getLogger(" by Name");
myLogger.info("change Product Name");

return productService.updateProductName(productName, newName);
}

// delete product by name
@DeleteMapping("/products/{productName}") // http://localhost:8080/api/products/{productName}
public Product deleteProduct(
@PathVariable String productName,
@RequestHeader("API-Key") String apiKey) {
if (!"123456".equals(apiKey)) {
throw new MissingApiKeyException("API-Key header is missing or invalid");
}
Logger myLogger = Logger.getLogger("ProductList by Name");
myLogger.info("delete Product by Name" + productName);

return productService.deleteProduct(productName)
.orElseThrow(() -> new ProductNotFoundException("Product not found"));
}

// get item by category
@GetMapping("/products/category/{category}") // http://localhost:8080/api/products/category/{category}
public List<Product> getProductByCategory(
@PathVariable ProductCategories category,
@RequestHeader("API-Key") String apiKey) {
if (!"123456".equals(apiKey)) {
throw new RuntimeException("Invalid API Key");
}
Logger myLogger = Logger.getLogger("ProductList by Category");
myLogger.info("get Product by Category");

return productService.getProductByCategory(category);
}

// get products by price range
@GetMapping("/products/price") // http://localhost:8080/api/products/price?min=10&max=30
public ResponseEntity<List<Product>> getProductsByPriceRange(
@RequestParam double min, // @RequestParam is /price > ?min=VALUE
@RequestParam double max, // &max=VALUE
@RequestHeader(value="API-Key") String apiKey) {
// Check API Key presence and validity
if (!"123456".equals(apiKey)) {
throw new MissingApiKeyException("API-Key header is missing or invalid");
}
// Check for valid price range
if (min > max) {
throw new InvalidPriceRangeException("Min price cannot be greater than max price");
}
// Business logic to find products by price range
List<Product> products = productService.getProductsByPriceRange(min, max);

if (products == null || products.isEmpty()) {
throw new ProductNotFoundException("No product found in this price range");
}
return ResponseEntity.ok(products);
}

/// IF YOU ARE READING THIS
/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW
/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW
/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW
/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW
/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW
/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW
/// MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW MEOW

/*
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⡷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⡿⠋⠈⠻⣮⣳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⡿⠋⠀⠀⠀⠀⠙⣿⣿⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣿⡿⠟⠛⠉⠀⠀⠀⠀⠀⠀⠀⠈⠛⠛⠿⠿⣿⣷⣶⣤⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⣾⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠻⠿⣿⣶⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣀⣠⣤⣤⣀⡀⠀⠀⣀⣴⣿⡿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⣿⣷⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣄⠀⠀
⢀⣤⣾⡿⠟⠛⠛⢿⣿⣶⣾⣿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⣿⣷⣦⣀⣀⣤⣶⣿⡿⠿⢿⣿⡀⠀
⣿⣿⠏⠀⢰⡆⠀⠀⠉⢿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⢿⡿⠟⠋⠁⠀⠀⢸⣿⠇⠀
⣿⡟⠀⣀⠈⣀⡀⠒⠃⠀⠙⣿⡆⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠇⠀
⣿⡇⠀⠛⢠⡋⢙⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀
⣿⣧⠀⠀⠀⠓⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠛⠋⠀⠀⢸⣧⣤⣤⣶⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⡿⠀⠀
⣿⣿⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠻⣷⣶⣶⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⠁⠀⠀
⠈⠛⠻⠿⢿⣿⣷⣶⣦⣤⣄⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⡏⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠉⠙⠛⠻⠿⢿⣿⣷⣶⣦⣤⣄⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠿⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⣿⡄⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠙⠛⠻⠿⢿⣿⣷⣶⣦⣤⣄⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣿⡄⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠛⠿⠿⣿⣷⣶⣶⣤⣤⣀⡀⠀⠀⠀⢀⣴⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⡿⣄
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠛⠿⠿⣿⣷⣶⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣹
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⠀⠀⠀⠀⠀⠀⢸⣧
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⣆⠀⠀⠀⠀⠀⠀⢀⣀⣠⣤⣶⣾⣿⣿⣿⣿⣤⣄⣀⡀⠀⠀⠀⣿
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⢿⣻⣷⣶⣾⣿⣿⡿⢯⣛⣛⡋⠁⠀⠀⠉⠙⠛⠛⠿⣿⣿⡷⣶⣿
*/

}
Loading