These notes were made while following Spring Academy’s Cash Card project — wherever you see “cash card,” you can substitute “item.”
@RestControllerclass CashCardController { @GetMapping("/cashcards/{requestedId}") private ResponseEntity<CashCard> findById(@PathVariable Long requestedId) { CashCard cashCard = /* Here would be the code to retrieve the CashCard */; return ResponseEntity.ok(cashCard); }}We annotate the class with @RestController to tell Spring this class handles REST
implementation. Then we annotate the method with @GetMapping to handle GET requests to the
specified endpoint, which takes requestedId as a path parameter via @PathVariable. We use
ResponseEntity to return a response body of CashCard, and ResponseEntity.ok to return a 200
status code.
GET request (by id)
For getting a specific resource by its id:
@GetMapping("/{requestedId}") private ResponseEntity<CashCard> findById(@PathVariable Long requestedId){ Optional<CashCard> cashCardOptional = cashCardRepository.findById(requestedId); if (cashCardOptional.isPresent()){ return ResponseEntity.ok(cashCardOptional.get()); } else { return ResponseEntity.notFound().build(); } }We try to find the resource with CrudRepository’s findById method, which returns an
Optional — the DB might or might not contain the cash card we’re looking for. We check if it’s
present: if so, we retrieve it with .get(); if not, we return a not-found response.
GET request (all items, with pagination and sorting)
- Fetching multiple items — when requesting multiple items from an API, it’s ideal to return a single response with a list of all items in JSON array format.
- Pagination — essential to avoid overwhelming API responses with a huge number of items. It involves specifying a page length and page index to retrieve subsets of data.
- Sorting — necessary for consistent pagination results. Sorting by a specific field organizes the data effectively, reducing cognitive overhead and potential errors.
- Spring Data Pagination API — Spring Data provides
PageRequestandSortfor implementing pagination and sorting conveniently. - Request URI composition:
- Second page:
/cashcards?page=1(page index starts at 0) - With a page size of 3:
/cashcards?page=1&size=3 - Sorted by amount, descending:
/cashcards?page=1&size=3&sort=amount,desc
- Second page:
- Java implementation — the controller method uses
Pageableto parse page number, size, and sort parameters, retrieves the items accordingly, and returns them in the response. - Page object — contains metadata about the current page (total elements, total pages, current page number, etc.), but for the client response it’s often enough to return just the content, without the metadata.
@GetMapping private ResponseEntity<List<CashCard>> findAll(Pageable pageable){ Page<CashCard> page = cashCardRepository.findAll( PageRequest.of( pageable.getPageNumber(), pageable.getPageSize(), pageable.getSortOr(Sort.by(Sort.Direction.DESC, "amount")))); return ResponseEntity.ok(page.getContent()); }The key parts:
private ResponseEntity<List<CashCard>> findAll(Pageable pageable)— takes aPageable, provided by Spring Data for pagination.cashCardRepository.findAll(...)— retrieves a page ofCashCardobjects based on the pagination and sorting parameters.ResponseEntity.ok(page.getContent())— builds a 200 response with the page’s content (the actual list ofCashCardobjects, not the surrounding page metadata).
GET request (all items, no pagination)
@GetMapping()private ResponseEntity<Iterable<CashCard>> findAll() { return ResponseEntity.ok(cashCardRepository.findAll());}Again using one of Spring Data’s built-in implementations: CrudRepository.findAll(). Our
CashCardRepository automatically returns all CashCard records from the database.
POST request
To implement a POST endpoint:
- Use the POST method.
- Define the endpoint URI.
- Accept a JSON request body (let the server generate the id).
- Respond with a 201 Created status code.
- Include a Location header with the URI of the newly created resource.
@PostMapping private ResponseEntity<Void> createCashCard(@RequestBody CashCard newCashCardRequest, UriComponentsBuilder ucb){ CashCard savedCashCard = cashCardRepository.save(newCashCardRequest); URI locationOfNewCashCard = ucb .path("cashcards/{id}") .buildAndExpand(savedCashCard.id()) .toUri(); return ResponseEntity.created(locationOfNewCashCard).build(); }CrudRepository.save() saves a new item for us, and returns the saved object with a unique id
from the database.
Unlike GET, POST expects a request body — data submitted to the API, which Spring Web
deserializes into the CashCard we want to create.
locationOfNewCashCard constructs the URI to the newly created item, which the caller can then
use to GET it. savedCashCard.id() is used as the identifier, matching the GET endpoint’s
cashcards/{CashCard.id} shape.
Where did UriComponentsBuilder come from? We added it as a method argument and it was
automatically injected — from Spring’s IoC container. Finally, ResponseEntity.created(...).build()
returns 201 Created with the correct Location header.
PUT request
- PUT vs. POST
- PUT — creates or replaces a resource where the client supplies the URI.
- POST — creates a sub-resource where the server generates and returns the URI.
- In the Cash Card API, PUT is not used for creating resources; POST is used for creating them.
- PUT for update — replaces the entire record with the object in the request body. Returns 204 No Content on success.
- Security — follows the same strategy as GET for unauthorized access and non-existent IDs, returning 404 Not Found in both cases.
- API decisions — PUT is not supported for creation; the update endpoint uses PUT, replaces existing cash cards, and returns 204 No Content on success. Unauthorized updates or updates to non-existent IDs return 404 Not Found.
@PutMapping("/{requestedId}") private ResponseEntity<Void> putCashCard(@PathVariable Long requestedId, @RequestBody CashCard cashCardUpdate, Principal principal) { CashCard cashCard = findCashCard(requestedId, principal); if (cashCard != null) { CashCard updatedCashCard = new CashCard(cashCard.id(), cashCardUpdate.amount(), principal.getName()); cashCardRepository.save(updatedCashCard); return ResponseEntity.noContent().build(); } return ResponseEntity.notFound().build(); }DELETE request
- Data specification
- Request:
DELETE /cashcards/{id}, empty body - Response: 204 No Content, empty body
- Request:
- Response codes
- 204 No Content — the record existed, the principal was authorized, and it was deleted.
- 404 Not Found — either the record doesn’t exist, or it exists but the principal isn’t the owner. Using 404 for both prevents leaking information about which IDs exist to unauthorized users.
- Additional options
- Hard delete — permanently removes the record.
- Soft delete — marks the record as “deleted” without removing it.
- Audit trail / archiving — capture historical information about modifications, including deletions; strategies include archiving deleted data, adding audit fields, or maintaining an audit log.
- Combinations — soft-delete then hard-delete/archive later; hard-delete with archiving; or just an audit log of operations.
- Specification vs. implementation — a 204 response with no body implies soft-delete isn’t happening here, but whether to hard/soft delete, add audit fields, or keep an audit trail depends on your specific requirements.
@DeleteMapping("/{id}") private ResponseEntity<Void> deleteCashCard(@PathVariable Long id, Principal principal) { if (cashCardRepository.existsByIdAndOwner(id, principal.getName())) { cashCardRepository.deleteById(id); return ResponseEntity.noContent().build(); } return ResponseEntity.notFound().build(); }