Day 14: Booting Spring

Nagraj MathNagraj Math
8 min read

It is 20 minutes to 1 in the afternoon of a rather fine day dated 29th of June and we are nearing the end of our 48-hour long course!

We reach the main framework most in use these day - Spring Boot!

What is Spring Boot?

Spring Boot is a framework built on top of Spring that helps us configure things faster, auto configure beans, bundles dependencies using starters, embeds Tomcat servers and helps us build microservices, backend and REST APIs in minutes.

@SpringBootApplication

This annotation is a combination of @Configuration, @EnableAutoConfiguration and @ComponentScan.

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Typical Spring Boot REST Architecture

src/
└── main/
    ├── java/com/example/myapp/
    │   ├── config/           → Security, CORS, Swagger, etc.
    │   ├── controller/       → REST endpoints (@RestController)
    │   ├── dto/              → Request/response objects
    │   ├── exception/        → Global error handling
    │   ├── model/            → JPA entities
    │   ├── repository/       → Spring Data JPA interfaces
    │   ├── service/          → Business logic layer
    │   └── MyAppApplication.java
    └── resources/
        ├── application.properties or application.yml
        └── static/           → Public assets (if needed)

Spring Boot Starters

Has some starters which auto injects related dependencies.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
| Starter                          | Purpose                         |
| -------------------------------- | ------------------------------- |
| `spring-boot-starter-web`        | REST APIs (Spring MVC + Tomcat) |
| `spring-boot-starter-data-jpa`   | DB + JPA ORM support            |
| `spring-boot-starter-security`   | Basic auth, JWT, etc.           |
| `spring-boot-devtools`           | Live reload & dev experience    |
| `spring-boot-starter-validation` | For DTO validation              |

application.properties / application.yml

Used to configure:

  • Port

  • DB credentials

  • Profiles

  • Logging

  • CORS/Security


REST APIs

REST stands for Representational State Transfer - an architectural style used to design APIs which communicate over HTTPs on Internet between clients and servers.

In REST, everything is a resource, data which you can access and manipulate and identified by URLs.

REST APIs are stateless, they don’t store any client info and hence they should contain all necessary information needed for web services to process the request.

Common HTTP methods - GET, POST, PUT, DELETE & PATCH.

Client-Server Architecture - HTTP requests and responses are communicated between these two separate parties.

Content Negotiation - JSON/XML format

What happens behind the request, how it is processed, which service is handling it, everything is hidden from the client.


Let’s build few REST APIs

Task: Build a Magic Beast Registry Program to perform CRUD operations on Magic Beasts.

File Structure

application.properties

spring.application.name=MagicBook
spring.datasource.url = jdbc:mysql://localhost:3306/test
spring.datasource.username = root
spring.datasource.password =
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
  • Datasource can be any database server on your local system or cloud and its credentials.

  • DDL auto updates automatically creates tables for us.

  • Show sql shows the sql queries executed by JPA.

pom.xml

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

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>3.5.0</version>
        </dependency>
    </dependencies>
  • Dependencies to download - data jpa, web, mysql connector (or any other connection driver you prefer) and validation.

MagicBeast.java

@Entity
public class MagicBeast {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;
    private String type;
    private int dangerLevel;
  • Model refers to our entities to be created in DBs.

  • Always generate getters/setters or just use Lombok annotations.

MagicBeastDto.java

public class MagicBeastDto {

    private int id;

    @NotBlank
    private String name;

    @NotBlank
    private String type;

    @Min(1)
    @Max(10)
    private int dangerLevel;
  • DTO or Data Transfer Objects refer to the entities which represent our model entity in request and response bodies. We don’t expose our model entities and also we might not have to send all the fields so we create a DTO in its place.

  • Validation annotations like notblank, min, max etc are added in this class to the fields to ensure the right data according to our constraints in table has been sent.

