Building a Rest API with Spring Boot in 10 steps


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: https://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:
👉 http://localhost:8080/h2-console
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
Subscribe to my newsletter
Read articles from Mirna De Jesus Cambero directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Mirna De Jesus Cambero
Mirna De Jesus Cambero
I’m a backend software engineer with over a decade of experience primarily in Java. I started this blog to share what I’ve learned in a simplified, approachable way — and to add value for fellow developers. Though I’m an introvert, I’ve chosen to put myself out there to encourage more women to explore and thrive in tech. I believe that by sharing what we know, we learn twice as much — that’s precisely why I’m here.