By Language 11 min read

Creating a REST API with Spring: Step-by-Step Tutorial

Step-by-step tutorial on creating a REST API with Spring: project setup, controllers, JSON responses, and testing. Build your first Java API endpoint today.

ST
Scraping.Pro Team
Data collection for business needs
Published: 12 April 2026

A REST API is the cleanest way to hand structured data to other applications — a mobile app, a dashboard, a bot, or another service on your team. If you have a web scraping pipeline collecting products, listings, or reviews, wrapping the results in a REST API means any client can pull fresh JSON without touching your database or your scraper. This Spring REST API tutorial builds a working Java API from an empty project to tested endpoints, using the current stack: Spring Boot 3, Java 21, and Spring Data JPA.

What "REST API" actually means

Before writing code, it helps to see what an API returns. Ask GitHub's public API for a user and you get JSON instead of a rendered web page:

bash
curl https://api.github.com/users/torvalds
json
{
  "login": "torvalds",
  "id": 1024025,
  "name": "Linus Torvalds",
  "company": "Linux Foundation",
  "location": "Portland, OR",
  "public_repos": 8,
  "followers": 230000
}

That is the whole point of an API: instead of downloading an HTML page and parsing it, the client receives clean, machine-readable data. REST (REpresentational State Transfer) is an architectural style built on top of ordinary HTTP. Two roles are involved — a server that holds the data and a client that requests it — and they communicate through URLs (resources) and HTTP verbs:

Verb Purpose Example
GET Read GET /products
POST Create POST /products
PUT / PATCH Update PUT /products/42
DELETE Remove DELETE /products/42

The beauty of REST is that the client and server share nothing but this contract. Your scraper can be written in Python, your API in Java, and your dashboard in TypeScript — as long as everyone speaks JSON over HTTP, they interoperate.

Why Spring in 2026

Java remains one of the most widely deployed languages in enterprise back ends, and Spring Boot is its default web framework. It handles the boilerplate — an embedded web server, JSON serialization, dependency injection, database wiring — so you write endpoints, not plumbing. It is mature, exhaustively documented, and heavily used, which means every problem you hit has already been answered. If your organization runs on the JVM, a Spring Boot REST API is the path of least resistance.

We will build a small API that stores and serves scraped products — a stand-in for any dataset you collect. Clients will be able to list, create, update, delete, and search records.

Setting up the project

Skip the manual Maven wiring the old tutorials used and start from Spring Initializr (start.spring.io). Choose:

  • Project: Maven
  • Language: Java
  • Spring Boot: 3.x (the current generation)
  • Java: 21 (the current LTS)
  • Dependencies: Spring Web, Spring Data JPA, and a database driver — H2 for a zero-config demo, or PostgreSQL / MySQL for production.

Download the generated ZIP and open it in IntelliJ IDEA, VS Code, or any editor. The pom.xml already contains the right dependencies:

xml
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>

The generated Application class is your entry point — no manual main wiring needed:

java
package pro.scraping.api;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Configure the datasource in src/main/resources/application.properties. For the H2 in-memory demo:

properties
spring.datasource.url=jdbc:h2:mem:products
spring.jpa.hibernate.ddl-auto=update
server.port=8080

To point at a real PostgreSQL instance instead, swap the URL and add credentials:

properties
spring.datasource.url=jdbc:postgresql://localhost:5432/scraped_data
spring.datasource.username=postgres
spring.datasource.password=${DB_PASSWORD}

Keep secrets in environment variables (${DB_PASSWORD}), never hard-coded in the file.

The entity

An entity is a Java class mapped to a database table. Note the import: Spring Boot 3 uses the jakarta.persistence namespace, not the old javax.persistence — this is the single most common thing that breaks when people copy pre-2023 tutorials.

java
package pro.scraping.api.product;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.math.BigDecimal;

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private BigDecimal price;
    private String sourceUrl;

    protected Product() { }  // JPA requires a no-arg constructor

    public Product(String name, BigDecimal price, String sourceUrl) {
        this.name = name;
        this.price = price;
        this.sourceUrl = sourceUrl;
    }

    // getters and setters
    public Long getId() { return id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
    public String getSourceUrl() { return sourceUrl; }
    public void setSourceUrl(String sourceUrl) { this.sourceUrl = sourceUrl; }
}

Note BigDecimal for the price — never use double or float for money, because binary floating point cannot represent decimal amounts exactly and you will lose cents.

