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

  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:

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

0
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