Basic CRUD Backend

박진석·2025년 2월 3일
post-thumbnail

Building a Blog API with Spring Boot: A Deep Dive into CRUD Operations

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.

Project Structure Overview

Our blog system is built using a clean, layered architecture that separates concerns and promotes maintainability. Here's how our components work together:

  • Controller Layer (BlogApiController.java): Handles HTTP requests and routes them to appropriate service methods
  • Service Layer (BlogService.java): Contains business logic and coordinates with the repository
  • Repository Layer (BlogRepository.java): Manages data persistence using Spring Data JPA
  • Domain Layer (Article.java): Defines our core blog article entity
  • DTO Layer (AddArticleRequest.java, ArticleResponse.java, UpdateArticleRequest.java): Manages data transfer objects

Understanding Each CRUD Operation

Let's start with understanding what happens when we perform each operation:

CREATE Operation (Adding a New Blog Article)

When someone wants to create a new blog article, here's the complete journey of what happens!!

  1. The process begins when a client sends a POST request to "/api/articles" with the article data in JSON format, like this:
{
    "title": "My First Article",
    "content": "This is my article content"
}
  1. This request first hits our 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);
}
  1. The magic of @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();
    }
}
  1. The controller then calls blogService.save(), passing this request object. In the service layer:
public Article save(AddArticleRequest request) {
    return blogRepository.save(request.toEntity());
}
  1. The 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).

READ Operations (Retrieving Blog Articles)

We have two types of read operations:

Getting All Articles:

@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

Getting a Single Article:

@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));
}
  1. If found, it's converted to an ArticleResponse and sent back; if not, we throw an exception

UPDATE Operation (Modifying an Existing Article)

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.

DELETE Operation (Removing an Article)

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)

The Role of JPA Repository

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:

  • save() for creating and updating
  • findById() for reading one
  • findAll() for reading all
  • deleteById() for deleting

Error Handling

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:

  • Atomic: It either completely succeeds or completely fails
  • Consistent: It maintains data integrity
  • Isolated: One operation doesn't interfere with another
  • Durable: Once completed, changes are permanent

Advanced Features and Best Practices

Error Handling

Our implementation includes robust error handling:

  • The findById method throws an IllegalArgumentException when an article isn't found
  • We use ResponseEntity to properly convey HTTP status codes
  • The service layer validates inputs before performing operations

Data Transfer Objects (DTOs)

We use three different DTOs to maintain clean separation of concerns:

  1. AddArticleRequest: Handles incoming article creation data
  2. UpdateArticleRequest: Manages article update information
  3. ArticleResponse: Shapes the API response data

This approach helps us:

  • Control what data is exposed to clients
  • Validate input data effectively
  • Maintain API versioning more easily
  • Reduce unnecessary data transfer

JPA Entity Design

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:

  • Protected default constructor for JPA
  • Builder pattern for object creation
  • Proper column constraints
  • Immutable ID field

Testing the API

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

Conclusion

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:

  • User authentication and authorization
  • Comment functionality
  • Categories and tags
  • Image upload support
  • Search functionality

These enhancements would build upon the solid CRUD foundation we've established here.

0개의 댓글