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:
curl https://api.github.com/users/torvalds
{
"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:
<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:
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:
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:
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.
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.
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.
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. ResponseEntityso you return correct HTTP status codes —404 Not Foundfor a missing record,201 Createdafter aPOST,204 No Contentafter a delete — rather than always returning200.
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:
# 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-validationand annotate fields (@NotBlank,@Positive) so bad input is rejected with a clear400. - Pagination. For large scraped datasets, return
Page<Product>and accept?page=0&size=50so clients don't pull a million rows at once. - Error handling. A
@RestControllerAdviceclass turns exceptions into consistent JSON error responses. - Documentation. Add
springdoc-openapiand 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.