Financial Advisor API with Spring Boot
Build a REST API with Spring Boot, JPA, and a relational data model.
Introduction
30 Second Summary
Financial advisors juggle dozens of clients, each with different goals, portfolios, and contact details. Without a system to track it all, important details slip through the cracks.
In this project, you will build a REST API backend that lets financial advisors manage their client relationships, powered by Spring Boot and an embedded database.
What You'll Build
A Java REST API that supports full create, read, update, and delete operations for financial advisors and their clients, with data persisted in an in-memory H2 database.
By the end of this project, you'll have:
- A Spring Boot application running locally with an embedded H2 database.
- Two JPA entities (Advisor and Client) connected by a one-to-many relationship.
- RESTful endpoints for full CRUD operations on both advisors and clients, tested with curl from the terminal.
- Secret Mission: Add input validation and custom error responses to your API.
Are there any prerequisites?
You should be comfortable with basic programming concepts like variables, functions, and loops. Familiarity with REST APIs and JSON is helpful but not required.
Before We Start
Building a REST API requires a working Java development environment and a properly structured project. Before writing any application code, you need the Java Development Kit installed and a Spring Boot project configured with the right dependencies.
In this step, you'll install OpenJDK 21, generate a Spring Boot project using Spring Initializr, and configure an embedded H2 database so your application starts clean.
In this step, get ready to:
- Install OpenJDK 21 on your Ubuntu machine.
- Generate and import a Spring Boot project into IntelliJ IDEA.
- Configure the H2 database and verify your application starts.
Install OpenJDK 21 on Ubuntu
Your Spring Boot application requires Java 21 to compile and run. Let's check whether Java is already available on your system.
- Open a terminal by pressing Ctrl+Alt+T.
- Check if Java is installed by running this command:
java --version
✔️ I see version 21 or higher
You're all set. Java 21 is already installed and ready to go.
ⓧ I see an older version
You have Java installed, but you need version 21 for this project. Install OpenJDK 21 alongside it by running these commands:
sudo apt update
sudo apt install openjdk-21-jdk
When prompted, enter your password and type Y to confirm the installation.
- After installation completes, verify the new version by running:
java --version
You should now see output starting with openjdk 21.
Still seeing the old version?
If java --version still shows an older version after installing, you may need to switch the active Java version. Run sudo update-alternatives --config java and select the entry that points to /usr/lib/jvm/java-21-openjdk.
help me switch my Java version to 21 on Ubuntu
ⓧ Command not found
Java is not installed yet. Install OpenJDK 21 by running these commands:
sudo apt update
sudo apt install openjdk-21-jdk
When prompted, enter your password and type Y to confirm the installation.
- After installation completes, verify by running:
java --version
You should see output starting with openjdk 21.
Installation failed?
Make sure you're connected to the internet. If apt can't find the package, try running sudo apt update again to refresh the package list.
help me install OpenJDK 21 on Ubuntu
What is OpenJDK?
OpenJDK is the open-source implementation of the Java Development Kit. It includes the compiler (javac) that turns your code into bytecode and the runtime (java) that executes it.
Spring Boot 3.x requires Java 17 or higher. This project uses Java 21 because it's the latest long-term support release.
Generate a Spring Boot project with Spring Initializr
Spring Initializr is a web tool that generates a ready-to-run Spring Boot project with your chosen dependencies already wired up. You'll use it to scaffold the project structure instead of creating everything from scratch.
- Open a browser and navigate to https://start.spring.io.
- Set Project to Maven.
- Set Language to Java.
- Set Spring Boot to the latest 3.5.x version available.
- Under Project Metadata, set Group to com.example.
- Set Artifact to advisor-api.
- Set Name to advisor-api.
- Set Description to Financial Advisor Client Management API.
- Set Packaging to Jar.
- Set Java to 21.
- Click ADD DEPENDENCIES and search for Spring Web. Select it.
- Search for Spring Data JPA and select it.
- Search for H2 Database and select it.
- Click the GENERATE button to download the project as a zip file.
Why these three dependencies?
Spring Web gives you the ability to create REST endpoints that respond to HTTP requests.
Spring Data JPA handles the mapping between your Java objects and database tables, so you don't write raw SQL.
H2 Database is an embedded in-memory database that requires zero setup. It runs inside your application and disappears when you stop it.
- Extract the downloaded zip to your home directory by running these commands:
cd ~
unzip ~/Downloads/advisor-api.zip
- Confirm the project folder was created by running:
ls ~/advisor-api
You should see files like pom.xml, mvnw, and a src folder.
Don't see the advisor-api folder?
Check your ~/Downloads folder for the zip file name. It might be named differently depending on your browser. Run ls ~/Downloads/*.zip to see what was downloaded.
help me find and extract my Spring Initializr zip file on Ubuntu
Now open the project in IntelliJ IDEA.
- Open IntelliJ IDEA.
- On the Welcome screen, click Open.
- Navigate to ~/advisor-api and select the advisor-api folder.
- Click Open.
IntelliJ IDEA detects the pom.xml and imports the project as a Maven project. Wait for the import to finish. You'll see a progress bar in the bottom status bar while IntelliJ downloads dependencies.
What is Maven?
Maven is a build tool that manages your project's dependencies, compiles your code, and packages it for deployment. The pom.xml file declares what libraries your project needs. Maven downloads them automatically.
Spring Initializr also includes a Maven Wrapper (mvnw) so you don't need Maven installed globally on your system.
Configure the H2 database and run the application
The generated project needs database configuration to know where to store data. You'll configure an in-memory H2 database and enable its web console for inspecting data later.
- In the IntelliJ Project tool window on the left, expand src > main > resources.
- Double-click application.properties to open it in the editor.
- Replace the entire contents of the file with the following configuration:
spring.application.name=advisor-api
spring.datasource.url=jdbc:h2:mem:advisordb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
What does this configuration do?
- spring.datasource.url=jdbc:h2:mem:advisordb creates an in-memory database named advisordb. Data lives only while the app runs.
- spring.h2.console.enabled=true lets you browse the database in your browser at /h2-console.
- spring.jpa.hibernate.ddl-auto=update tells JPA to create or update tables automatically based on your Java classes.
- spring.jpa.show-sql=true prints every SQL query to the console so you can see what the database is doing.
- Save the file with Ctrl+S.
- Open a terminal inside IntelliJ by clicking Terminal in the bottom tool bar (or press Alt+F12).
- Make the Maven Wrapper executable and start the application by running these commands:
chmod +x mvnw
./mvnw spring-boot:run
The first run downloads all dependencies, so it may take a minute or two. When you see a line containing Started AdvisorApiApplication and Tomcat started on port 8080, your application is running.
Seeing a build failure?
If you see an error about Java version mismatch, make sure java --version shows version 21. If it shows an older version, revisit the OpenJDK installation step above.
If you see a network error while downloading dependencies, check your internet connection and run ./mvnw spring-boot:run again.
help me fix my Spring Boot build failure
- Open your browser and navigate to http://localhost:8080/h2-console.
- In the JDBC URL field, enter jdbc:h2:mem:advisordb.
- Click Connect.
You should see the H2 Console interface with an empty database. This confirms your application and database are both working correctly.
Keep the application running
Leave the terminal running ./mvnw spring-boot:run for now. You'll restart it later when you add new code. To stop it, press Ctrl+C in the terminal.
✔️ Awesome, I've got everything!
Great. Double check you've saved application.properties and that your app is running with no errors in the terminal.
ⓧ I'd like to double check the full code
Here is the complete application.properties file:
spring.application.name=advisor-api
spring.datasource.url=jdbc:h2:mem:advisordb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
And the AdvisorApiApplication.java file (auto-generated by Spring Initializr, should already look like this):
package com.example.advisorapi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AdvisorApiApplication {
public static void main(String[] args) {
SpringApplication.run(AdvisorApiApplication.class, args);
}
}
And the pom.xml (auto-generated by Spring Initializr):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.14</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>advisor-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>advisor-api</name>
<description>Financial Advisor Client Management API</description>
<properties>
<java.version>21</java.version>
</properties>
<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.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Your development environment is ready and your Spring Boot application is running. Next up, you'll create the data model that represents financial advisors and their clients.
Create the Data Model
Your Spring Boot application can start, but it has no idea what data it manages. Before you can build API endpoints, you need to define the shape of the data your app stores.
In this step, you will create two JPA entities that map Java classes to database tables. Spring Boot automatically creates the corresponding tables in your H2 database when the application starts.
In this step, get ready to:
- Create the Advisor entity with fields for name and email.
- Create the Client entity with a many-to-one relationship to Advisor.
- Create repository interfaces for database access.
Create the Advisor entity
A JPA entity is a Java class that maps directly to a database table. Each instance of the class becomes a row in that table.
- In IntelliJ IDEA's Project panel on the left, expand src/main/java/com/example/advisorapi.
- Right-click on the advisorapi folder, select New, then Package.
- Type model and press Enter.
- Right-click on the new model package, select New, then Java Class.
- Type Advisor and press Enter.
- Replace the entire contents of Advisor.java with the following code:
package com.example.advisorapi.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.CascadeType;
import java.util.List;
@Entity
public class Advisor {
// Primary key with auto-generated ID
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
// One advisor can have many clients
@OneToMany(mappedBy = "advisor", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Client> clients;
// No-args constructor required by JPA
public Advisor() {
}
public Advisor(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public List<Client> getClients() {
return clients;
}
public void setClients(List<Client> clients) {
this.clients = clients;
}
}
What does this code do?
The @Entity annotation tells JPA to map this class to a database table called ADVISOR. Each field becomes a column.
- @Id marks id as the primary key. @GeneratedValue tells the database to auto-increment it.
- @OneToMany(mappedBy = "advisor") declares that one Advisor owns many Clients. The cascade = CascadeType.ALL setting means deleting an advisor also deletes their clients.
- The no-args constructor is required by JPA for creating entity instances from database rows.
Why jakarta.persistence instead of javax.persistence?
Spring Boot 3.x uses the Jakarta EE namespace. Older tutorials may show javax.persistence imports, but those will not compile with Spring Boot 3.5.
IntelliJ will show a red underline on Client because that class does not exist yet. You will create it next.
Create the Client entity
Each financial advisor manages multiple clients. The Client entity represents the "many" side of this one-to-many relationship.
- Right-click on the model package, select New, then Java Class.
- Type Client and press Enter.
- Replace the entire contents of Client.java with the following code:
package com.example.advisorapi.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.JoinColumn;
@Entity
public class Client {
// Primary key with auto-generated ID
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
private String phone;
// Many clients belong to one advisor
@ManyToOne
@JoinColumn(name = "advisor_id")
@JsonIgnore
private Advisor advisor;
// No-args constructor required by JPA
public Client() {
}
public Client(String firstName, String lastName, String email, String phone) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Advisor getAdvisor() {
return advisor;
}
public void setAdvisor(Advisor advisor) {
this.advisor = advisor;
}
}
What does this code do?
- @ManyToOne declares the relationship from the Client side. Many clients can belong to one advisor.
- @JoinColumn(name = "advisor_id") creates a foreign key column in the Client table that references the Advisor table.
- @JsonIgnore prevents infinite recursion when converting to JSON. Without it, serializing a Client would serialize its Advisor, which would serialize all its Clients, and so on forever.
- Save both Advisor.java and Client.java with Ctrl+S.
The red underline on Client in Advisor.java should now disappear since both classes exist in the same package.
Still seeing red underlines?
- Make sure both files are in the com.example.advisorapi.model package. Check the package statement at the top of each file.
- If IntelliJ still shows errors, click File then Invalidate Caches / Restart to refresh its index.
Why won't my entity errors go away?
Create the repository interfaces
Repositories give you database operations (create, read, update, delete) without writing any SQL. Spring Data JPA generates the implementation automatically from the interface definition.
- Right-click on the advisorapi folder in the Project panel, select New, then Package.
- Type repository and press Enter.
- Right-click on the repository package, select New, then Java Class.
- Select Interface from the kind dropdown, type AdvisorRepository, and press Enter.
- Replace the entire contents of AdvisorRepository.java with the following code:
package com.example.advisorapi.repository;
import com.example.advisorapi.model.Advisor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
// Spring Data JPA generates CRUD methods automatically
@Repository
public interface AdvisorRepository extends JpaRepository<Advisor, Long> {
}
What does this code do?
By extending JpaRepository<Advisor, Long>, this interface inherits methods like findAll(), findById(), save(), and delete(). You get full database access without writing a single line of SQL.
- Create another interface in the repository package. Right-click on repository, select New, then Java Class.
- Select Interface, type ClientRepository, and press Enter.
- Replace the entire contents of ClientRepository.java with the following code:
package com.example.advisorapi.repository;
import com.example.advisorapi.model.Client;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
// Custom query methods derived from method names
@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
List<Client> findByAdvisorId(Long advisorId);
Optional<Client> findByIdAndAdvisorId(Long id, Long advisorId);
}
What does this code do?
The ClientRepository adds two custom query methods on top of the standard CRUD operations.
- findByAdvisorId(Long advisorId) returns all clients belonging to a specific advisor.
- findByIdAndAdvisorId(Long id, Long advisorId) finds a single client only if it belongs to the specified advisor.
Spring Data JPA parses the method name and generates the SQL query automatically. No implementation code needed.
- Save both repository files with Ctrl+S.
- Run the application to verify everything compiles. Open the terminal in IntelliJ (View then Tool Windows then Terminal) and run:
./mvnw spring-boot:run
- Look for Started AdvisorApiApplication in the output. You should also see Hibernate SQL statements like create table advisor and create table client, confirming your entities were mapped to database tables.
- Press Ctrl+C in the terminal to stop the application.
Seeing a compilation error?
- Check that your package statements match the folder structure exactly. Advisor.java and Client.java should both declare package com.example.advisorapi.model; at the top.
- Check that the repository files declare package com.example.advisorapi.repository; at the top.
- Verify that your imports use jakarta.persistence (not javax.persistence).
Help me fix my Spring Boot compilation error.
✔️ Awesome, I've got everything!
Great. Double check you have saved all four new files.
ⓧ I'd like to double check the full code
Here is the complete code for each new file:
package com.example.advisorapi.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.CascadeType;
import java.util.List;
@Entity
public class Advisor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
@OneToMany(mappedBy = "advisor", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Client> clients;
public Advisor() {
}
public Advisor(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public List<Client> getClients() {
return clients;
}
public void setClients(List<Client> clients) {
this.clients = clients;
}
}
package com.example.advisorapi.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.JoinColumn;
@Entity
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
private String phone;
@ManyToOne
@JoinColumn(name = "advisor_id")
@JsonIgnore
private Advisor advisor;
public Client() {
}
public Client(String firstName, String lastName, String email, String phone) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Advisor getAdvisor() {
return advisor;
}
public void setAdvisor(Advisor advisor) {
this.advisor = advisor;
}
}
package com.example.advisorapi.repository;
import com.example.advisorapi.model.Advisor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AdvisorRepository extends JpaRepository<Advisor, Long> {
}
package com.example.advisorapi.repository;
import com.example.advisorapi.model.Client;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
List<Client> findByAdvisorId(Long advisorId);
Optional<Client> findByIdAndAdvisorId(Long id, Long advisorId);
}
Your data model is complete. Next up, you will build the REST API endpoints that let you create, read, update, and delete advisors.
Build the Financial Advisor CRUD API
Your data model is ready. You have two entities (Advisor and Client) and their repositories wired up.
Now you need a way for the outside world to interact with that data. In this step, you will create a REST controller that exposes endpoints for creating, reading, updating, and deleting financial advisors.
In this step, get ready to:
- Create the AdvisorController with GET endpoints for listing and retrieving advisors.
- Add POST, PUT, and DELETE endpoints for full CRUD operations.
- Test every endpoint using curl from the terminal.
Create the AdvisorController with GET endpoints
A REST controller is a Java class that receives HTTP requests and returns responses. Spring Boot uses annotations to map URL paths to specific methods in your controller.
- In IntelliJ's Project sidebar, right-click the com.example.advisorapi package inside src/main/java.
- Select New, then Package.
- Name the new package controller and press Enter.
- Right-click the new controller package, select New, then Java Class.
- Name the class AdvisorController and press Enter.
- Replace the entire contents of the file with the following code:
package com.example.advisorapi.controller;
import com.example.advisorapi.model.Advisor;
import com.example.advisorapi.repository.AdvisorRepository;
import org.springframework.http.HttpStatus;
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;
import java.util.List;
@RestController
@RequestMapping("/api/advisors")
public class AdvisorController {
private final AdvisorRepository advisorRepository;
// Spring injects the repository automatically via constructor injection
public AdvisorController(AdvisorRepository advisorRepository) {
this.advisorRepository = advisorRepository;
}
// GET /api/advisors - retrieve all advisors
@GetMapping
public List<Advisor> getAllAdvisors() {
return advisorRepository.findAll();
}
// GET /api/advisors/{id} - retrieve a single advisor or return 404
@GetMapping("/{id}")
public ResponseEntity<Advisor> getAdvisorById(@PathVariable Long id) {
return advisorRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
What does this code do?
@RestController tells Spring Boot this class handles HTTP requests and returns JSON responses automatically. @RequestMapping("/api/advisors") sets the base URL path for every endpoint in the class.
The constructor takes an AdvisorRepository parameter. Spring sees this and automatically provides the repository instance at startup. This pattern is called constructor injection.
The getAllAdvisors() method responds to GET requests at /api/advisors and returns every advisor in the database. The getAdvisorById() method responds to GET requests at /api/advisors/{id} and returns either the matching advisor or a 404 response.
- Save AdvisorController.java with Ctrl+S.
- Check that IntelliJ shows no red error underlines in the file. A green checkmark or no markers in the gutter means the code compiles cleanly.
Seeing red underlines?
Make sure your class is inside the controller package under com.example.advisorapi. If IntelliJ cannot find AdvisorRepository, confirm the repository file from the previous step exists in the repository package.
If imports are greyed out, IntelliJ may not have indexed the project yet. Click File, then Invalidate Caches / Restart and select Invalidate and Restart.
help me fix red underlines in my AdvisorController
Add POST, PUT, and DELETE endpoints
The GET endpoints let you read data. Now you need endpoints that modify data. POST creates a new advisor, PUT updates an existing one, and DELETE removes one.
- In AdvisorController.java, place your cursor on the blank line just before the final closing } brace of the class.
- Add the following three methods:
// POST /api/advisors - create a new advisor
@PostMapping
public ResponseEntity<Advisor> createAdvisor(@RequestBody Advisor advisor) {
Advisor savedAdvisor = advisorRepository.save(advisor);
return ResponseEntity.status(HttpStatus.CREATED).body(savedAdvisor);
}
// PUT /api/advisors/{id} - update an existing advisor
@PutMapping("/{id}")
public ResponseEntity<Advisor> updateAdvisor(@PathVariable Long id, @RequestBody Advisor advisorDetails) {
return advisorRepository.findById(id)
.map(advisor -> {
advisor.setFirstName(advisorDetails.getFirstName());
advisor.setLastName(advisorDetails.getLastName());
advisor.setEmail(advisorDetails.getEmail());
return ResponseEntity.ok(advisorRepository.save(advisor));
})
.orElse(ResponseEntity.notFound().build());
}
// DELETE /api/advisors/{id} - remove an advisor
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteAdvisor(@PathVariable Long id) {
return advisorRepository.findById(id)
.map(advisor -> {
advisorRepository.delete(advisor);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
What do these methods do?
- createAdvisor() reads the JSON request body into an Advisor object with @RequestBody, saves it to the database, and returns it with a 201 Created status.
- updateAdvisor() looks up the advisor by ID. If found, it copies the new field values and saves. If not found, it returns 404.
- deleteAdvisor() looks up the advisor by ID. If found, it deletes the record and returns 204 No Content. If not found, it returns 404.
- Save AdvisorController.java with Ctrl+S.
- Confirm there are no red error underlines anywhere in the file.
Red underlines on the new methods?
Check that your new methods are inside the class braces but after the getAdvisorById() method. A misplaced closing brace is the most common cause.
Also verify the imports at the top of the file include PostMapping, PutMapping, DeleteMapping, RequestBody, and HttpStatus. If you copied the full file from the previous chunk, all imports are already present.
help me fix errors in my POST, PUT, and DELETE methods
✔️ Awesome, I've got everything!
Great. Double check you have saved AdvisorController.java before moving on.
ⓧ I'd like to double check the full code
package com.example.advisorapi.controller;
import com.example.advisorapi.model.Advisor;
import com.example.advisorapi.repository.AdvisorRepository;
import org.springframework.http.HttpStatus;
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;
import java.util.List;
@RestController
@RequestMapping("/api/advisors")
public class AdvisorController {
private final AdvisorRepository advisorRepository;
public AdvisorController(AdvisorRepository advisorRepository) {
this.advisorRepository = advisorRepository;
}
@GetMapping
public List<Advisor> getAllAdvisors() {
return advisorRepository.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Advisor> getAdvisorById(@PathVariable Long id) {
return advisorRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Advisor> createAdvisor(@RequestBody Advisor advisor) {
Advisor savedAdvisor = advisorRepository.save(advisor);
return ResponseEntity.status(HttpStatus.CREATED).body(savedAdvisor);
}
@PutMapping("/{id}")
public ResponseEntity<Advisor> updateAdvisor(@PathVariable Long id, @RequestBody Advisor advisorDetails) {
return advisorRepository.findById(id)
.map(advisor -> {
advisor.setFirstName(advisorDetails.getFirstName());
advisor.setLastName(advisorDetails.getLastName());
advisor.setEmail(advisorDetails.getEmail());
return ResponseEntity.ok(advisorRepository.save(advisor));
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteAdvisor(@PathVariable Long id) {
return advisorRepository.findById(id)
.map(advisor -> {
advisorRepository.delete(advisor);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
}
Test all advisor endpoints using curl
Your controller is complete. Time to start the application and confirm every endpoint works by sending real HTTP requests with curl.
- Open a terminal in IntelliJ by clicking View, then Tool Windows, then Terminal.
- Start the application by running this command:
./mvnw spring-boot:run
Wait until you see a line containing Started AdvisorApiApplication in the output. This confirms the server is listening on port 8080.
Why use the Maven Wrapper?
The ./mvnw script is a Maven Wrapper bundled with your project. It downloads and uses the correct Maven version automatically, so you do not need Maven installed globally on your system.
- Open a second terminal tab by clicking the + icon in the terminal panel. Keep the first terminal running your application.
- Create a new advisor by running this command:
curl -X POST http://localhost:8080/api/advisors -H "Content-Type: application/json" -d '{"firstName":"Sarah","lastName":"Johnson","email":"sarah.johnson@example.com"}'
You should see a JSON response containing the new advisor with an id of 1, plus the first name, last name, and email you provided.
- Retrieve all advisors by running:
curl http://localhost:8080/api/advisors
You should see a JSON array containing the advisor you just created.
- Retrieve the single advisor by ID by running:
curl http://localhost:8080/api/advisors/1
You should see the same advisor object, this time returned individually rather than inside an array.
- Update the advisor's last name and email by running:
curl -X PUT http://localhost:8080/api/advisors/1 -H "Content-Type: application/json" -d '{"firstName":"Sarah","lastName":"Smith","email":"sarah.smith@example.com"}'
You should see the updated advisor with lastName now showing Smith and the new email address.
- Delete the advisor by running:
curl -X DELETE http://localhost:8080/api/advisors/1 -w "\nHTTP Status: %{http_code}\n"
You should see HTTP Status: 204 printed. A 204 means the deletion succeeded and there is no response body.
- Confirm the advisor is gone by running:
curl http://localhost:8080/api/advisors
You should see an empty array [] confirming the database has no advisors left.
Getting Connection refused?
Make sure the application is still running in your first terminal tab. Look for the Started AdvisorApiApplication message. If the terminal shows an error or has stopped, run ./mvnw spring-boot:run again.
If you see a 404 response instead, double check that your @RequestMapping annotation uses /api/advisors exactly. A missing leading slash or typo in the path will cause Spring Boot to not match the URL.
help me troubleshoot my curl requests to the advisor API
Your Advisor API is fully operational. You can create, read, update, and delete financial advisors through clean REST endpoints. Next, you will build the Client management API so each advisor can have multiple clients.
Build the Client Management API
Your advisor endpoints are working. Now you need a way to manage the clients that belong to each advisor.
In a real financial advisory firm, every client belongs to a specific advisor. Your API needs to reflect this relationship in its URL structure. Instead of a flat /api/clients endpoint, you'll nest clients under their advisor: /api/advisors/{advisorId}/clients. This is called a nested resource pattern.
In this step, get ready to:
- Create the ClientController with nested resource URLs and GET endpoints.
- Add POST, PUT, and DELETE endpoints for managing clients within an advisor.
- Test the complete client management workflow using curl.
Create the ClientController with GET endpoints
The ClientController will handle all requests for client resources. Every client URL includes the advisor's ID, which ensures clients are always accessed within the context of their advisor.
- In IntelliJ IDEA, right-click the controller package in the src/main/java/com/example/advisorapi/controller folder.
- Select New, then Java Class.
- Name it ClientController and press Enter.
- Replace the generated content with the following code:
package com.example.advisorapi.controller;
import com.example.advisorapi.model.Client;
import com.example.advisorapi.repository.AdvisorRepository;
import com.example.advisorapi.repository.ClientRepository;
import org.springframework.http.HttpStatus;
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;
import java.util.List;
// Maps all requests under /api/advisors/{advisorId}/clients to this controller
@RestController
@RequestMapping("/api/advisors/{advisorId}/clients")
public class ClientController {
// Inject both repositories so we can validate the advisor and manage clients
private final ClientRepository clientRepository;
private final AdvisorRepository advisorRepository;
public ClientController(ClientRepository clientRepository, AdvisorRepository advisorRepository) {
this.clientRepository = clientRepository;
this.advisorRepository = advisorRepository;
}
// GET /api/advisors/{advisorId}/clients - list all clients for an advisor
@GetMapping
public ResponseEntity<List<Client>> getAllClients(@PathVariable Long advisorId) {
if (!advisorRepository.existsById(advisorId)) {
return ResponseEntity.notFound().build();
}
List<Client> clients = clientRepository.findByAdvisorId(advisorId);
return ResponseEntity.ok(clients);
}
// GET /api/advisors/{advisorId}/clients/{clientId} - get a specific client
@GetMapping("/{clientId}")
public ResponseEntity<Client> getClientById(@PathVariable Long advisorId, @PathVariable Long clientId) {
if (!advisorRepository.existsById(advisorId)) {
return ResponseEntity.notFound().build();
}
return clientRepository.findByIdAndAdvisorId(clientId, advisorId)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
What does this code do?
The @RequestMapping("/api/advisors/{advisorId}/clients") annotation creates a nested resource URL. Every endpoint in this controller requires an advisor ID in the path.
Both GET methods check whether the advisor exists before doing anything with clients. If the advisor ID is invalid, the API returns a 404 immediately. This prevents confusing results when someone requests clients for a non-existent advisor.
The findByAdvisorId and findByIdAndAdvisorId methods come from the custom queries you defined in ClientRepository in the previous step.
Why nested resource URLs?
A flat URL like /api/clients would make it unclear which advisor a client belongs to. Nesting under /api/advisors/{advisorId}/clients makes the ownership relationship explicit in the URL itself.
This pattern is common in REST APIs whenever one resource is owned by another. Think of GitHub repositories nested under users: /users/{username}/repos.
- Save ClientController.java.
Add POST, PUT, and DELETE endpoints
The GET endpoints let you read client data. Now you need endpoints to create, update, and delete clients. Each write operation also validates that the advisor exists before making changes.
- In ClientController.java, find the closing brace of the getClientById method.
- Add the following three methods below it, before the final closing brace of the class:
// POST /api/advisors/{advisorId}/clients - create a new client for an advisor
@PostMapping
public ResponseEntity<Client> createClient(@PathVariable Long advisorId, @RequestBody Client client) {
return advisorRepository.findById(advisorId)
.map(advisor -> {
client.setAdvisor(advisor);
Client savedClient = clientRepository.save(client);
return ResponseEntity.status(HttpStatus.CREATED).body(savedClient);
})
.orElse(ResponseEntity.notFound().build());
}
// PUT /api/advisors/{advisorId}/clients/{clientId} - update an existing client
@PutMapping("/{clientId}")
public ResponseEntity<Client> updateClient(@PathVariable Long advisorId, @PathVariable Long clientId, @RequestBody Client clientDetails) {
if (!advisorRepository.existsById(advisorId)) {
return ResponseEntity.notFound().build();
}
return clientRepository.findByIdAndAdvisorId(clientId, advisorId)
.map(client -> {
client.setFirstName(clientDetails.getFirstName());
client.setLastName(clientDetails.getLastName());
client.setEmail(clientDetails.getEmail());
client.setPhone(clientDetails.getPhone());
return ResponseEntity.ok(clientRepository.save(client));
})
.orElse(ResponseEntity.notFound().build());
}
// DELETE /api/advisors/{advisorId}/clients/{clientId} - remove a client
@DeleteMapping("/{clientId}")
public ResponseEntity<Void> deleteClient(@PathVariable Long advisorId, @PathVariable Long clientId) {
if (!advisorRepository.existsById(advisorId)) {
return ResponseEntity.notFound().build();
}
return clientRepository.findByIdAndAdvisorId(clientId, advisorId)
.map(client -> {
clientRepository.delete(client);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
What does this code do?
- createClient looks up the advisor, calls client.setAdvisor(advisor) to establish the relationship, then saves. This links the new client to their advisor in the database.
- updateClient first checks the advisor exists, then finds the client by both its own ID and the advisor ID. This prevents editing a client that belongs to a different advisor.
- deleteClient follows the same ownership check. It uses findByIdAndAdvisorId to ensure you can only delete a client through their own advisor's URL.
- Save ClientController.java.
Seeing red underlines in IntelliJ?
Make sure you placed the new methods inside the class body (before the final }) and not after it.
If imports are unresolved, click the red text and press Alt+Enter to let IntelliJ auto-import the missing classes.
Help me fix red underlines in my ClientController.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
package com.example.advisorapi.controller;
import com.example.advisorapi.model.Client;
import com.example.advisorapi.repository.AdvisorRepository;
import com.example.advisorapi.repository.ClientRepository;
import org.springframework.http.HttpStatus;
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;
import java.util.List;
@RestController
@RequestMapping("/api/advisors/{advisorId}/clients")
public class ClientController {
private final ClientRepository clientRepository;
private final AdvisorRepository advisorRepository;
public ClientController(ClientRepository clientRepository, AdvisorRepository advisorRepository) {
this.clientRepository = clientRepository;
this.advisorRepository = advisorRepository;
}
@GetMapping
public ResponseEntity<List<Client>> getAllClients(@PathVariable Long advisorId) {
if (!advisorRepository.existsById(advisorId)) {
return ResponseEntity.notFound().build();
}
List<Client> clients = clientRepository.findByAdvisorId(advisorId);
return ResponseEntity.ok(clients);
}
@GetMapping("/{clientId}")
public ResponseEntity<Client> getClientById(@PathVariable Long advisorId, @PathVariable Long clientId) {
if (!advisorRepository.existsById(advisorId)) {
return ResponseEntity.notFound().build();
}
return clientRepository.findByIdAndAdvisorId(clientId, advisorId)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Client> createClient(@PathVariable Long advisorId, @RequestBody Client client) {
return advisorRepository.findById(advisorId)
.map(advisor -> {
client.setAdvisor(advisor);
Client savedClient = clientRepository.save(client);
return ResponseEntity.status(HttpStatus.CREATED).body(savedClient);
})
.orElse(ResponseEntity.notFound().build());
}
@PutMapping("/{clientId}")
public ResponseEntity<Client> updateClient(@PathVariable Long advisorId, @PathVariable Long clientId, @RequestBody Client clientDetails) {
if (!advisorRepository.existsById(advisorId)) {
return ResponseEntity.notFound().build();
}
return clientRepository.findByIdAndAdvisorId(clientId, advisorId)
.map(client -> {
client.setFirstName(clientDetails.getFirstName());
client.setLastName(clientDetails.getLastName());
client.setEmail(clientDetails.getEmail());
client.setPhone(clientDetails.getPhone());
return ResponseEntity.ok(clientRepository.save(client));
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{clientId}")
public ResponseEntity<Void> deleteClient(@PathVariable Long advisorId, @PathVariable Long clientId) {
if (!advisorRepository.existsById(advisorId)) {
return ResponseEntity.notFound().build();
}
return clientRepository.findByIdAndAdvisorId(clientId, advisorId)
.map(client -> {
clientRepository.delete(client);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
}
Test the complete client management workflow using curl
You have all the code in place. Now you need to restart the application so Spring Boot picks up the new controller, then test each endpoint to confirm everything works.
- Switch back to the terminal where your application is running.
- Press Ctrl+C to stop the running application.
- Restart the application by running:
./mvnw spring-boot:run
Wait until you see the startup log confirming the app is running on port 8080.
Open a second terminal tab for running your curl commands. First, create a fresh advisor to work with.
- Create an advisor by running:
curl -s -X POST http://localhost:8080/api/advisors \
-H "Content-Type: application/json" \
-d '{"firstName": "Sarah", "lastName": "Chen", "email": "sarah.chen@example.com"}'
What should I see?
You should see a JSON response with the advisor's details and an id field. Note this ID (likely 1) because you'll use it in the client URLs below.
- Create a client under that advisor by running:
curl -s -X POST http://localhost:8080/api/advisors/1/clients \
-H "Content-Type: application/json" \
-d '{"firstName": "Michael", "lastName": "Torres", "email": "m.torres@example.com", "phone": "555-0101"}'
You should see a JSON response with the new client's details and an assigned ID.
- List all clients for the advisor by running:
curl -s http://localhost:8080/api/advisors/1/clients
You should see a JSON array containing the client you just created.
- Get a specific client by ID by running:
curl -s http://localhost:8080/api/advisors/1/clients/1
You should see the single client object with all its fields.
- Update the client's phone number by running:
curl -s -X PUT http://localhost:8080/api/advisors/1/clients/1 \
-H "Content-Type: application/json" \
-d '{"firstName": "Michael", "lastName": "Torres", "email": "m.torres@example.com", "phone": "555-0202"}'
The response should show the client with the updated phone number 555-0202.
- Delete the client by running:
curl -s -X DELETE http://localhost:8080/api/advisors/1/clients/1
- Confirm the deletion by listing clients again:
curl -s http://localhost:8080/api/advisors/1/clients
You should see an empty JSON array [], confirming the client was removed.
- Test the 404 handling by requesting clients for a non-existent advisor:
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/advisors/999/clients
You should see 404 printed, confirming your advisor validation is working correctly.
Getting a 404 on valid advisor IDs?
Remember that the H2 database is in-memory. If you restarted the application, all previous data was erased. Make sure you created a new advisor first (the POST to /api/advisors) before trying to create clients.
Also check that the advisor ID in your client URL matches the ID returned when you created the advisor. It might not always be 1 if you've created and deleted advisors before.
Help me debug my client endpoints returning 404.
Your client management API is fully operational. You now have a complete backend that models the real-world relationship between financial advisors and their clients. Next up, you'll take on the Secret Mission to add input validation and custom error handling to your API.
Secret mission
Add Input Validation and Error Handling
Your API currently accepts any data you throw at it, including empty names and nonsense email addresses. In a real financial services application, bad data can cause serious downstream problems. Can you add validation rules and structured error responses?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with an in-memory H2 database. There are no cloud resources and no ongoing costs.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The advisor-api project folder on your Desktop.
- OpenJDK 21 installed on your system.
- The running Spring Boot application process.
Keep everything running
No action needed. Choose this if you are still actively building on this project or plan to extend it further.
- Your Spring Boot application continues running on port 8080.
- Your project files remain in the advisor-api folder.
- OpenJDK 21 stays installed and available for other Java projects.
Pause - I'll come back to this later
Stop the running application to free up system resources. Your project files stay on disk so you can pick up where you left off.
- In the terminal where the application is running, press Ctrl+C to stop the Spring Boot process.
- The H2 database is in-memory, so all stored data disappears when the application stops. Your code and configuration files remain intact.
- To resume later, open the advisor-api folder in IntelliJ IDEA and run ./mvnw spring-boot:run again.
Delete - I don't want to use this again
Remove all project files and optionally uninstall OpenJDK 21 from your system.
- Stop the running application by pressing Ctrl+C in the terminal.
- Delete the project folder by running:
cd ~/Desktop
rm -rf advisor-api
- If you no longer need Java on your system, uninstall OpenJDK 21 by running:
sudo apt remove openjdk-21-jdk
Should I uninstall OpenJDK?
Only remove OpenJDK if you have no other Java projects on this machine. If you plan to work with Java or Spring Boot again, keep it installed.
Nice Work!
Nice Work!
Nice work! You just built a production-ready REST API backend using Spring Boot, complete with relational data modeling and full CRUD operations.
You learned how to:
- Built a relational data model with Advisor and Client JPA entities connected by a one-to-many relationship.
- Created full CRUD REST APIs for managing advisors and their clients using nested resource URLs.
- Tested your API endpoints from the terminal using curl to verify all HTTP methods work correctly.
- Secret Mission: Added input validation with Jakarta Bean Validation and custom error handling with @ControllerAdvice.
Ready to quiz yourself?