  • If the data doesn’t validate then MethodArgumentNotValidException is thrown, which is handled in globalexceptionhandler.

MagicBeastController.java

@RestController
@RequestMapping("/magicbeasts/")
public class MagicBeastController {
    @Autowired
    MagicBeastService service;
    @GetMapping
    public ResponseEntity<List<MagicBeastDto>> getBeasts(){
        return new ResponseEntity<>(service.getBeasts(), HttpStatus.OK);
    }
    @PostMapping
    public ResponseEntity<MagicBeastDto> createBeast(@Valid @RequestBody MagicBeastDto dto){
        return new ResponseEntity<MagicBeastDto>(service.create(dto), HttpStatus.CREATED);
    }

    @GetMapping("id/{id}")
    public ResponseEntity<MagicBeastDto> getById(@PathVariable("id") int id){
        return new ResponseEntity<MagicBeastDto>(service.getById(id), HttpStatus.OK);
    }

    @PutMapping("id/{id}")
    public ResponseEntity<MagicBeastDto> update(@RequestBody MagicBeastDto dto){
        return new ResponseEntity<MagicBeastDto>(service.update(dto), HttpStatus.OK);
    }

    @DeleteMapping("id/{id}")
    public ResponseEntity<String> delete(@PathVariable("id") int id){
        service.delete(id);
        return new ResponseEntity<String>("Deleted" , HttpStatus.OK);
    }

    @GetMapping("/paginated")
    public ResponseEntity<Page<MagicBeastDto>> getPaginatedBeasts(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "5") int size
    ){
        return new ResponseEntity<>(service.getPaginatedBeasts(page, size), HttpStatus.OK);
    }


}
  • There are annotations for each HTTP Request method. The URL mapping is specified in brackets.

  • The REST Controller annotation marks this class as one and request mapping sets the root URL path.

  • Response Entity refers to the object returned to client, it has body and HTTP status code params.

  • In this Controller class, we only call the methods from service layer where these methods are implemented respectively. And that too the interface is autowired, not the implementation.

  • The variables inside URL path can be accessed by PathVariable annotation, the data sent via request body can be accessed by RequestBody and the param values set in URL path can be accessed by RequestParam.

  • For pagination, Spring Boot provides an in-built class to handle it - Page where we need two parameters to create a Pageable instance - pageNumber and size.

MagicBeastService.java

public interface MagicBeastService {

    MagicBeastDto create(MagicBeastDto dto);
    List<MagicBeastDto> getBeasts();
    MagicBeastDto getById(int id);
    MagicBeastDto update(MagicBeastDto dto);
    void delete(int id);
    Page<MagicBeastDto> getPaginatedBeasts(int page, int size);
}
  • Here we only mention the method signatures of all the methods required by us.

Before we head to implementation of MagicBeastService, we first must see -

MagicBeastRepository.java

public interface MagicBeastRepository extends JpaRepository<MagicBeast, Integer> {
}
  • It’s just an interface which extends JpaRepository which automatically creates all basic CRUD operations for us using the two values - <Entity, PrimaryKeyType>

MagicBeastServiceImpl.java


@Service
public class MagicBeastServiceImpl implements MagicBeastService{

    @Autowired
    MagicBeastRepository repo;

    public MagicBeastDto mapToDto(MagicBeast mb){
        MagicBeastDto mbDto = new MagicBeastDto();
        mbDto.setId(mb.getId());
        mbDto.setName(mb.getName());
        mbDto.setType(mb.getType());
        mbDto.setDangerLevel(mb.getDangerLevel());
        return mbDto;
    }

    public MagicBeast mapToEntity(MagicBeastDto mbto){
        MagicBeast mb = new MagicBeast();
        mb.setId(mbto.getId());
        mb.setName(mbto.getName());
        mb.setType(mbto.getType());
        mb.setDangerLevel(mbto.getDangerLevel());
        return mb;
    }

    @Override
    public MagicBeastDto create(MagicBeastDto dto) {
        return mapToDto(repo.save(mapToEntity(dto)));
    }

    @Override
    public List<MagicBeastDto> getBeasts() {
        return repo.findAll().stream().map(this::mapToDto).toList();
    }

    @Override
    public MagicBeastDto getById(int id) {
        return mapToDto(repo.findById(id).orElseThrow(() -> new ResourceNotFoundException("MagicBeast", "id", id)));
    }

