A REST API is the most common thing you will build with Spring Boot, and the framework makes it almost embarrassingly quick. You write plain Java methods, annotate them with the HTTP verb they handle, and Spring takes care of routing, JSON conversion and the response. This guide builds a small products API covering the full set of operations.
If you have not run a Spring Boot app yet, start with your first Spring Boot application; this page assumes you have a project with the Web starter already running.
The model and the controller shell
REST APIs expose resources. Ours is a product, a simple Java class:
public class Product {
private Long id;
private String name;
private double price;
// constructors, getters and setters omitted for brevity
public Product(Long id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// getId(), getName(), getPrice(), setters ...
}
The controller is a bean the container manages. @RestController marks it as a web component
whose method return values become the HTTP response body — Spring turns them into JSON
automatically using the Jackson library that the Web starter includes.
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/products") // common prefix for every endpoint here
public class ProductController {
// A simple in-memory store to keep the example runnable
private final Map<Long, Product> store = new HashMap<>();
private long sequence = 0;
}
@RequestMapping("/api/products") sets a base path, so every method below only declares the
part after it. In a real app the store would be a database accessed through
Spring Data JPA; the in-memory map keeps this
focused on the web layer.
GET: read resources
Two read endpoints — list everything, and fetch one by id.
@GetMapping // GET /api/products
public Collection<Product> all() {
return store.values(); // Spring serializes the list to JSON
}
@GetMapping("/{id}") // GET /api/products/5
public Product byId(@PathVariable Long id) {
Product p = store.get(id);
if (p == null) {
throw new NoSuchElementException("Product " + id + " not found");
}
return p;
}
@PathVariable Long id binds the {id} segment of the URL to the method parameter, so a
request to /api/products/5 passes 5. Contrast this with @RequestParam, which reads
query-string values like ?size=10 — path variables identify a resource, request params
filter or configure. Returning a Product is enough; Spring writes it out as JSON.
POST: create a resource
Creating means reading a JSON body from the request and returning the created object with a 201 status.
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@PostMapping // POST /api/products
public ResponseEntity<Product> create(@RequestBody Product incoming) {
long id = ++sequence;
Product saved = new Product(id, incoming.getName(), incoming.getPrice());
store.put(id, saved);
// 201 Created is the correct status for a successful POST
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
@RequestBody tells Spring to deserialize the incoming JSON into a Product object — the
reverse of what it does for responses. Wrapping the result in ResponseEntity lets you set
the status explicitly; a successful create should return 201 Created, not the default 200.
Getting status codes right is part of what makes an API correct rather than merely working.
Common mistake: Returning 200 OK for every response regardless of what happened. Clients and other services rely on status codes — 201 for created, 404 for not found, 400 for bad input. Sloppy status codes make an API frustrating to consume. Use
ResponseEntityto be deliberate about them.
PUT and DELETE: update and remove
The remaining verbs complete the CRUD set.
@PutMapping("/{id}") // PUT /api/products/5
public Product update(@PathVariable Long id, @RequestBody Product incoming) {
Product existing = store.get(id);
if (existing == null) {
throw new NoSuchElementException("Product " + id + " not found");
}
Product updated = new Product(id, incoming.getName(), incoming.getPrice());
store.put(id, updated);
return updated;
}
@DeleteMapping("/{id}") // DELETE /api/products/5
@ResponseStatus(HttpStatus.NO_CONTENT) // 204, no body
public void delete(@PathVariable Long id) {
store.remove(id);
}
@PutMapping handles full updates, combining a path variable (which resource) with a request
body (the new data). @DeleteMapping removes the resource; a successful delete conventionally
returns 204 No Content with an empty body, which @ResponseStatus sets cleanly.
Test the API end to end
Run the app and exercise it with curl:
# Create a product
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Keyboard","price":1499}'
# -> {"id":1,"name":"Keyboard","price":1499.0}
# Fetch it back
curl http://localhost:8080/api/products/1
# -> {"id":1,"name":"Keyboard","price":1499.0}
# List all
curl http://localhost:8080/api/products
Four annotations — @GetMapping, @PostMapping, @PutMapping, @DeleteMapping — plus
@PathVariable, @RequestBody and ResponseEntity cover the vast majority of REST work you
will ever do. Spring handles routing and JSON in both directions so you write only the logic.
Pro tip: In interviews, be ready to explain the difference between
@Controllerand@RestController(the latter adds@ResponseBodyso methods return data, not view names), and between@PathVariableand@RequestParam. These two comparisons come up constantly — the Spring Boot REST API interview set drills them.
What makes an API "RESTful"
The annotations get you endpoints, but a good REST API follows a few conventions that make it predictable to consume. Interviewers and code reviewers both look for these, so build the habits from the start.
- Nouns, not verbs, in the URL. The resource is
/api/products; the action comes from the HTTP method. Avoid/api/getProductor/api/createProduct— the verb is already in GET and POST. - The right method for the job. GET reads and never changes data, POST creates, PUT replaces, PATCH partially updates, DELETE removes. GET and DELETE should be idempotent — calling them twice has the same effect as once.
- Meaningful status codes. 200 for a successful read, 201 for a create, 204 for a delete with no body, 400 for bad input, 404 for a missing resource, 500 only for genuine faults.
- Consistent JSON shapes. Return the same field names and structure across endpoints so clients can rely on them.
These conventions are not Spring-specific — they are what REST means — but Spring Boot makes
following them easy. The URL design maps to @RequestMapping and the verb annotations, and
ResponseEntity makes the status codes explicit.
Layering: keep controllers thin
The example above kept the store inside the controller to stay focused, but that is not how
you structure a real API. Controllers should handle only the web concern — read the request,
call something, shape the response — and delegate the actual work to a @Service bean.
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService service; // injected by the container
public ProductController(ProductService service) {
this.service = service;
}
@GetMapping("/{id}")
public Product byId(@PathVariable Long id) {
return service.findById(id); // logic lives in the service, not here
}
}
The service holds the business rules and, in a real app, talks to a repository. This
separation is why constructor injection matters: the controller declares it needs a
ProductService and the container supplies it, following the pattern in
Spring beans. A thin controller is far easier to test
and reason about than one mixing HTTP handling with business logic, and it is the structure
interviewers expect to see when they ask you to sketch an API.
Where JSON conversion happens
It is worth understanding the piece doing the invisible work, because interviewers ask about
it. When your method returns a Product, Spring does not magically know how to produce JSON —
it hands the object to Jackson, the JSON library the Web starter pulls in. Jackson reflects
over the object's getters and builds the JSON; for incoming @RequestBody data it does the
reverse, calling your setters or constructor to rebuild the object.
That means the shape of your JSON follows your Java fields. Rename a field and the JSON key
changes with it. You can control the mapping with Jackson annotations — @JsonProperty to
rename a key, @JsonIgnore to hide a field from output — but the default of "fields become
JSON keys" covers most cases. Knowing that a real library, not framework magic, sits behind
the conversion helps you debug the occasional surprise, like a field missing from the response
because it lacked a getter.
What comes next
This controller works, but a production API needs two things it does not yet have. First, input validation — right now you would happily store a product with a blank name or a negative price; the fix is covered in request validation. Second, clean error handling — those thrown exceptions currently produce ugly default responses, which REST exception handling turns into consistent JSON errors.
Add real persistence with Spring Data JPA and you have a complete backend. Work through these in order from the Spring Boot learning hub and you will have built exactly the kind of API that Java backend interviews and real jobs ask for.
Frequently Asked Questions
How do I create a REST API in Spring Boot?
What is the difference between @Controller and @RestController?
How does Spring Boot convert objects to JSON?
What is the difference between @PathVariable and @RequestParam?
How do I return a proper HTTP status code in Spring Boot?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

