Spring REST Web Services – CRUD API for Products
REST (Representational State Transfer) web services are the backbone of modern web and mobile applications. Spring Boot makes building REST APIs straightforward with its @RestController, @RequestMapping, and Spring Data JPA. This tutorial builds a complete RESTful CRUD API for a product catalog — the kind of API that a mobile app or front-end framework like Angular or React would consume.
What You Will Build
| HTTP Method | URL | Operation |
|---|---|---|
| GET | /api/products | Get all products |
| GET | /api/products/{id} | Get product by ID |
| POST | /api/products | Add new product |
| PUT | /api/products/{id} | Update product |
| DELETE | /api/products/{id} | Delete product |
Maven Dependencies (Spring Boot)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/java9rdb?useSSL=false
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
Product.java – JPA Entity
package com.java9r.model;
import javax.persistence.*;
import javax.validation.constraints.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Column(nullable = false, length = 100)
private String name;
@Positive
private double price;
@Min(0)
private int quantity;
public Product() {}
public Product(String name, double price, int quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
// Getters and setters
public Long getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
public int getQuantity() { return quantity; }
public void setId(Long id) { this.id = id; }
public void setName(String n){ this.name = n; }
public void setPrice(double p){ this.price = p; }
public void setQuantity(int q){ this.quantity = q; }
}
ProductRepository.java – Spring Data JPA
package com.java9r.repository;
import com.java9r.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Spring Data JPA auto-generates CRUD methods
// Custom query methods using method name conventions:
List<Product> findByNameContainingIgnoreCase(String keyword);
List<Product> findByPriceLessThan(double maxPrice);
List<Product> findByOrderByPriceAsc();
}
ProductController.java – REST Controller
package com.java9r.controller;
import com.java9r.model.Product;
import com.java9r.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/products")
@CrossOrigin(origins = "*") // allow requests from any origin (Angular, React, etc.)
public class ProductController {
@Autowired
private ProductRepository repo;
// GET /api/products – list all
@GetMapping
public List<Product> getAllProducts() {
return repo.findAll();
}
// GET /api/products/{id} – find one
@GetMapping("/{id}")
public ResponseEntity<Product> getById(@PathVariable Long id) {
return repo.findById(id)
.map(p -> ResponseEntity.ok(p))
.orElse(ResponseEntity.notFound().build());
}
// POST /api/products – create
@PostMapping
public ResponseEntity<Product> create(@Valid @RequestBody Product product) {
Product saved = repo.save(product);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
// PUT /api/products/{id} – update
@PutMapping("/{id}")
public ResponseEntity<Product> update(@PathVariable Long id,
@Valid @RequestBody Product updated) {
return repo.findById(id).map(existing -> {
existing.setName(updated.getName());
existing.setPrice(updated.getPrice());
existing.setQuantity(updated.getQuantity());
return ResponseEntity.ok(repo.save(existing));
}).orElse(ResponseEntity.notFound().build());
}
// DELETE /api/products/{id}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
if (repo.existsById(id)) {
repo.deleteById(id);
return ResponseEntity.noContent().build(); // 204 No Content
}
return ResponseEntity.notFound().build(); // 404 Not Found
}
// GET /api/products/search?keyword=laptop
@GetMapping("/search")
public List<Product> search(@RequestParam String keyword) {
return repo.findByNameContainingIgnoreCase(keyword);
}
// GET /api/products/cheap?maxPrice=10000
@GetMapping("/cheap")
public List<Product> findCheap(@RequestParam double maxPrice) {
return repo.findByPriceLessThan(maxPrice);
}
}
Testing the API with curl
# Get all products
curl http://localhost:8080/api/products
# Get one product
curl http://localhost:8080/api/products/1
# Create a product
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Laptop","price":75000,"quantity":10}'
# Update a product
curl -X PUT http://localhost:8080/api/products/1 \
-H "Content-Type: application/json" \
-d '{"name":"Laptop Pro","price":85000,"quantity":8}'
# Delete a product
curl -X DELETE http://localhost:8080/api/products/3
# Search
curl "http://localhost:8080/api/products/search?keyword=laptop"
Sample JSON Responses
// GET /api/products
[
{"id":1,"name":"Laptop","price":75000.0,"quantity":10},
{"id":2,"name":"Smartphone","price":25000.0,"quantity":50},
{"id":3,"name":"Tablet","price":35000.0,"quantity":30}
]
// POST /api/products (201 Created)
{"id":4,"name":"Monitor","price":15000.0,"quantity":20}
// DELETE /api/products/3 (204 No Content – empty body)
// GET /api/products/99 (404 Not Found)
HTTP Status Codes Used
| Status | Meaning | Used For |
|---|---|---|
| 200 OK | Success | GET, PUT |
| 201 Created | Resource created | POST |
| 204 No Content | Success, no body | DELETE |
| 400 Bad Request | Validation failed | Invalid input |
| 404 Not Found | Resource not found | GET/PUT/DELETE by ID |
Summary
Spring Boot + Spring Data JPA makes building REST APIs remarkably concise. The JpaRepository interface provides all standard CRUD methods automatically, and custom query methods are generated from method names — no SQL needed for most use cases. The @RestController converts return values to JSON automatically. Always return appropriate HTTP status codes (201 for creates, 204 for deletes, 404 for missing resources) and validate input with Bean Validation annotations. This API is ready to be consumed by Angular, React, Vue.js, or mobile applications.
Comments