Spring BootREST APIsbeginner
Updated:

Building a REST API with Spring Boot

7 min read

Turn Spring Boot into a working REST API — controllers, the full set of HTTP verbs, JSON request and response bodies, path variables and proper status codes, step by step.

TL;DR – Quick Answer

To build a REST API with Spring Boot, create a class annotated with @RestController and map HTTP methods to Java methods using @GetMapping, @PostMapping, @PutMapping and @DeleteMapping. Spring automatically converts Java objects to and from JSON, reads request bodies with @RequestBody, and pulls URL segments with @PathVariable. Wrap responses in ResponseEntity to control status codes. The embedded server means the API runs as a plain jar with no extra setup.

On This Page

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 ResponseEntity to 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 @Controller and @RestController (the latter adds @ResponseBody so methods return data, not view names), and between @PathVariable and @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/getProduct or /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?
Add the spring-boot-starter-web dependency, create a class annotated with @RestController, and map endpoints with @GetMapping, @PostMapping, @PutMapping and @DeleteMapping. Spring handles JSON conversion automatically. Run the app and the API is live on the embedded server, typically at port 8080.
What is the difference between @Controller and @RestController?
@Controller is the classic MVC annotation whose methods usually return view names for server-rendered pages. @RestController is @Controller combined with @ResponseBody, so every method returns data written directly to the HTTP response body, normally as JSON. For APIs you use @RestController.
How does Spring Boot convert objects to JSON?
Spring Boot includes the Jackson library, which serializes your return objects to JSON automatically and deserializes incoming JSON into your Java objects for @RequestBody parameters. You just return a POJO or a list, and the framework produces the JSON. No manual parsing is needed.
What is the difference between @PathVariable and @RequestParam?
@PathVariable reads a value embedded in the URL path, such as the 5 in /products/5. @RequestParam reads a query-string parameter, such as the size in /products?size=10. Use path variables to identify a resource and request params for filters and options.
How do I return a proper HTTP status code in Spring Boot?
Return a ResponseEntity, which lets you set the body and status together, for example ResponseEntity.status(HttpStatus.CREATED).body(saved) for a 201. Alternatively annotate the method with @ResponseStatus. Returning the right status codes is important for a correct, well-behaved REST API.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 15 July 2026 LinkedIn
Chat with us