Spring Boot REST API for Production

Spring Boot REST API for Production

Build production level REST API with industry standard framework Spring Boot in Test Driven Development environment (TDD)

Introduction

As a Java developer, mastering Spring Boot is crucial for building enterprise-level applications quickly and efficiently. In this blog post, I'll share my experience creating a REST API using Spring Boot, focusing on key concepts and best practices. Instead of a line-by-line code breakdown, I'll explain the core components and their functions, giving you a comprehensive overview of the project.

For the complete codebase, check out the GitHub repository.

Spring Academy

Spring Academy is a learning platform created by the stewards of the Spring framework. It offers a learning path for Spring Certified Professional with courses for various mastery levels. As of April 2024, all of its paid resources are accessible for free and this project was a part of it first level of mastery.

Key Concepts

Before diving into the project, let's clarify some important concepts which will clarify the project methodology.

Spring vs Spring Boot

While related, Spring and Spring Boot serve different purposes:

  • Spring: A Java framework for building enterprise-level applications with highly configurable features.

  • Spring Boot: A module built on top of the Spring Framework, simplifying setup and configuration.

    Spring Boot provides:

  • Auto configuration: It automatically configures components like the web server, database connection, and logging based on the dependencies in the project.

  • Starter dependencies: It provides a set of starter dependencies that bundle common libraries and configurations, reducing the need for manual setup.

  • Embedded servers: Spring Boot applications can be packaged as standalone, executable JAR files that include an embedded web server (e.g., Tomcat, Jetty, or Undertow).

  • Pagination: In order to send a large chunk of data it is required to send it in the form of pages that is what called pagination.

Test-Driven Development (TDD)

We'll use the TDD approach, writing tests before implementing the application code. This method guides you to write the minimum code needed to satisfy the implementation.

The Red, Green, Refactor Loop

Its an approach of code refactoring,

  1. Red: Write a failing test for the desired functionality

  2. Green: Implement the simplest solution to make the test pass

  3. Refactor: Improve the code without changing behavior

  4. Repeat...!!!

Controller Repository Architecture

We'll use a layered architecture, separating concerns into distinct modules:

Database Choice

For this project, we'll use H2, an embedded, in-memory database. It's convenient for development but has trade-offs compared to persistent databases.

Security

Security is crucial for production-level applications. We'll implement:

  1. Authentication: Verifying the identity of a principal (user or program)

  2. Authorization: Controlling access based on authenticated user roles

  3. Protection against common web exploits:

    • Cross-Site Request Forgery (CSRF)

    • Cross-Site Scripting (XSS)

Project: Cash Card Application

It is a basic REST API that allows the users to perform CRUD operations on the Database of Cash Cards with RBAC and authentication.

  • REST : Representational State Transfer. Its purpose to manage the state of the data objects within the RESTful system.

  • CRUD : Create, Read, Update, Delete. These are the basic operations that can be performed on objects in a data store.

  • RBAC: Role Based Access Controller. Each feature implementation is divided in 3 step approach - Overview, Implementation and Testing. Just like a regular CRUD API it has all the necessary endpoints like GET, POST, PUT and DELETE with a basic authentication.

API Structure

The software industry has adopted several patterns for capturing agreed upon API behavior documentation and code, these agreements are often called "contracts", here its a basic CRUD API hence I won't be including one rather provide a simple structure to understand the working.

HTTP MethodEndpointPurposeResponse StatusResponse Content
GET/cashcards/{id}Read one200 (OK)CashCard
404 (NOT FOUND)None
/cashcardsRead Many200 (OK)Spring Page Content
POST/cashcardsCreate201(CREATED)CashCard
PUT/cashcards/{id}Update204 (NO CONTENT)None
404 (NOT FOUND)None
DELETEcashcards/{id}DELETE204 (NO CONTENT)None
404 (NOT FOUND)None
*Auth faliure403 (FORBIDDEN)None

Cash Card Schema

  • id: integer

  • amount: integer

  • owner: string