    @Override
    public MagicBeastDto update(MagicBeastDto dto) {
        if (repo.existsById(dto.getId()))
            return mapToDto(repo.save(mapToEntity(dto)));
        else {
            throw new ResourceNotFoundException("MagicBeast", "id", dto.getId());
        }
    }

    @Override
    public void delete(int id) {
        repo.delete(repo.findById(id).orElseThrow(() -> new ResourceNotFoundException("MagicBeast", "id", id)));
    }

    @Override
    public Page<MagicBeastDto> getPaginatedBeasts(int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        Page<MagicBeast> pageResult = repo.findAll(pageable);
        return pageResult.map(this::mapToDto);
    }
}
  • This is the main business logic layer, here we define all the methods in service interface.

  • We start with mapping to DTO and mapping to Entity methods as JpaRepository deals with Entity class but we need to send DTO in responses and receive DTO in requests.

  • Then we implement each method easily using the MagicBeastRepository autowired in the beginning.

  • We throw ResourceNotFoundException - a custom exception class defined to be thrown wherever the id of a particular resource is not found. We only send the values here as it is defined separately.

  • A pageable instance is created from PageRequest method which is passed to findAll method of JpaRepository which implements it and sends us a Page<Entity> object.

ResourceNotFoundException.java

public class ResourceNotFoundException extends RuntimeException{
    public ResourceNotFoundException(String resource, String field, Object value){
        super(resource + " not found with " + field + " = " + value);
    }
}

GlobalExceptionHandler.java

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<Map<String, String>> handleNotFound(ResourceNotFoundException ex){
        Map<String,String> error = new HashMap<>();
        error.put("error", ex.getMessage());
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidation(MethodArgumentNotValidException ex){
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(err -> {
            errors.put(err.getField(), err.getDefaultMessage());
        });
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}
  • The RestControllerAdvice marks this class as a global scope and whenever an exception is thrown throughout our program session, it checks if it has been handled here and executes promptly.

  • Thus, two common exceptions are handled here - our custom ResourceNotFoundException and MethodArgumentNotValidException which gets thrown when the request body doesn’t pass the validation checks of DTO entities. But it works only when @Valid annotation is passed in our controller methods. Since there can be many errors, we get all the field errors and map it properly so that it can be handled properly via ResponseEntity body.

And our main class remains empty -

MagicBookApplication.java

@SpringBootApplication
public class MagicBookApplication {

    public static void main(String[] args) {
       SpringApplication.run(MagicBookApplication.class, args);
    }

}

APIs to be executed:

Sample post body:

{
    "name": "Night Howler",
    "type": "Shadow Wolf",
    "dangerLevel": 6
}

Sample get response:

[
    {
        "id": 2,
        "name": "Night Howler",
        "type": "Shadow Wolf",
        "dangerLevel": 10
    }
]

Paginated Response:

{
    "content": [
        {
            "id": 2,
            "name": "Night Howler",
            "type": "Shadow Wolf",
            "dangerLevel": 10
        }
    ],
    "pageable": {
        "pageNumber": 0,
        "pageSize": 5,
        "sort": {
            "empty": true,
            "sorted": false,
            "unsorted": true
        },
        "offset": 0,
        "paged": true,
        "unpaged": false
    },
    "totalPages": 1,
    "totalElements": 1,
    "last": true,
    "size": 5,
    "number": 0,
    "numberOfElements": 1,
    "sort": {
        "empty": true,
        "sorted": false,
        "unsorted": true
    },
    "first": true,
    "empty": false
}

Errors:

//ResourceNotFoundException
{
    "error": "MagicBeast not found with id = 1"
}

//MethodArgumentNotValidException
{
    "dangerLevel": "must be less than or equal to 10"
}

With that we come to an end with Spring Boot REST APIs, let us secure these endpoints in our next learning!

Always be RESTful adventurers!

0
Subscribe to my newsletter

Read articles from Nagraj Math directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nagraj Math
Nagraj Math