Spring Boot CRUD API
user_crud_server   Introduction
Section titled “Introduction”Let’s expose user CRUD API without implementing any HTML views.
You can use spring initializr to create the project skeleton.

Server
Section titled “Server”Server Project Structure
Section titled “Server Project Structure”The project structure is:
- Directorysrc.main- Directoryjava.com.pietropouzzi.user_crud_server- Directorycontrollers- UserController.java
 
- Directorymodels- User.java
- UserRequest.java
 
- Directoryservices- UserService.java
 
- Directoryswagger- SwaggerCreateUser.java
- …
 
- UserCrudApplication.java
 
- Directoryresources- application.properties
 
 
Let’s take a closer look to the components’ duties by zooming in into user_crud_server package:
- Directorycontrollers- UserController.java exposes API endpoints, instantiates the service
 
- Directorymodels- User.java database model
- UserRequest.java API response model
 
- Directoryservices- UserService.java business logic that implements CRUD operations
 
- Directoryswagger- SwaggerCreateUser.java  @interfaceannotation for Swagger UI’s POST API endpoint
- … all the other Swagger annotation interfaces
 
- SwaggerCreateUser.java  
- UserCrudApplication.java starting point of Spring Boot application
Server Code
Section titled “Server Code”- Model
 A record class is a shallowly immutable, transparent carrier for a fixed set of values, called record components. Let’s define the user model as a javaRecordwith the following record components: id, name and age.
- Service
 Defines how the CRUD operations are performed. Let’s use a.csvfile to avoid implementing a whole database structure.
- Controller
 Expose APIs based on the Service methods.
If you don’t want to move away from my blog, here is the code!
package com.pietropoluzzi.user_crud_server.models;
public record User(Long id, String name, int age) {}package com.pietropoluzzi.user_crud_server.services;
// imports...
@Slf4j@Servicepublic class UserService {    private static final String CSV_FILE = "src/main/resources/static/users.csv";
    public List<User> findAll() {        log.info("find all users");        try (Stream<String> lines = Files.lines(Paths.get(CSV_FILE))) {            return lines                    .map(this::fromCsv)                    .collect(Collectors.toList());        } catch (IOException e) {            return new ArrayList<>();        }    }
    public User save(User user) {        log.info("save user");        List<User> users = findAll();        long nextId = users.stream()                .mapToLong(User::id)                .max()                .orElse(0L) + 1;
        User newUser = new User(nextId, user.name(), user.age());        users.add(newUser);        writeAll(users);        return newUser;    }
    public Optional<User> findById(Long id) {        log.info("find user by id");        return findAll().stream()                .filter(u -> u.id().equals(id))                .findFirst();    }
    public boolean delete(Long id) {        log.info("delete user");        List<User> users = findAll();        boolean removed = users.removeIf(u -> u.id().equals(id));        if (removed) {            writeAll(users);        }        return removed;    }
    public Optional<User> update(Long id, User updatedUser) {        log.info("update user");        List<User> users = findAll();        for (int i = 0; i < users.size(); i++) {            if (users.get(i).id().equals(id)) {                User newUser = new User(id, updatedUser.name(), updatedUser.age());                users.set(i, newUser);                writeAll(users);                return Optional.of(newUser);            }        }        return Optional.empty();    }
    /**     * Write all users to .csv file (fake DB)     * @param users list of users to write     */    private void writeAll(List<User> users) {        try (PrintWriter writer = new PrintWriter(CSV_FILE)) {            users.forEach(u -> writer.println(toCsv(u)));        } catch (IOException e) {            e.printStackTrace();        }    }
    private String toCsv(User user) {        return String.format("%d,%s,%d", user.id(), user.name(), user.age());    }
    private User fromCsv(String line) {        String[] parts = line.split(",");        return new User(Long.parseLong(parts[0]), parts[1], Integer.parseInt(parts[2]));    }}package com.pietropoluzzi.user_crud_server.controllers;
