Spring Boot CRUD API
CRUD stands for Create, Read, Update, and Delete. These four actions cover nearly everything an application does with stored data. A CRUD API exposes the four actions as web addresses so that mobile apps, websites, and other systems can manage data over HTTP.
Building a complete CRUD API combines several earlier ideas: controllers, services, repositories, and entities. The result is a working backend that stores real records.
The Library Desk Idea
A librarian performs four jobs. Registering a new book is Create. Finding a book for a visitor is Read. Correcting a wrong title is Update. Removing a damaged book from the shelf is Delete. A CRUD API gives software the same four jobs.
Library job HTTP method Address --------------------------------------------------- Register new book --> POST --> /api/books Find books --> GET --> /api/books Find one book --> GET --> /api/books/5 Correct a book --> PUT --> /api/books/5 Remove a book --> DELETE --> /api/books/5
Layers of the API
Each layer owns one responsibility. Requests travel downward and responses travel back upward.
Client | Controller (receives HTTP request) | Service (applies business rules) | Repository (reads and writes data) | Database
Step 1: The Entity
The entity describes one row of the books table.
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private double price;
// getters and setters
}Step 2: The Repository
An interface that extends JpaRepository delivers save, find, and delete methods automatically.
public interface BookRepository extends JpaRepository<Book, Long> {
}Step 3: The Service
The service holds the logic and keeps controllers thin.
@Service
public class BookService {
private final BookRepository repository;
public BookService(BookRepository repository) {
this.repository = repository;
}
public Book create(Book book) {
return repository.save(book);
}
public List<Book> findAll() {
return repository.findAll();
}
public Optional<Book> findById(Long id) {
return repository.findById(id);
}
public Optional<Book> update(Long id, Book data) {
return repository.findById(id).map(book -> {
book.setTitle(data.getTitle());
book.setAuthor(data.getAuthor());
book.setPrice(data.getPrice());
return repository.save(book);
});
}
public boolean delete(Long id) {
if (!repository.existsById(id)) {
return false;
}
repository.deleteById(id);
return true;
}
}Step 4: The Controller
The controller maps each HTTP method to a service call and chooses a fitting status code.
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService service;
public BookController(BookService service) {
this.service = service;
}
@PostMapping
public ResponseEntity<Book> create(@RequestBody Book book) {
return ResponseEntity.status(HttpStatus.CREATED).body(service.create(book));
}
@GetMapping
public List<Book> findAll() {
return service.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Book> findOne(@PathVariable Long id) {
return service.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PutMapping("/{id}")
public ResponseEntity<Book> update(@PathVariable Long id, @RequestBody Book book) {
return service.update(id, book)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
return service.delete(id)
? ResponseEntity.noContent().build()
: ResponseEntity.notFound().build();
}
}Status Codes
| Action | Success Code | Failure Code |
|---|---|---|
| Create | 201 Created | 400 Bad Request |
| Read | 200 OK | 404 Not Found |
| Update | 200 OK | 404 Not Found |
| Delete | 204 No Content | 404 Not Found |
Testing with curl
The curl command sends requests from a terminal without any extra tools.
curl -X POST http://localhost:8080/api/books \
-H "Content-Type: application/json" \
-d '{"title":"Java Basics","author":"Mia Stone","price":19.5}'
curl http://localhost:8080/api/books/1
curl -X DELETE http://localhost:8080/api/books/1Understanding Each Endpoint
The create endpoint reads a JSON body and converts it into a Book object. The repository assigns an id and stores the row. The reply carries status 201 and the saved book, which includes the new id.
The list endpoint returns every book as a JSON array. Large tables need pagination, because a single reply with thousands of rows slows both the server and the client.
The single-item endpoint reads the id from the address. A missing record returns 404, and a found record returns 200 with the book.
The update endpoint loads the stored book, copies the new values onto it, and saves the result. A PUT request replaces the full record, so clients send every field.
The delete endpoint checks that the record exists before removing it. A successful removal returns 204 with an empty body, and a missing record returns 404.
Adding Validation
Validation rules stop bad data at the door. Annotations such as @NotBlank on the title and @Positive on the price reject empty or negative values. The @Valid annotation on the controller parameter activates the rules, and a failed check returns status 400 with a clear message.
Best Practices
- Use plural nouns in addresses, such as /books, and keep verbs out of them.
- Return DTO classes instead of entities when the entity holds private fields.
- Add validation so that empty titles never reach the database.
- Handle missing records with a global exception handler for cleaner controllers.
- Version the API with a prefix such as /api/v1 when clients depend on it.
A CRUD API forms the base of most backend systems. Once the four actions work, new entities follow the same pattern with small changes.
