Step 1: Install Necessary Tools
1- Install sdkman
2- Install java:
Run: sdk install java
Verify: java -version
3- Install Eclipse STS (Spring Tools for Eclipse):
Download from: spring.io/tools
For example, in my case, I’ve downloaded: spring-tool-suite-4-4.28.1.RELEASE-e4.34.0-macosx.cocoa.aarch64.dmg
Install and launch STS
4- Install Apache Maven:
STS comes with an embedded Maven, but it’s recommend to install it separately for better control
Run: brew install maven
Verify Installation, by running: mvn -version
Note: if Maven is not picking up the right version of java you may need to set the JAVA_HOME in your shell config file, for example, in my case (since I’m using MacOS): Run: echo 'export JAVA_HOME=$HOME/.sdkman/candidates/java/current' >> ~/.zshrc source ~/.zshrc
Run: mvn -version:
You should see an output like this:
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937) |
Step 2: Create a New Spring Boot Project in STS
Open Spring Tool Suite (STS)
Go to File → New → Spring Starter Project
Enter project details:
Name: spring-crud-posts
Type: Maven
Packaging: Jar
Java Version: 17 (or the latest installed)
Spring Boot Version: Select 3.x.x (Latest)
Click Next.
Step 3: Add additional dependencies to your pom.xml:
<dependencies>
<!-- These are in addition to the dependencies included by default -->
<!-- Spring Web (for REST API) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA (for database access) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 Database (for testing, can be replaced with MySQL/PostgreSQL) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Now test, by running the Spring Boot Application
Open SpringCrudPostsApplication.java
Run it:
- Right-click → Run As → Spring Boot App
You should see logs indicating that Tomcat has started on port 8080:
Tomcat started on port 8080
Step 4: Create a Simple REST API Endpoint and test it:
1- Create a new package: com.example.demo.controller
2- Create a new class with the following code:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("api/posts")
public class PostController {
@GetMapping("/hello")
public String sayHello() {
return "Hello World with Spring, so exciting";
}
}
Test the new endpoint by invoking http://localhost:8080/api/posts/hello on your browser:
Step 5: Configure in Memory Database:
Open the application.properties file and add the following configuration:
# Enable H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
Then restart the application and test the connection:
JDBC URL: jdbc:h2:mem:testdb
Username: sa
Password: (leave blank)
Click Connect.
Step 6: Implement a simple entity:
package com.example.demo.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
public Post() {
}
public Post(String title, String content) {
this.title = title;
this.content = content;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
Step 7: Implement a Repository:
package com.example.demo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.demo.model.Post;
//This makes it a Spring-managed bean
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
}
Step 8 - Implement the Service layer:
package com.example.demo.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.model.Post;
import com.example.demo.repository.PostRepository;
@Service
public class PostService {
private final PostRepository repository;
// As long as the class has one constructor Spring injects the dependency
// without the need of Autowired.
public PostService(PostRepository repository) {
this.repository = repository;
}
public List<Post> getAllPosts() {
return repository.findAll();
}
public Post getPostById(long id) {
return repository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Post not found with the id " + id));
}
public Post createPost(Post post) {
Post savedPost = repository.save(post);
System.out.println("savedPost : " + savedPost);
return savedPost;
}
public Post updatePost(Long id, Post updatedPost) {
return repository.findById(id).map(post -> {
post.setTitle(updatedPost.getTitle());
post.setContent(updatedPost.getContent());
return repository.save(post);
}).orElseThrow(() -> new RuntimeException("Post not found with the id " + id));
}
public void delete(Long id) {
repository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Post not found with id: " + id));
repository.deleteById(id);
}
}
And the custom ResponseStatus:
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND) // This makes Spring return a 404 response
public class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ResourceNotFoundException(String message) {
super(message);
}
}
Step 9: Create the Controller:
package com.example.demo.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.Post;
import com.example.demo.service.PostService;
@RestController
@RequestMapping("api/posts")
public class PostController {
private final PostService service;
public PostController(PostService service) {
this.service = service;
}
@GetMapping
public List<Post> getAllPosts() {
return service.getAllPosts();
}
@GetMapping("/{id}")
public ResponseEntity<Post> getPostById(@PathVariable long id) {
return ResponseEntity.ok(service.getPostById(id));
}
@PostMapping
public ResponseEntity<Post> createPost(@RequestBody Post post) {
Post savedPost = service.createPost(post);
return ResponseEntity.status(HttpStatus.CREATED).body(savedPost);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePost(@PathVariable long id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
@PutMapping("/{id}")
public ResponseEntity<Post> updatePost(@PathVariable long id, @RequestBody Post post) {
Post updatedPost = service.updatePost(id, post);
return ResponseEntity.ok(updatedPost);
}
}
Then restart the app and test it:
Now, let’s add some test data and configuration to pre-load some posts by default:
Application properties:
#Automatically creates or updates tables based on your JPA entities.
spring.jpa.hibernate.ddl-auto=update
# Ensure Spring runs SQL scripts in the correct order (schema first and then data)
spring.sql.init.mode=always
# Ensure Hibernate fully initializes before data.sql runs.
spring.jpa.defer-datasource-initialization=true
src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS posts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL
);
src/main/resources/data.sql
INSERT INTO posts (title, content) VALUES ('First Post', 'This is the first post');
INSERT INTO posts (title, content) VALUES ('Second Post', 'This is the second post');
Finally, restart the application and invoke the posts endpoint:
Step 10: Create the remaining of the endpoints:
package com.example.demo.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.model.Post;
import com.example.demo.service.PostService;
@RestController
@RequestMapping("api/posts")
public class PostController {
private final PostService service;
public PostController(PostService service) {
this.service = service;
}
@GetMapping
public List<Post> getAllPosts() {
return service.getAllPosts();
}
@GetMapping("/{id}")
public ResponseEntity<Post> getPostById(@PathVariable long id) {
return ResponseEntity.ok(service.getPostById(id));
}
@PostMapping
public ResponseEntity<Post> createPost(@RequestBody Post post) {
Post savedPost = service.createPost(post);
return ResponseEntity.status(HttpStatus.CREATED).body(savedPost);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePost(@PathVariable long id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
@PutMapping("/{id}")
public ResponseEntity<Post> updatePost(@PathVariable long id, @RequestBody Post post) {
Post updatedPost = service.updatePost(id, post);
return ResponseEntity.ok(updatedPost);
}
}
Finally, we can test everything using Postman:
Verified all endpoints return expected JSON responses.
Ensured correct HTTP status codes (200 OK, 201 Created, 204 No Content, 400 Bad Request, 404 Not Found).
For example:
Github repo with this code on the commit https://github.com/mdjc/blog-posts-app/commit/79b4ef269b23968ff6cf6c2627e80a183758bd46