
We are going to talk about how to implement a RESTful Blog API using Spring Boot! We'll explore how we implemented Create, Read, Update, and Delete (CRUD) operations for a simple but powerful blog system. We'll break down each component and understand how they work together to create a robust backend service.
Our blog system is built using a clean, layered architecture that separates concerns and promotes maintainability. Here's how our components work together:
BlogApiController.java): Handles HTTP requests and routes them to appropriate service methodsBlogService.java): Contains business logic and coordinates with the repositoryBlogRepository.java): Manages data persistence using Spring Data JPAArticle.java): Defines our core blog article entityAddArticleRequest.java, ArticleResponse.java, UpdateArticleRequest.java): Manages data transfer objectsLet's start with understanding what happens when we perform each operation:
When someone wants to create a new blog article, here's the complete journey of what happens!!
{
"title": "My First Article",
"content": "This is my article content"
}
BlogApiController. The @PostMapping annotation tells Spring that this method should handle POST requests:@PostMapping("/api/articles")
public ResponseEntity<Article> addArticle(@RequestBody AddArticleRequest request) {
Article savedArticle = blogService.save(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(savedArticle);
}
@RequestBody automatically converts the incoming JSON into our AddArticleRequest object (Serialization). Think of it like a postal service that knows exactly how to unpack your package. Inside AddArticleRequest, we have:public class AddArticleRequest {
private String title;
private String content;
public Article toEntity() {
return Article.builder()
.title(title)
.content(content)
.build();
}
}
blogService.save(), passing this request object. In the service layer:public Article save(AddArticleRequest request) {
return blogRepository.save(request.toEntity());
}
toEntity() method converts our request into an actual Article entity, and then JPA saves it to the database. It's like taking our draft (the request) and publishing it (saving to database).We have two types of read operations:
@GetMapping("/api/articles")
public ResponseEntity<List<ArticleResponse>> findAllArticles() {
List<ArticleResponse> articles = blogService.findAll()
.stream()
.map(ArticleResponse::new)
.toList();
return ResponseEntity.ok()
.body(articles);
}
This is like asking a librarian to bring you all books. Here's what happens:
1. When a GET request comes to "/api/articles", Spring routes it to this method
2. The service layer fetches all articles from the database using blogRepository.findAll()
3. We transform each Article entity into an ArticleResponse (think of it as creating a summary card for each book)
4. The list of summaries is sent back to the client
@GetMapping("/api/articles/{id}")
public ResponseEntity<ArticleResponse> findArticle(@PathVariable long id) {
Article article = blogService.findById(id);
return ResponseEntity.ok()
.body(new ArticleResponse(article));
}
This is like asking for a specific book by its ID number:
1. The @PathVariable extracts the ID from the URL
2. The service layer tries to find the article:
public Article findById(long id) {
return blogRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("not found: " + id));
}
Updating is like revising an already published article:
@PutMapping("/api/articles/{id}")
public ResponseEntity<Article> updateArticle(@PathVariable long id,
@RequestBody UpdateArticleRequest request) {
Article updatedArticle = blogService.update(id, request);
return ResponseEntity.ok()
.body(updatedArticle);
}
The interesting part happens in the service layer:
@Transactional
public Article update(long id, UpdateArticleRequest request) {
Article article = blogRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("not found: " + id));
article.update(request.getTitle(), request.getContent());
return article;
}
The @Transactional annotation is crucial here - it's like putting up a "Do Not Disturb" sign while you're updating the article. If anything goes wrong during the update, all changes are rolled back, keeping our data consistent.
The delete operation is straightforward but needs careful handling:
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable long id) {
blogService.delete(id);
return ResponseEntity.ok()
.build();
}
In the service layer:
public void delete(long id) {
blogRepository.deleteById(id);
}
Think of this like removing a book from the library:
1. We check if the article exists
2. If it exists, we remove it completely from the database
3. We return a success response even if the article didn't exist (idempotency)
Our BlogRepository interface is quite simple but powerful:
public interface BlogRepository extends JpaRepository<Article, Long> {
}
By extending JpaRepository, we get all these CRUD operations for free! Spring Data JPA provides implementations for:
Throughout these operations, we handle errors gracefully:
1. When creating: We validate the input data
2. When reading: We handle the case of non-existent articles
3. When updating: We ensure transactional consistency
4. When deleting: We handle the case of already-deleted articles
Each operation is designed to be:
Our implementation includes robust error handling:
findById method throws an IllegalArgumentException when an article isn't foundResponseEntity to properly convey HTTP status codesWe use three different DTOs to maintain clean separation of concerns:
AddArticleRequest: Handles incoming article creation dataUpdateArticleRequest: Manages article update informationArticleResponse: Shapes the API response dataThis approach helps us:
Our Article entity demonstrates good practices in JPA entity design:
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "content", nullable = false)
private String content;
// ...
}
We've used:
We can test these endpoints using any HTTP client (like Postman or curl). Here are some example requests:
# Create an article
POST http://localhost:8080/api/articles
{
"title": "My First Article",
"content": "Hello, World!"
}
# Get all articles
GET http://localhost:8080/api/articles
# Get specific article
GET http://localhost:8080/api/articles/1
# Update an article
PUT http://localhost:8080/api/articles/1
{
"title": "Updated Title",
"content": "Updated content"
}
# Delete an article
DELETE http://localhost:8080/api/articles/1
This blog API demonstrates how to build a robust CRUD application using Spring Boot. By following REST principles and implementing proper separation of concerns, we've created a maintainable and scalable backend service.
Remember that this implementation can be extended with additional features like:
These enhancements would build upon the solid CRUD foundation we've established here.