Tech Stack

  • Java: Programming Language

  • Gradle: Build automation tool

  • H2: In-memory database

  • Spring Boot

  • Spring Security: Spring library to set up RBAC

Pre-requisites

Project Set-Up

Spring Initializr provide a simple interface to initialize a ready-to-run Spring application in real quick. Depending on the IDE there are different interfaces are provided here I will be covering only for VS Code or other IDEs you can follow the start-up guide here.

  • Open up VS Code and using CTRL+shift+P open the command panel and search for Spring Initializr: Create a Gradle Project and follow these steps.

  • Add following dependencies for H2 and JDBC into the /build.gradle file.

...
dependencies{
    ...
    implementation 'org.springframework.data:spring-data-jdbc'
    implementation 'com.h2database:h2'
}
...

Files and its functionality

All the essential code is limited to src directory and the following file structure is followed, where each file serves a different purpose. Below shown the tree view of the files that being utilized for the project.

/src:
    /main:
        /java/example/cashcard:
            - CashCard.java
            - CashCardApplication.java
            - CashCardController.java
            - CashCardRepository.java
            - SecurityConfig.java
        /resources:
            - schema.sql
    /test:
        /java/example/cashcard:
            - CashCardApplicationTest.java
            - CashCardJsonTest.java
        /resources:
            - data.sql

[! Attention] Bellow mentioned snippets are only a few chunks of the original files and are included to helps the reader to understand the workings of each. They are self explanatory and essential to understand the functionality. For complete files consider checking out the repository here.

Now let's dive into each file for more details,

CashCard.java

Its a bean file to provide the interface of CashCard in the codebase.

package example.cashcard;
import ...

record CashCard(@Id Long id, Double amount, String owner) {
}
CashCardApplication.java

This file defines the Spring Application for the project.

package example.cashcard;
import ...

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

This file defines the controllers for the API that includes GET, POST, POST, PUT and DELETE for respective endpoints.

package example.cashcard;
import ...

@RestController
@RequestMapping("/cashcards")
class CashCardController {
    private final CashCardRepository cashCardRepository;

    private CashCardController(CashCardRepository cashCardRepository) {
        this.cashCardRepository = cashCardRepository;
    }

    @GetMapping("/{requestedId}")
    private ResponseEntity<CashCard> findById(@PathVariable Long requestedId, Principal principal) {...}

    @PostMapping
    private ResponseEntity<Void> createCashCard(@RequestBody CashCard newCashCardRequest, UriComponentsBuilder ucb, Principal principal) {...}

    @GetMapping
    private ResponseEntity<List<CashCard>> findAll(Pageable pageable, Principal principal) {...}

    @PutMapping("/{requestedId}")
    private ResponseEntity<Void> putCashCard(@PathVariable Long requestedId, @RequestBody CashCard cashCardUpdate, Principal principal) {...}

    @DeleteMapping("/{requestedId}")
    private ResponseEntity<Void> deleteCashCard(@PathVariable Long requestedId, Principal principal) {...}

    private CashCard findCashCard(Long requestedId, Principal principal) {
        return cashCardRepository
        .findByIdAndOwner(requestedId, principal.getName());
    }
}
CashCardRepository.java

This file is a part of the Layered Architecture that we'll discuss later, that extends the provided repository with extra functionalities.

package example.cashcard;
import ...

interface CashCardRepository extends CrudRepository<CashCard, Long>, PagingAndSortingRepository<CashCard, Long>{
    CashCard findByIdAndOwner(Long id, String owner);
    boolean existsByIdAndOwner(Long id, String owner);
    Page<CashCard> findByOwner(String owner, PageRequest pageRequest);
}
SecurityConfig.java

This file enable the RBAC for the application, that allows only the owner to perform the operations on the CashCard.

package example.cashcard;
import ...

@Configuration
class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {...}

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    UserDetailsService testOnlyUsers(PasswordEncoder passwordEncoder) {...}
}
Schema.sql

File to define schema in the of the collection in the database.

CREATE TABLE cash_card
(
    ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    AMOUNT NUMBER NOT NULL DEFAULT 0,
    OWNER VARCHAR(256) NOT NULL
);
CashCardApplicationTest.java

