Java + Spring Boot track

Lesson 24 · Part 6: Spring Boot

REST Controllers

REST API is a promise to handle resources using only HTTP methods (GET / POST / PUT / DELETE) and URLs. In Spring Boot, it can be implemented very concisely with…

Part 6: Spring Boot2 min readFree lesson

REST API is a promise to handle resources using only HTTP methods (GET / POST / PUT / DELETE) and URLs. In Spring Boot, it can be implemented very concisely with @RestController and various mapping annotations.

Learning Objectives

  • Know the difference between @RestController and @Controller
  • Know the meaning of @GetMapping / @PostMapping / @PutMapping / @DeleteMapping
  • Receive input as @PathVariable and @RequestParam
  • Receive JSON as an object with @RequestBody
  • Reply with a status code of ResponseEntity

Key concepts

1) @RestController

@RestController
@RequestMapping("/api/hello")
public class HelloController { ... }

Composition of @Controller + @ResponseBody. Serialize the return value as JSON and write it as is in the response body.

2) Mapping annotation

annotation HTTP Method
@GetMapping GET
@PostMapping POST
@PutMapping PUT
@DeleteMapping DELETE

3) @PathVariable vs @RequestParam

@GetMapping("/users/{id}")
public User get(@PathVariable Long id) { ... }      // /users/3

@GetMapping("/users")
public List<User> list(@RequestParam(defaultValue="0") int page) { ... }
// /users?page=2

4) @RequestBody

@PostMapping("/users")
public User create(@RequestBody UserCreateRequest req) { ... }

The JSON body is automatically converted to Java objects (using Jackson).

5) ResponseEntity

return ResponseEntity.status(HttpStatus.CREATED).body(user);

Used when specifying status code + header + body together.

See as example

Example 1 — HelloController.java : GET + @RequestParam

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(@RequestParam(defaultValue = "Spring") String name) {
        return "Hello, " + name + "!";
    }
}

curl-http/part/localhost-8083/hello-name-index → Hello, Jisoo!

Example 2 — GreetingController.java : @PathVariable + JSON

@RestController
@RequestMapping("/api/greetings")
public class GreetingController {
    record Greeting(String message, int length) {}

    @GetMapping("/{name}")
    public Greeting greet(@PathVariable String name) {
        String msg = "Hello, " + name + "!";
        return new Greeting(msg, msg.length());
    }
}

curl http://localhost:8083/api/greetings/Java → {"message":"Hello, Java!","length":12}

Example 3 — EchoController.java : POST + @RequestBody

@RestController
@RequestMapping("/api/echo")
public class EchoController {
    record EchoRequest(String text, int repeat) {}
    record EchoResponse(String result) {}

    @PostMapping
    public EchoResponse echo(@RequestBody EchoRequest req) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < req.repeat(); i++) sb.append(req.text());
        return new EchoResponse(sb.toString());
    }
}
curl -X POST http://localhost:8083/api/echo \
     -H "Content-Type: application/json" \
     -d '{"text":"hi","repeat":3}'
# {"result":"hihihi"}

Example 4 — StatusController.java : ResponseEntity

@RestController
public class StatusController {
    @GetMapping("/teapot")
    public ResponseEntity<String> teapot() {
        return ResponseEntity.status(418).body("I'm a teapot");
    }

    @PostMapping("/items")
    public ResponseEntity<String> create() {
        return ResponseEntity.status(HttpStatus.CREATED).body("created");
    }
}

run

cd 06_Spring_Boot/24_REST_Controller
mvn spring-boot:run

Common Mistakes

  1. Set to @Controller and JSON does not return → Use @RestController
  2. Missing @RequestParam → If it is different from the method signature name, matching fails.
  3. If @RequestBody is missing, the body is not mapped.
  4. Omission of ("id") when the name of @PathVariable is different from the URL variable name.
  5. Directly returning JSON incompatible types

Cleanup

  • The combination of @RestController + mapping annotation is the basis of REST
  • Input is separated into @RequestParam / @PathVariable / @RequestBody
  • If the status code is significant, it is ResponseEntity

Try it yourself