The repository

A repository is an interface that gives you database access without writing SQL. Extend JpaRepository and you inherit findAll, findById, save, deleteById, and more. Spring even derives queries from method names — findByPriceBetween becomes a real SQL WHERE clause automatically.

java
package pro.scraping.api.product;

import org.springframework.data.jpa.repository.JpaRepository;
import java.math.BigDecimal;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContainingIgnoreCase(String name);
    List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
}

The REST controller

The controller maps HTTP requests to Java methods. @RestController tells Spring every method returns data (serialized to JSON), not a view. @RequestMapping("/products") sets the base path so each method only declares its own suffix.

java
package pro.scraping.api.product;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;

@RestController
@RequestMapping("/products")
public class ProductController {

    private final ProductRepository repo;

    public ProductController(ProductRepository repo) {  // constructor injection
        this.repo = repo;
    }

    @GetMapping
    public List<Product> list() {
        return repo.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> get(@PathVariable Long id) {
        return repo.findById(id)
                   .map(ResponseEntity::ok)
                   .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(org.springframework.http.HttpStatus.CREATED)
    public Product create(@RequestBody Product product) {
        return repo.save(product);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> update(@PathVariable Long id, @RequestBody Product body) {
        return repo.findById(id).map(existing -> {
            existing.setName(body.getName());
            existing.setPrice(body.getPrice());
            existing.setSourceUrl(body.getSourceUrl());
            return ResponseEntity.ok(repo.save(existing));
        }).orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        if (!repo.existsById(id)) return ResponseEntity.notFound().build();
        repo.deleteById(id);
        return ResponseEntity.noContent().build();
    }

    @GetMapping("/search")
    public List<Product> search(@RequestParam String name) {
        return repo.findByNameContainingIgnoreCase(name);
    }

    @GetMapping("/range")
    public List<Product> range(@RequestParam BigDecimal min, @RequestParam BigDecimal max) {
        return repo.findByPriceBetween(min, max);
    }
}

Two modern practices worth noting compared to older Spring code:

  • Constructor injection instead of field-level @Autowired. It makes dependencies explicit and the class easier to test.
  • ResponseEntity so you return correct HTTP status codes — 404 Not Found for a missing record, 201 Created after a POST, 204 No Content after a delete — rather than always returning 200.

Running and testing

Start the app with ./mvnw spring-boot:run (or the run button in your IDE). It boots an embedded Tomcat server on port 8080. Test it from the command line with curl:

bash
# create a product
curl -X POST http://localhost:8080/products \
  -H "Content-Type: application/json" \
  -d '{"name":"Coffee grinder","price":34.90,"sourceUrl":"https://shop.example/p/1"}'

# list everything
curl http://localhost:8080/products

# find products between $10 and $50
curl "http://localhost:8080/products/range?min=10&max=50"

For interactive testing, use a GUI client — Postman, Bruno, or Insomnia are the common choices in 2026 (Bruno and Insomnia are popular open-source options). For automated tests, Spring Boot ships with MockMvc and @SpringBootTest so you can assert on status codes and JSON bodies in your CI pipeline.

Production checklist

The tutorial API works, but before it faces real clients, add:

  • A DTO layer. Don't expose entities directly — map them to response records (public record ProductResponse(Long id, String name, BigDecimal price) {}) so your database schema and your API contract can evolve independently.
  • Validation. Add spring-boot-starter-validation and annotate fields (@NotBlank, @Positive) so bad input is rejected with a clear 400.
  • Pagination. For large scraped datasets, return Page<Product> and accept ?page=0&size=50 so clients don't pull a million rows at once.
  • Error handling. A @RestControllerAdvice class turns exceptions into consistent JSON error responses.
  • Documentation. Add springdoc-openapi and you get interactive Swagger UI docs generated from your controllers for free.
  • Security. Put Spring Security in front and require an API key or OAuth2 token for anything beyond public reads.

From scraper to API

A REST API is the natural front door for collected data: your web scraper writes rows into the database on a schedule, and this Spring service serves them to whoever needs them. If you would rather skip both halves — the collection and the serving — scraping.pro can deliver clean, structured data straight into your systems as a data-as-a-service feed or a ready-made API, so your team consumes endpoints instead of maintaining crawlers. Either way, the pattern is the same one this tutorial builds: structured data in, JSON out, any client welcome.