A small full-stack Spring Boot application built to practice the standard Java web stack end to end: JPA entities and relationships, a JSON REST API, a server-rendered Thymeleaf UI with AJAX, automated tests, and a CI/CD pipeline that builds and deploys the app. It manages address books, each holding a list of buddies (name, phone, address), through both the REST API and the web UI.
- Language: Java 21 (
maven.compiler.releasein pom.xml; CI uses Microsoft Build of OpenJDK 21) - Framework: Spring Boot 3.3.3 (
spring-boot-starter-parent) - Persistence: Spring Data JPA (
spring-boot-starter-data-jpa), withspring-boot-starter-data-restalso on the classpath - Database: H2 (
com.h2database:h2, runtime scope), noapplication.properties/.ymlis present, so Spring Boot's default in-memory H2 auto-configuration is used - View layer: Thymeleaf (
spring-boot-starter-thymeleaf) for HTML templates, plain JavaScript (fetch) for AJAX calls from the browser - Test frameworks: JUnit 4 (
junit:junit:4.13.2) for the model unit tests, JUnit 5/Jupiter and Spring Boot Test (spring-boot-starter-test) for the integration test,TestRestTemplatefor HTTP calls in tests - Build tool: Maven, with
maven-compiler-plugin3.11.0,maven-surefire-plugin3.2.5,spring-boot-maven-plugin(repackaging), andmaven-assembly-plugin3.6.0 (builds an additional jar-with-dependencies, see Testing/CI notes below) - CI/CD: GitHub Actions (
.github/workflows/maven.yml), building onwindows-latest, runningmvn -B -ntp clean verify, then deploying to Azure App Service (see CI/CD section)
flowchart TD
Browser["Browser<br/>(HTML + fetch AJAX)"]
Curl["REST client<br/>(curl / API consumer)"]
ViewController["AddressBookViewController<br/>(@Controller)"]
RestController["AddressBookController<br/>(@RestController, /addressbook)"]
PingController["PingController<br/>(@RestController, /ping)"]
AddressBookRepo["AddressBookRepository"]
BuddyInfoRepo["BuddyInfoRepository"]
DB[("H2, in-memory")]
Thymeleaf["Thymeleaf templates<br/>addressbooks.html / addressbook.html"]
Browser -- "GET/POST /view/..." --> ViewController
Browser -- "fetch: POST /addressbook/..." --> RestController
Curl -- "JSON over HTTP" --> RestController
Curl --> PingController
ViewController --> Thymeleaf
ViewController --> AddressBookRepo
ViewController --> BuddyInfoRepo
RestController --> AddressBookRepo
RestController --> BuddyInfoRepo
AddressBookRepo --> DB
BuddyInfoRepo --> DB
AddressBookRepo -. "AddressBook 1 --- * BuddyInfo" .- BuddyInfoRepo
BuddyInfo(org.example.model): JPA entity for a single contact (name,phone,address).setPhonestrips a leading+or0from the input before storing it. Holds a@ManyToOneback-reference to its owningAddressBook(@JsonBackReference, so it is not re-serialized inside a buddy's JSON).AddressBook(org.example.model): JPA entity with a@OneToManylist ofBuddyInfo(mappedBy = "addressBook", cascadeALL,orphanRemoval = true).addBuddy/removeBuddykeep both sides of the relationship in sync. The list is serialized to JSON (@JsonManagedReference).- Repositories (
org.example.repo):AddressBookRepositoryandBuddyInfoRepositoryareCrudRepositoryinterfaces (Spring Data JPA).BuddyInfoRepositoryaddsfindByNameandfindByPhonequery-derivation methods. AddressBookController(org.example.api,@RestController, base path/addressbook): JSON REST endpoints for creating/listing/fetching/deleting address books and adding/removing buddies.AddressBookViewController(org.example.api,@Controller): serves the Thymeleaf HTML pages and handles the classic form-post routes used by the browser UI.PingController(org.example.api,@RestController): a simple/pinghealth-check endpoint returning aPingrecord (id,ping,status,timestamp).AddressBookApplication: the@SpringBootApplicationentry point. It also defines aCommandLineRunnerbean that seeds one address book with two buddies (Nina, Alex) on every startup and prints them to the console.
All endpoints below are implemented in AddressBookController (base path /addressbook) and PingController.
| Method | Path | Request body | Response |
|---|---|---|---|
| POST | /addressbook/newbook |
none, or a JSON AddressBook (optional) |
200 OK with the created AddressBook JSON |
| GET | /addressbook |
none | 200 OK with a JSON array of all AddressBooks |
| GET | /addressbook/{id} |
none | 200 OK with the AddressBook JSON, or 404 Not Found if it does not exist |
| DELETE | /addressbook/{id}/delete |
none | 204 No Content, or 404 Not Found if it does not exist |
| POST | /addressbook/{id}/add |
JSON BuddyInfo (name, phone, address) |
200 OK with the updated AddressBook JSON, or 404 Not Found if the book does not exist |
| DELETE | /addressbook/{id}/remove/{buddyId} |
none | 200 OK with the updated AddressBook JSON, or 404 Not Found if the book or buddy does not exist |
| GET | /ping |
none | 200 OK with a JSON Ping (id, ping: "pong", status: "ok", timestamp) |
HTML view routes (AddressBookViewController, server-rendered Thymeleaf, not JSON):
GET /,GET /view,GET /view/addressbooks- list all address books (addressbooks.html)POST /view/addressbook/create- create a new address book, then redirect to its detail viewGET /view/addressbook/{id}/view- show one address book and its buddies (addressbook.html)POST /view/addressbook/{id}/addbuddy- add a buddy from an HTML form (name,phone,addressas form fields), then redirect back to the detail view
Example: create a book and add a buddy via curl.
# Create a new address book
curl -s -X POST http://localhost:8080/addressbook/newbook
# Add a buddy to book with id 1
curl -s -X POST http://localhost:8080/addressbook/1/add \
-H "Content-Type: application/json" \
-d '{"name":"Nina","phone":"+15559990100","address":"123 Maple St"}'Prerequisites: JDK 21 and Maven.
Build:
mvn clean packageRun:
mvn spring-boot:runor run the packaged jar:
java -jar target/testJava-1.0-SNAPSHOT.jarOpen http://localhost:8080/ for the HTML UI, or call the JSON API directly (for example http://localhost:8080/addressbook).
The database is H2, in-memory (no application.properties/.yml overrides Spring Boot's default). Data does not persist across restarts, and the CommandLineRunner in AddressBookApplication re-seeds one address book with two buddies (Nina, Alex) every time the app starts.
Run the tests with:
mvn testor as part of a full build with:
mvn verifyBuddyInfoTest(JUnit 4): unit tests forBuddyInfo, covering field initialization tonull,setName, and the phone-cleaning logic insetPhone(leading+or0, null input, empty input, no prefix).AddressBookTest(JUnit 4): unit tests forAddressBook, coveringaddBuddy,removeBuddy(including removing a middle element),get,size, andgetBuddies.AddressBookApiIT(JUnit 5 / Spring Boot Test,@SpringBootTestwith a random port andTestRestTemplate): an end-to-end HTTP test that creates a book viaPOST /addressbook/newbook, adds a buddy viaPOST /addressbook/{id}/add, then fetches the book viaGET /addressbook/{id}and asserts the buddy is present with the expected phone and address.
.github/workflows/maven.yml runs on every push to master (and can also be triggered manually via workflow_dispatch):
- Build job (
windows-latest): checks out the code, sets up Microsoft Build of OpenJDK 21, runsmvn -B -ntp clean verify, lists the contents oftarget, and uploadstarget\*.jar(plus an optionalweb.config) as a build artifact. - Deploy job: downloads that artifact and deploys it to Azure App Service (
azure/webapps-deploy@v3, app nameAddressbookwebapp, using a publish profile stored in theAZURE_WEBAPP_PUBLISH_PROFILEsecret).
The app was deployed to Azure App Service through this workflow; the hosting subscription has since ended.