// imports ...
@RestController@RequestMapping("/users")public class UserController {
    private final UserService service;
    public UserController(UserService service) {        this.service = service;    }
    @SwaggerGetAllUsers    @GetMapping    public List<User> getAllUsers() {        return service.findAll();    }
    @SwaggerCreateUser    @PostMapping    public ResponseEntity<?> createUser(@RequestBody User user) {        UserResult result = service.save(user);
        if (result.isSuccessFlag()) {            return ResponseEntity.status(HttpStatus.OK).body(result.getUser());            // return new ResponseEntity<>(result.getUser(), HttpStatus.CREATED);        } else {            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getErrorMsg());            // return new ResponseEntity<>(result.getErrorMsg(), HttpStatus.BAD_REQUEST);        }    }
    @SwaggerGetUser    @GetMapping("/{id}")    public ResponseEntity<?> getUser(@PathVariable Long id) {        UserResult result = service.findById(id);
        if (result.isSuccessFlag()) {            return ResponseEntity.status(HttpStatus.OK).body(result.getUser());        } else {            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getErrorMsg());        }    }
    @SwaggerUserUpdate    @PutMapping("/{id}")    public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody User user) {        UserResult result = service.update(id, user);
        if (result.isSuccessFlag()) {            return ResponseEntity.status(HttpStatus.OK).body(result.getUser());        } else {            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getErrorMsg());        }    }
    @SwaggerUserDelete    @DeleteMapping("/{id}")    public ResponseEntity<?> deleteUser(@PathVariable Long id) {        UserResult result = service.delete(id);
        if (result.isSuccessFlag()) {            return ResponseEntity.status(HttpStatus.OK).body(result.getUser());        } else {            // this is a finesse to show that additional controls can be made            // if the ID a positive long but does not represent any user, returns a NOT_FOUND status            // if the ID is malformed (e.g. negative), returns a BAD_REQUEST status            if (Objects.equals(result.getErrorMsg(), "User not found"))                return ResponseEntity.status(HttpStatus.NOT_FOUND).body(result.getErrorMsg());            else return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getErrorMsg());        }    }}Server API calls
Section titled “Server API calls”Here is a list of API calls based on the OS you’re using.
POST
curl -Method POST http://localhost:8080/users `  -Headers @{ "Content-Type" = "application/json" } `  -Body '{ "name": "John", "age": 18 }'curl -X POST http://localhost:8080/users \  -H "Content-Type: application/json" \  -d '{"name": "Bill", "age": 20}'GET by ID
curl -Method GET http://localhost:8080/users/1# get User from id=1curl -X GET http://localhost:8080/users/1GET all
curl -Method GET http://localhost:8080/users# get User from id=1curl -X GET http://localhost:8080/usersUPDATE by ID
curl -Method PUT http://localhost:8080/users/1 `  -Headers @{ "Content-Type" = "application/json" } `  -Body '{ "name": "New Name", "age": 18 }'curl -X PUT http://localhost:8080/users/1 \  -H "Content-Type: application/json" \  -d '{"name": "New Name", "age": 20}'DELETE by ID
curl -Method DELETE http://localhost:8080/users/1curl -X DELETE http://localhost:8080/users/1API Documentation
Section titled “API Documentation”Add the following dependency to the pom.xml file in order to enable Swagger UI:
<dependency>  <groupId>org.springdoc</groupId>  <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>  <version>2.5.0</version></dependency>Then run the project and take a look at localhost:8080/swagger-ui/index.html page.
Let’s create a utility record class called UserRequest.java to be used within Swagger annotations.
It has the same parameters as User.java except for the id (which is auto-generated by UserService.java class):
package com.pietropoluzzi.user_crud_server.models;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Payload for creating a new user")public record UserRequest(        @Schema(description = "User name", example = "John") String name,        @Schema(description = "User age", example = "24") Integer age) {}@RequestBody annotation from io.swagger.v3.oas.annotations.parameters conflicts with @RequestBody annotation from org.springframework.web.bind.annotation.RequestBody.
The most straightforward solution is to use fully-qualified name for Swagger’s @RequestBody.
package com.pietropoluzzi.user_crud_server.controllers;
import io.swagger.v3.oas.annotations.Operation;import io.swagger.v3.oas.annotations.media.Content;import io.swagger.v3.oas.annotations.media.Schema;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;// DO NOT import io.swagger.v3.oas.annotations.parameters.RequestBody
@RestController@RequestMapping("/users")public class UserController {
    // ...    @Operation(            summary = "Create a new user",            description = "Add a new user to the database.",            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(                    required = true,                    content = @Content(                            mediaType = "application/json",                            schema = @Schema(implementation = UserRequest.class)                    )            )    )    @PostMapping    public User createUser(@RequestBody User user) {        return service.save(user);    }}Custom Swagger package
Section titled “Custom Swagger package”To avoid writing controllers with hundreds of lines of code dedicated to Swagger annotations, you can create a package where to put custom interfaces for each REST API method. Then, just annotate the API methods within the controller with the custom interfaces.
Let’s take the POST method that creates a new user. The following code snippets shows both UserController implementation and SwaggerCreateUser interface:
package com.pietropoluzzi.user_crud_server.controllers;
// import ...
@RestController@RequestMapping("/users")public class UserController {
    @SwaggerCreateUser    @PostMapping    public ResponseEntity<?> createUser(@RequestBody User user) {        /* method implementation */    }
    /* all the other CRUD endpoints */}package com.pietropoluzzi.user_crud_server.swagger;
import com.pietropoluzzi.user_crud_server.models.User;import com.pietropoluzzi.user_crud_server.models.UserRequest;import io.swagger.v3.oas.annotations.Operation;import io.swagger.v3.oas.annotations.media.Content;import io.swagger.v3.oas.annotations.media.ExampleObject;import io.swagger.v3.oas.annotations.media.Schema;import io.swagger.v3.oas.annotations.responses.ApiResponse;import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.lang.annotation.*;
@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Operation(        summary = "Create a new user",        description = "Add a new user to the database. ID is generated automatically.",        requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(                required = true,                content = @Content(                        mediaType = "application/json",                        schema = @Schema(implementation = UserRequest.class)                )        ))@ApiResponses(value = {        @ApiResponse(                responseCode = "200",                description = "User successfully created",                content = @Content(                        mediaType = "application/json",                        schema = @Schema(implementation = User.class)                )        ),        @ApiResponse(                responseCode = "400",                description = "Bad request",                content = @Content(                        mediaType = "text/plain",                        examples = {                                @ExampleObject(name = SwaggerConstants.AGE_NAME,  summary = SwaggerConstants.AGE_SUM, value = SwaggerConstants.AGE),                                @ExampleObject(name = SwaggerConstants.NAME_NAME,  summary = SwaggerConstants.NAME_SUM, value = SwaggerConstants.NAME)                        }                )        )})public @interface SwaggerCreateUser {}