cd 06_Spring_Boot/24_REST_Controller
mvn spring-boot:run
#another terminal
curl http://localhost:8083/hello
curl http://localhost:8083/api/greetings/Spring
curl -X POST http://localhost:8083/api/echo -H "Content-Type: application/json" -d '{"text":"hi","repeat":3}'

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/main/java/com/codingnow/lecture/spring24/Application.javaJava
package com.codingnow.lecture.spring24;

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);
    }
}
src/main/java/com/codingnow/lecture/spring24/controller/EchoController.javaJava
package com.codingnow.lecture.spring24.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/echo")
public class EchoController {
    public record EchoRequest(String text, int repeat) {}
    public record EchoResponse(String result) {}

    @PostMapping
    public EchoResponse echo(@RequestBody EchoRequest req) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < req.repeat(); i++) sb.append(req.text());
        return new EchoResponse(sb.toString());
    }
}
src/main/java/com/codingnow/lecture/spring24/controller/GreetingController.javaJava
package com.codingnow.lecture.spring24.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/greetings")
public class GreetingController {
    record Greeting(String message, int length) {}

    @GetMapping("/{name}")
    public Greeting greet(@PathVariable String name) {
        String msg = "Hello, " + name + "!";
        return new Greeting(msg, msg.length());
    }
}
src/main/java/com/codingnow/lecture/spring24/controller/HelloController.javaJava
package com.codingnow.lecture.spring24.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(@RequestParam(defaultValue = "Spring") String name) {
        return "Hello, " + name + "!";
    }
}
src/main/java/com/codingnow/lecture/spring24/controller/StatusController.javaJava
package com.codingnow.lecture.spring24.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StatusController {
    @GetMapping("/teapot")
    public ResponseEntity<String> teapot() {
        return ResponseEntity.status(418).body("I'm a teapot");
    }

    @PostMapping("/items")
    public ResponseEntity<String> create() {
        return ResponseEntity.status(HttpStatus.CREATED).body("created");
    }
}
src/main/resources/application.propertiesText
server.port=8083
spring.application.name=lecture-spring24

Assignment

Problem — Memory-based Todo CRUD

  • Reference solution: shown below
  • Core concept: Handling 5 HTTP methods with one controller

Requirements

TodoController provides the following endpoints:

method path Action
GET /todos Full list
GET /todos/{id} Single inquiry (404 if none)
POST /todos generate (id automatically)
PUT /todos/{id} Edit
DELETE /todos/{id} Delete

Internal storage is ConcurrentHashMap<Long, Todo> + AtomicLong (DB has 26 parts).

Expected behavior

$ curl -X POST localhost:8084/todos -H "Content-Type: application/json" \
       -d '{"title":"Milk","done":false}'
{"id":1,"title":"milk","done":false}

$ curl localhost:8084/todos
[{"id":1,"title":"milk","done":false}]

Check the correct answer

Look at the Maven project in the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

pom.xmlXML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.6</version>
        <relativePath/>
    </parent>

    <groupId>com.codingnow</groupId>
    <artifactId>lecture-spring24-hw</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
src/main/java/com/codingnow/lecture/spring24hw/Application.javaJava
package com.codingnow.lecture.spring24hw;

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);
    }
}
src/main/java/com/codingnow/lecture/spring24hw/TodoController.javaJava
package com.codingnow.lecture.spring24hw;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

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;

@RestController
@RequestMapping("/todos")
public class TodoController {
    public record Todo(Long id, String title, boolean done) {}
    public record TodoInput(String title, boolean done) {}

    private final Map<Long, Todo> store = new ConcurrentHashMap<>();
    private final AtomicLong seq = new AtomicLong();

    @GetMapping
    public List<Todo> list() {
        return List.copyOf(store.values());
    }

    @GetMapping("/{id}")
    public ResponseEntity<Todo> get(@PathVariable Long id) {
        Todo t = store.get(id);
        return t == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(t);
    }

    @PostMapping
    public Todo create(@RequestBody TodoInput in) {
        long id = seq.incrementAndGet();
        Todo t = new Todo(id, in.title(), in.done());
        store.put(id, t);
        return t;
    }

    @PutMapping("/{id}")
    public ResponseEntity<Todo> update(@PathVariable Long id, @RequestBody TodoInput in) {
        if (!store.containsKey(id)) return ResponseEntity.notFound().build();
        Todo t = new Todo(id, in.title(), in.done());
        store.put(id, t);
        return ResponseEntity.ok(t);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        return store.remove(id) == null
            ? ResponseEntity.notFound().build()
            : ResponseEntity.noContent().build();
    }
}
src/main/resources/application.propertiesText
server.port=8084
spring.application.name=lecture-spring24-hw

Learn this lesson with a mentor

Work through the Java + Spring Boot track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now