Writing tests for each features helps to write optimized code and prevent the bugs in the long run. This file includes the tests in order to check functionality of each controller from end to end.

package example.cashcard;
import...

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CashCardApplicationTests {

    @Autowired
    TestRestTemplate restTemplate;

    @Test
    void shouldReturnACashCardWhenDataIsSaved() {...}

    @Test
    void shouldNotReturnACashCardWithAnUnknownId() {...}

    @Test
    @DirtiesContext
    void shouldCreateANewCashCard() {...}

    @Test
    void shouldReturnAllCashCardsWhenListIsRequested() {...}

    @Test
    void shouldReturnAPageOfCashCards() {...}

    @Test
    void shouldReturnASortedPageOfCashCards() {...}

    @Test
    void shouldReturnASortedPageOfCashCardsWithNoParametersAndUseDefaultValues() {...}

    @Test
    void shouldNotReturnACashCardWhenUsingBadCredentials() {...}

    @Test
    void shouldRejectUsersWhoAreNotCardOwners() {...}

    @Test
    void shouldNotAllowAccessToCashCardsTheyDoNotOwn() {...}

    @Test
    @DirtiesContext
    void shouldUpdateAnExistingCashCard() {...}

    @Test
    void shouldNotUpdateACashCardThatDoesNotExist() {...}

    @Test
    void shouldNotUpdateACashCardThatIsOwnedBySomeoneElse() {...}

    @Test
    @DirtiesContext
    void shouldDeleteAnExistingCashCard() {...}

    @Test
    void shouldNotDeleteACashCardThatDoesNotExist() {...}

    @Test
    void shouldNotAllowDeletionOfCashCardsTheyDoNotOwn() {...}
}
CashCardJsonTest.java

This file include the test to verify the structure of the requested and response data as they are in JSON hence we check them by serializing and deserializing them.

package example.cashcard;
import ...

@JsonTest
class CashCardJsonTest {

    @Autowired
    private JacksonTester<CashCard> json;

    @Autowired
    private JacksonTester<CashCard[]> jsonList;
    private CashCard[] cashCards;

    @BeforeEach
    void setUp() {
        cashCards = Arrays.array(
        new CashCard(99L, 123.45, "sarah1"),
        new CashCard(100L, 1.00, "sarah1"),
        new CashCard(101L, 150.00, "sarah1"));
    }

    @Test 
    void cashCardSerializationTest() throws IOException {...}

    @Test
    void cashCardDeserializationTest() throws IOException {...}

    @Test
    void cashCardListSerializationTest() throws IOException {...}

    @Test
    void cashCardListDeserializationTest() throws IOException {...}
}
data.sql

As we are using an in-memory database it is required to insert the test data every time we run the tests which is provided by this file.

INSERT INTO CASH_CARD(ID, AMOUNT, OWNER) VALUES (99, 123.45, 'sarah1');
INSERT INTO CASH_CARD(ID, AMOUNT, OWNER) VALUES (100, 1.00, 'sarah1');
INSERT INTO CASH_CARD(ID, AMOUNT, OWNER) VALUES (101, 150.00, 'sarah1');
INSERT INTO CASH_CARD(ID, AMOUNT, OWNER) VALUES (102, 200.00, 'kumar2');

Working of the project

As its a REST API it acts as one. This application will be deployed to a server and users can make request to communicate with the database. It provides the middle layer between users and the database, with the provided codebase you can build other functionalities on top of it and test them along the way. For testing you have to write the essential tests other than the provided ones and just enter,

./gradlew test

to run the test and if you see something like this,

You have done it... :)


Conclusion

I know that reading these much for a basic application looks unnecessary but it will provide sufficient information to gets you hand dirty and try your own. Our ultimate task is to deliver a production level application requires attention to details and solid understanding of core concepts which takes time. This blog isn't meant for the understanding of spring boot instead it helps to getting started with spring boot. Its lacks the in depth working of the Spring Framework and so do I hence stay tuned for the next one.

Thank you for reading...