Building a Rest API with Spring Boot in 10 steps

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: 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

  1. Open Spring Tool Suite (STS)

  2. Go to File → New → Spring Starter Project

  3. 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)

  4. 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

  1. Open SpringCrudPostsApplication.java

  2. Run it:

    • Right-click → Run As → Spring Boot App
  3. 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:

👉 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