Repositories
In the previous chapter we wrote PostRepository ourselves, with a map inside. Spring Data JPA makes such classes unnecessary: you declare an interface, and Spring creates the implementation with SQL queries at startup. Methods for saving, finding and deleting are already there.
The repository
Replace the content of PostRepository.java:
// src/main/java/com/example/blog/PostRepository.java
package com.example.blog;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository<Post, Long> {
}JpaRepository<Post, Long> means: a repository for the Post entity with a key of type Long. No @Repository annotation is needed — Spring finds such interfaces itself.
Ready-made methods
| Method | What it does |
|---|---|
save(post) | Inserts a new row or updates an existing one |
findById(id) | Looks up by key, returns Optional<Post> |
findAll() | All rows of the table |
existsById(id) | Whether a row with this key exists |
count() | The number of rows |
delete(post), deleteById(id) | Deleting |
The API response
An entity should not go to the client directly: it may later gain internal fields and relationships. Let's create a record for the response:
// src/main/java/com/example/blog/PostResponse.java
package com.example.blog;
import java.time.Instant;
public record PostResponse(Long id, String title, String body, String author, Instant createdAt) {
public static PostResponse from(Post post) {
return new PostResponse(post.getId(), post.getTitle(), post.getBody(),
post.getAuthor(), post.getCreatedAt());
}
}The static from method turns an entity into a response. The JSON for the client stays the same — the fields match the former Post record.
The service on the database
// src/main/java/com/example/blog/PostService.java
@Service
public class PostService {
private final PostRepository posts;
private final Clock clock;
public PostService(PostRepository posts, Clock clock) {
this.posts = posts;
this.clock = clock;
}
public List<PostResponse> findAll() {
return posts.findAll().stream().map(PostResponse::from).toList();
}
public PostResponse findById(Long id) {
return PostResponse.from(getPost(id));
}
public PostResponse create(NewPostRequest request) {
Post post = new Post(request.title(), request.body(), request.author(), Instant.now(clock));
return PostResponse.from(posts.save(post));
}
public PostResponse update(Long id, NewPostRequest request) {
Post post = getPost(id);
post.setTitle(request.title());
post.setBody(request.body());
return PostResponse.from(posts.save(post));
}
public void delete(Long id) {
posts.delete(getPost(id));
}
private Post getPost(Long id) {
return posts.findById(id).orElseThrow(() -> new PostNotFoundException(id));
}
}In the controller, change the return types from Post to PostResponse. The author filter comes back in the next lesson; for now the list method calls findAll() without a parameter.
Checking it
curl -i -X POST http://localhost:8080/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"Hello, Spring","body":"First post","author":"anna"}'HTTP/1.1 201
Location: /api/posts/1
{"id":1,"title":"Hello, Spring","body":"First post","author":"anna","createdAt":"2026-10-04T09:37:51.134416400Z"}The id 1 came from the database. Restart the application and run curl http://localhost:8080/api/posts — the post is still there, now stored in the data/blog.mv.db file. The log shows the SQL Hibernate ran:
Hibernate: insert into posts (author,body,created_at,title,id) values (?,?,?,?,default)Common mistake
findById returns an Optional, not the object itself. Calling posts.findById(id).get() for a missing id throws NoSuchElementException: No value present, and the client gets a 500. Use orElseThrow with your own exception, as in the getPost method — then the error handler returns a clear 404.