A Full CRUD API with JPA
Put it all together: create, read, update and delete real database records through a clean set of REST endpoints.
What you will learn
- Build all four CRUD endpoints
- Map each CRUD action to the right HTTP method
- Handle a missing record
CRUD = the four basic actions
Almost every app does CRUD: Create, Read, Update, Delete. In a REST API each maps to an HTTP method (HTTP — HyperText Transfer Protocol — is the set of rules browsers and servers use to talk; its “methods” like GET and POST say what kind of action you want) on the same /products resource:
| Action | HTTP method | URL | Repository call |
|---|---|---|---|
| Create | POST | /products | save(p) |
| Read all | GET | /products | findAll() |
| Read one | GET | /products/{id} | findById(id) |
| Update | PUT | /products/{id} | save(p) |
| Delete | DELETE | /products/{id} | deleteById(id) |
The full CRUD flow, in order
Picture using the finished API to manage one product from start to finish. Each step is one request, and each request runs one method in the controller below:
- Create: send
POST /productswith a JSON body. The controller callssave(p)and the database gives the new row anid. - Read all: send
GET /products. The controller callsfindAll()and returns every row as a JSON array. - Read one: send
GET /products/1. The controller reads the{id}from the URL and callsfindById(1)to return just that row. - Update: send
PUT /products/1with new JSON. The controller sets the id from the URL and callssave(p), which overwrites that row. - Delete: send
DELETE /products/1. The controller callsdeleteById(1)and the row is gone.
The full controller
@RestController
@RequestMapping("/products") // every URL below starts with /products
public class ProductController {
private final ProductRepository repo;
public ProductController(ProductRepository repo) {
this.repo = repo;
}
@GetMapping // GET /products
public List<Product> all() {
return repo.findAll();
}
@GetMapping("/{id}") // GET /products/5
public Product one(@PathVariable Long id) {
return repo.findById(id).orElseThrow();
}
@PostMapping // POST /products
public Product create(@RequestBody Product p) {
return repo.save(p);
}
@PutMapping("/{id}") // PUT /products/5
public Product update(@PathVariable Long id, @RequestBody Product p) {
p.setId(id); // make sure we update the right row
return repo.save(p);
}
@DeleteMapping("/{id}") // DELETE /products/5
public String delete(@PathVariable Long id) {
repo.deleteById(id);
return "Deleted product " + id;
}
}Note: Output: POST /products {"name":"Keyboard","price":799} -> {"id":1,"name":"Keyboard","price":799.0} GET /products -> [{"id":1,"name":"Keyboard","price":799.0}] PUT /products/1 {"name":"Keyboard Pro","price":999} -> {"id":1,"name":"Keyboard Pro","price":999.0} DELETE /products/1 -> Deleted product 1 Five endpoints give complete control over the data — all backed by the free repository methods.
One nice helper
Notice @RequestMapping("/products") on the class. It sets a base path so every method’s URL starts with /products — you do not repeat it on each method.
Also notice findById(id).orElseThrow(). findById returns an Optional (it might be empty if the id does not exist). orElseThrow() turns an empty result into an error instead of a confusing null. Cleaner error handling comes in the next unit.
Tip: PUT vs POST: POST creates a new item (no id yet); PUT updates an existing item at a known id. Following these conventions makes your API predictable for anyone using it.
Watch out: In update, setting the id from the path (p.setId(id)) is important. Without it, save could create a brand-new row instead of updating the one you meant.
Q. In a REST API, which HTTP method is normally used to delete a record?
✍️ Practice
- Build the full five-endpoint CRUD API for a
Bookentity. - Test create, read all, read one, update and delete with Postman or curl.
🏠 Homework
- Build complete CRUD for a
Taskentity (id, title, done) and test every endpoint.