Build a Job Application Tracker API
Build a persistent Java REST API to track job applications.
Introduction
30 Second Summary
Job searches generate a trail of companies, roles, notes, and changing outcomes. Scattered records make it harder to remember where each application stands.
In this project, you will build a local Java REST API with Spring Boot for tracking internship and job applications. You will publish the tested tracker as a documented GitHub repository that you can explain in interviews.
What You'll Build
When you demo the finished tracker, Windows PowerShell shows a newly stored job application with a generated ID that remains available after a server restart.
By the end of this project, you'll have:
- A working REST API that accepts JSON records. Each successful request returns 201 Created with a generated ID.
- A persistent tracker that keeps saved data after a server restart. You can list records. You can change a status to INTERVIEWING. You can delete old entries.
- A portfolio-ready GitHub repository with a passing MockMvc test. Its README shows another developer how to run the API. It also gives you architecture notes plus interview talking points.
- Secret Mission: Add a status-specific endpoint through Spring Data query derivation without writing SQL by hand.
Are there any prerequisites?
You need a Windows laptop with internet access plus your existing GitHub account. The guide checks IntelliJ IDEA, Eclipse Temurin JDK 21, and Git for Windows before you build the API.
Before We Start
Your first commitment is to define the tracker you are building. This gives every later decision a clear purpose for your internship or job search.
Set Up Your Java Workspace
A reliable backend starts with tools you can identify and verify. Your Java Development Kit, editor, build system, and version control must agree before you add application code.
This step prepares IntelliJ IDEA, Eclipse Temurin JDK 21, Git, and a Maven project. The final build check proves that your workspace can compile the generated Spring Boot application.
In this step, get ready to:
- Generate a Spring Boot Maven project with the required dependencies.
- Select Eclipse Temurin JDK 21 for the project.
- Configure Git before verifying the complete workspace.
Generate and open the Spring Boot project
Spring Initializr creates the project structure plus the Maven Wrapper. The wrapper lets you build with the project's configured Maven distribution without installing Maven separately.
- Press the Windows key to open the Windows search bar.
- Type IntelliJ IDEA into the search bar.
- Press Enter to launch IntelliJ IDEA.
✔️ IntelliJ IDEA opens
IntelliJ IDEA is available. Leave it open while you generate the project.
ⓧ IntelliJ IDEA is missing
The unified IntelliJ IDEA product includes free core functionality for Java development.
- Download IntelliJ IDEA from the official installation guide.
- Complete the Windows installation with the default options.
- Press the Windows key after installation.
- Type IntelliJ IDEA into the search bar.
- Press Enter to launch IntelliJ IDEA.
- Open Spring Initializr in your browser.
- Select Maven under Project.
- Select Java under Language.
- Select Spring Boot version 4.1.2.
- Enter com.example in the Group field.
- Enter job-application-tracker in the Artifact field.
- Enter com.example.jobtracker in the Package name field.
- Select Java version 21.
Why use Spring Initializr?
Spring Initializr generates a standard Spring Boot layout with Maven Wrapper scripts. It also records your selected dependencies in pom.xml.
This gives every developer the same project structure. It also keeps the setup reproducible.
- Add Spring Web to Dependencies.
- Add Spring Data JPA to Dependencies.
- Add Validation to Dependencies.
- Add H2 Database to Dependencies.
- Click Generate to download the project ZIP.
- Open the downloaded ZIP in File Explorer.
- Click Extract all in the File Explorer toolbar.
- Set the extraction destination to your Desktop.
The extracted job-application-tracker folder contains mvnw.cmd, pom.xml, and the generated source directories.
- Return to IntelliJ IDEA.
- Click Open on the Welcome screen.
- Select pom.xml inside the Desktop's job-application-tracker folder.
- Click OK.
- Click Open as Project when IntelliJ IDEA asks how to open the Maven file.
Good progress. IntelliJ IDEA now recognizes the tracker as a Maven project and begins loading its dependency model.
Project does not open as Maven
Close the current editor window if IntelliJ IDEA opened pom.xml as a plain file. Use File, then Open, then select the top-level pom.xml again.
Choose Open as Project when the choice appears. Help me open this pom.xml as a Maven project.
Select JDK 21 and configure Git
The IDE can run on its bundled runtime while your project still lacks a standalone JDK. Your tracker uses Eclipse Temurin JDK 21 so the editor, terminal, and Maven build use the same Java version.
- Open IntelliJ IDEA's Terminal tool window by pressing Alt+F12.
- Check the Java version by running this command:
java -version
✔️ I see Java 21
Your terminal already finds Java 21. Confirm that IntelliJ IDEA assigns the same JDK to this project.
- Open File from IntelliJ IDEA's main menu.
- Select Project Structure.
- Select Project under Project Settings.
- Select the JDK 21 installation in Project SDK.
- Click OK to save the project SDK.
ⓧ I see an older Java version
The terminal is finding an older JDK. Download Eclipse Temurin JDK 21 through the project settings.
- Open File from IntelliJ IDEA's main menu.
- Select Project Structure.
- Select Project under Project Settings.
- Open the Project SDK list.
- Select Download JDK.
- Select version 21.
- Select Eclipse Temurin as the vendor.
- Complete the download.
- Select the downloaded JDK in Project SDK.
- Click OK to save the project SDK.
ⓧ Java is not recognized
No standalone JDK is available to this terminal. IntelliJ IDEA can download Eclipse Temurin JDK 21 for the project.
- Open File from IntelliJ IDEA's main menu.
- Select Project Structure.
- Select Project under Project Settings.
- Open the Project SDK list.
- Select Download JDK.
- Select version 21.
- Select Eclipse Temurin as the vendor.
- Complete the download.
- Select the downloaded JDK in Project SDK.
- Click OK to save the project SDK.
- Press Ctrl+Alt+S to open IntelliJ IDEA's settings.
- Select Tools.
- Select Terminal.
- Enable Add project JDK to PATH.
- Click OK to save the terminal setting.
- Close the current Terminal session.
- Open a fresh Command Prompt session from New Predefined Session in the Terminal toolbar.
- Confirm the selected JDK by running this command:
java -version
You should now see Java version 21 in the output. This confirms that the project SDK is also available to new terminal sessions.
- Check whether Git for Windows is available by running this command:
git --version
✔️ I see a Git version
Git for Windows is ready. Continue with your commit identity.
ⓧ Git is not recognized
Git for Windows is missing from the machine or unavailable to the current terminal.
- Download the maintained Windows build from the official Git for Windows page.
- Run the downloaded installer.
- Keep the default installer options.
- Close the current Terminal session after installation.
- Open a fresh Command Prompt session from IntelliJ IDEA's Terminal toolbar.
- Check the installation by running:
git --version
You should now see a Git version in the output.
Git records an author name plus an email address in every commit. Configure the identity you want attached to this portfolio project.
- Replace the example name with your commit name in the first command below.
- Replace the example email with the email connected to your GitHub account in the second command below.
- Configure your global commit identity by running:
git config --global user.name "Your Name"
git config --global user.email "YOUR_EMAIL"
What do these commands change?
The global Git configuration supplies your author identity to repositories on this Windows account. Future commits can use it without asking again.
- Confirm the stored commit name by running:
git config --global user.name
You should see the commit name you entered.
- Confirm the stored commit email by running:
git config --global user.email
You should see the commit email you entered.
Git still unavailable?
A terminal opened before the installation keeps its old environment settings. Close every IntelliJ IDEA Terminal session before opening a new Command Prompt session.
If the command still fails, restart IntelliJ IDEA so it reloads the Windows environment. Help me troubleshoot Git for Windows in IntelliJ IDEA.
Align the Maven configuration and verify the build
The Project Object Model in pom.xml defines the Java version, Spring Boot parent, dependencies, and build plugin. Replacing the generated file with the project version removes any differences caused by changing Initializr defaults.
Why use managed dependency versions?
The Spring Boot parent manages compatible versions for Spring Web MVC, Spring Data JPA, Jakarta Validation, and H2 Database. Each child dependency can omit its own version.
This keeps the dependency set aligned with Spring Boot 4.1.2.
- Open pom.xml from IntelliJ IDEA's Project tool window.
- Compare the file with the complete project version in the tabs below.
✔️ Awesome, I've got everything!
Your pom.xml matches the complete version. Save it before continuing.
ⓧ I'd like to double check the full code
Use this exact file so the parent version, project metadata, dependencies, and Maven plugin match the project artifact.
- Select every line in pom.xml.
- Replace the selected contents with the complete file below:
<?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>4.1.2</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>job-application-tracker</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>job-application-tracker</name>
<description>A portfolio-ready API for tracking job applications</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-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>
- Save pom.xml by pressing Ctrl+S.
The final check tests every part of the workspace together. The first Maven Wrapper build can take a few minutes while it downloads the configured Maven distribution plus the project dependencies.
Before you run the checks, do you expect Java, Git, and Maven to report three successful results?
- Run the complete workspace check from the project-root Command Prompt:
java -version
git --version
mvnw.cmd clean install
What does this verification prove?
- The first command proves that the terminal uses Java 21.
- The second command proves that Git for Windows is available.
- The Maven Wrapper command compiles the generated project.
- The build also resolves every dependency declared in pom.xml.
You should see Java 21, a Git version, and BUILD SUCCESS. That is the workspace proven from editor to build output.
Build does not finish successfully?
- Check that the Command Prompt starts inside the job-application-tracker folder containing mvnw.cmd.
- Compare pom.xml with the full-code tab for missing XML tags or dependency entries.
- Confirm that java -version reports Java 21 before retrying the Maven Wrapper build.
Help me diagnose my Maven Wrapper build failure.
Your Java workspace can now compile the generated Spring Boot project with a reproducible Maven build. Next, you'll create the tracker model and expose its first readable API endpoint.
Build the First API Endpoint
Your Java workspace is ready. You can now turn the generated project into a running Spring Boot service with its first REST API endpoint.
A visible JSON response proves that the application can start. It also proves that the controller can reach the repository before you add write operations or database settings.
In this step, get ready to:
- Define the application entry point plus the job application data model.
- Connect a repository to a controller through constructor injection.
- Start the API and receive an empty collection from `GET /applications`.
Define the application data
A JPA entity describes the data stored by the database. A request record describes the input that future API calls can accept.
How the model fits together
The status enum limits each application to a known stage. The entity represents a stored application.
The request record keeps incoming API data separate from the stored entity. That separation gives each layer a clear job.
- In IntelliJ IDEA's Project tool window, select the com.example.jobtracker package.
- Press Alt+Insert to open the New menu.
- Select Java Class.
- Enter JobApplicationTrackerApplication.
- Press Enter to create the class.
- Replace the generated contents of JobApplicationTrackerApplication.java by pasting this code:
package com.example.jobtracker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JobApplicationTrackerApplication {
public static void main(String[] args) {
SpringApplication.run(JobApplicationTrackerApplication.class, args);
}
}
What does this class do?
- `@SpringBootApplication` marks this class as the starting point for configuration and component discovery.
- `main()` gives Java an entry point for the application.
- `SpringApplication.run()` starts Spring Boot with `JobApplicationTrackerApplication` as its source.
- Save JobApplicationTrackerApplication.java.
- Switch back to the project-root Command Prompt from earlier.
- Confirm the entry point compiles by running this command:
mvnw.cmd clean install
What does this build prove?
The Maven Wrapper compiles the project with its configured Maven distribution. A successful build confirms that Java can load the application entry point.
You should see BUILD SUCCESS. Your Spring Boot entry point now compiles.
Build stopped after adding the entry point?
Check that the file is inside src/main/java/com/example/jobtracker. Confirm that the package line is package com.example.jobtracker;.
Compare every import with the code block above. A missing Spring import prevents the entry point from compiling.
Ask for help with the build output.
- Repeat the same Alt+Insert and Java Class flow to create ApplicationStatus.java inside com.example.jobtracker.
- Replace the generated contents of ApplicationStatus.java by pasting this code:
package com.example.jobtracker;
public enum ApplicationStatus {
WISHLIST,
APPLIED,
INTERVIEWING,
OFFER,
REJECTED
}
What does this enum represent?
`ApplicationStatus` defines the stages available to the tracker. Java prevents the application from using an undefined status value.
- Save ApplicationStatus.java.
- Confirm that IntelliJ IDEA shows no red syntax highlights in the enum.
- Repeat the same Alt+Insert and Java Class flow to create JobApplication.java inside com.example.jobtracker.
- Replace the generated contents of JobApplication.java with this first complete version:
package com.example.jobtracker;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class JobApplication {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String company;
private String role;
@Enumerated(EnumType.STRING)
private ApplicationStatus status;
private String notes;
protected JobApplication() {
}
}
What does this entity define?
- `@Entity` tells JPA that `JobApplication` represents stored data.
- `@Id` identifies the primary key field.
- `GenerationType.IDENTITY` lets the database generate each `id`.
- `EnumType.STRING` stores readable status names such as `APPLIED`.
- The protected no-argument constructor lets JPA create entity objects.
- Save JobApplication.java.
- Confirm the entity compiles by running this command in the project-root Command Prompt:
mvnw.cmd clean install
What does this build check?
This build checks the enum plus the first entity version together. It confirms that the JPA annotations and status type resolve correctly.
You should see BUILD SUCCESS again. The database model is valid Java code.
- Return to JobApplication.java in IntelliJ IDEA.
- Place the cursor after the protected no-argument constructor.
- Insert this constructor plus the first three getters before the final closing brace:
public JobApplication(String company, String role, ApplicationStatus status, String notes) {
this.company = company;
this.role = role;
this.status = status;
this.notes = notes;
}
public Long getId() {
return id;
}
public String getCompany() {
return company;
}
public String getRole() {
return role;
}
Why add these methods?
The public constructor creates a populated application from API input. The getters expose the generated ID plus the company and role when Spring converts the entity to JSON.
- Save JobApplication.java.
- Confirm that IntelliJ IDEA shows no red syntax highlights around the constructor or getters.
- Place the cursor after getRole().
- Insert the remaining access methods before the final closing brace:
public ApplicationStatus getStatus() {
return status;
}
public String getNotes() {
return notes;
}
public void setStatus(ApplicationStatus status) {
this.status = status;
}
Why expose status and notes?
The final getters include the status and notes in API responses. `setStatus()` supports the status update operation that you add later.
- Save JobApplication.java.
- Confirm the completed entity compiles by running this command:
mvnw.cmd clean install
What does this build confirm?
The build now checks the complete entity. It confirms that the constructor plus every access method belongs inside the class.
You should see BUILD SUCCESS. The full entity is ready for the repository.
- Repeat the same Alt+Insert and Java Class flow to create JobApplicationRequest.java inside com.example.jobtracker.
- Replace the generated contents of JobApplicationRequest.java by pasting this code:
package com.example.jobtracker;
import jakarta.validation.constraints.NotBlank;
public record JobApplicationRequest(
@NotBlank(message = "Company is required") String company,
@NotBlank(message = "Role is required") String role,
ApplicationStatus status,
String notes
) {
}
What does this request record do?
- `JobApplicationRequest` groups the four values accepted from a future request body.
- `@NotBlank` rejects missing company names.
- The second `@NotBlank` rejects missing role names.
- The status and notes remain optional at this stage.
- Save JobApplicationRequest.java.
- Confirm the complete data model compiles by running this command:
mvnw.cmd clean install
What does this checkpoint cover?
This build checks the entry point plus all three data types. It catches misplaced braces and unresolved validation imports before the web layer is added.
You should see BUILD SUCCESS. The application now has a validated data model.
Connect the repository and controller
A repository gives the application database operations without handwritten SQL. The controller receives that repository through dependency injection.
Why constructor injection?
The constructor makes `JobApplicationRepository` an explicit requirement of the controller. Spring supplies the repository when it creates `JobApplicationController`.
- Repeat the same Alt+Insert and Java Class flow to create JobApplicationRepository.java inside com.example.jobtracker.
- Replace the generated contents of JobApplicationRepository.java by pasting this code:
package com.example.jobtracker;
import org.springframework.data.jpa.repository.JpaRepository;
public interface JobApplicationRepository extends JpaRepository<JobApplication, Long> {
}
What does the repository provide?
Extending `JpaRepository<JobApplication, Long>` gives the project operations for `JobApplication` entities. The `Long` type matches the entity's `id` field.
- Save JobApplicationRepository.java.
- Confirm that IntelliJ IDEA resolves JpaRepository without red syntax highlights.
- Repeat the same Alt+Insert and Java Class flow to create JobApplicationController.java inside com.example.jobtracker.
- Replace the generated contents of JobApplicationController.java by pasting this code:
package com.example.jobtracker;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/applications")
public class JobApplicationController {
private final JobApplicationRepository repository;
public JobApplicationController(JobApplicationRepository repository) {
this.repository = repository;
}
@GetMapping
public List<JobApplication> getAll() {
return repository.findAll();
}
}
How does the endpoint work?
- `@RestController` gives each mapped method response-body behaviour.
- `@RequestMapping("/applications")` sets the shared path for this controller.
- The constructor receives `JobApplicationRepository` from Spring.
- `@GetMapping` connects an HTTP GET request to `getAll()`.
- `repository.findAll()` reads every stored application.
- Save JobApplicationController.java.
- Confirm that IntelliJ IDEA shows no red syntax highlights in the controller.
✔️ Awesome, I've got everything!
Great. Save every Java file before starting the application.
ⓧ I'd like to double check the full code
Compare each file below with your project. These reference files match the complete state for this step.
<?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>4.1.2</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>job-application-tracker</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>job-application-tracker</name>
<description>A portfolio-ready API for tracking job applications</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-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>
What does the POM control?
The POM keeps Java at `21`. It also gives the application its web, persistence, validation, database, and test dependencies.
package com.example.jobtracker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JobApplicationTrackerApplication {
public static void main(String[] args) {
SpringApplication.run(JobApplicationTrackerApplication.class, args);
}
}
What starts the application?
`JobApplicationTrackerApplication` contains the Java entry point. Spring Boot starts from this class.
package com.example.jobtracker;
public enum ApplicationStatus {
WISHLIST,
APPLIED,
INTERVIEWING,
OFFER,
REJECTED
}
What limits the status values?
`ApplicationStatus` contains every stage currently supported by the tracker.
package com.example.jobtracker;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class JobApplication {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String company;
private String role;
@Enumerated(EnumType.STRING)
private ApplicationStatus status;
private String notes;
protected JobApplication() {
}
public JobApplication(String company, String role, ApplicationStatus status, String notes) {
this.company = company;
this.role = role;
this.status = status;
this.notes = notes;
}
public Long getId() {
return id;
}
public String getCompany() {
return company;
}
public String getRole() {
return role;
}
public ApplicationStatus getStatus() {
return status;
}
public String getNotes() {
return notes;
}
public void setStatus(ApplicationStatus status) {
this.status = status;
}
}
What does the entity store?
`JobApplication` stores the generated ID plus the company, role, status, and notes.
package com.example.jobtracker;
import jakarta.validation.constraints.NotBlank;
public record JobApplicationRequest(
@NotBlank(message = "Company is required") String company,
@NotBlank(message = "Role is required") String role,
ApplicationStatus status,
String notes
) {
}
What validates future input?
`JobApplicationRequest` requires non-blank company and role values.
package com.example.jobtracker;
import org.springframework.data.jpa.repository.JpaRepository;
public interface JobApplicationRepository extends JpaRepository<JobApplication, Long> {
}
What reaches the database?
`JobApplicationRepository` provides the database operations used by the controller.
package com.example.jobtracker;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/applications")
public class JobApplicationController {
private final JobApplicationRepository repository;
public JobApplicationController(JobApplicationRepository repository) {
this.repository = repository;
}
@GetMapping
public List<JobApplication> getAll() {
return repository.findAll();
}
}
What serves the first endpoint?
`JobApplicationController` maps `GET /applications` to the repository's `findAll()` operation.
Run and call the endpoint
Spring Boot can now assemble the application from the classes you created. The included H2 Database starts in memory because no persistent database URL has been configured.
- Switch back to the project-root Command Prompt.
- Start the application through the Maven Wrapper by running this command:
.\mvnw.cmd spring-boot:run
What does this command do?
The Maven Wrapper launches the Spring Boot Maven plugin. Spring Boot starts the embedded web server plus the in-memory H2 database.
You should see the startup logs finish while the Command Prompt remains occupied. Keep this process running so it can receive the request.
Application stopped during startup?
Read the first reported cause in the Command Prompt. A compile failure usually points to a Java file with a missing import or misplaced brace.
Confirm that every Java file uses package com.example.jobtracker;.
Ask for help with the startup output.
- Press the Windows key to open Windows search.
- Enter Windows PowerShell.
- Press Enter to open a second terminal.
Before you send the request, what should an application tracker with no saved records return?
- Call the running endpoint from Windows PowerShell by running this command:
Invoke-RestMethod -Method 'Get' -Uri 'http://localhost:8080/applications'
How does the request travel?
- Windows PowerShell sends an HTTP GET request to `/applications`.
- Spring MVC routes the request to `getAll()` in `JobApplicationController`.
- `getAll()` calls `repository.findAll()`.
- The repository queries the in-memory H2 database.
- Spring converts the empty result into a JSON collection for PowerShell.
You should see an empty collection. That result proves the client, controller, repository, and database are connected.
That is your first readable API endpoint working from end to end. Next, you will create real applications and discover what an in-memory database does after a restart.
Create and Manage Applications
Your REST API already responds with an empty list. The next goal is to turn that readable endpoint into a tracker that accepts real changes.
A useful tracker must create records. It also needs to update statuses plus remove unwanted records. You will finish by testing whether the default H2 Database keeps those records through a restart.
In this step, get ready to:
- Create validated job applications with generated IDs.
- Manage saved applications through status changes plus deletion.
- Test the default database through an application restart.
Create a validated application
An HTTP POST request sends a new resource to the API. Your controller will validate the incoming JSON before saving it. A successful request returns 201 Created with the stored application.
- Switch back to the Command Prompt running the API.
- Stop the API by pressing Ctrl+C.
- In IntelliJ IDEA, locate src/main/java/com/example/jobtracker/JobApplicationController.java.
- Replace the imports below import java.util.List; with this complete import group:
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
What do these imports unlock?
These imports give the controller the Spring MVC annotations needed for POST, PATCH, plus DELETE requests. They also provide validation plus HTTP response handling.
Some imports support methods you add later in this step. Adding the complete group now keeps the controller imports organized.
- Save JobApplicationController.java.
- Confirm the new imports compile by starting the API from the project-root Command Prompt:
.\mvnw.cmd spring-boot:run
What does this command confirm?
The Maven Wrapper compiles the controller before Spring Boot starts the embedded server. A successful startup proves that every imported type is available to the project.
- Wait until the startup output confirms that the application has started.
- Stop the API by pressing Ctrl+C.
- Return to JobApplicationController.java.
- Add this method directly below getAll() to accept new applications:
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public JobApplication create(@Valid @RequestBody JobApplicationRequest request) {
ApplicationStatus status = request.status() == null
? ApplicationStatus.WISHLIST
: request.status();
JobApplication application = new JobApplication(
request.company(),
request.role(),
status,
request.notes()
);
return repository.save(application);
}
How does creation work?
- The @Valid annotation activates the rules in JobApplicationRequest before the method runs.
- A missing request status becomes WISHLIST so every saved application has a useful starting state.
- The repository.save(application) call stores the entity. H2 generates its ID.
- The @ResponseStatus(HttpStatus.CREATED) annotation changes the successful response to 201 Created.
- Save JobApplicationController.java.
- Start the updated API from the project-root Command Prompt:
.\mvnw.cmd spring-boot:run
What is running now?
The restarted server now maps POST requests at /applications to create(). Keep this Command Prompt running while you send requests from a separate window.
- Press the Windows key to open system search.
- Type Windows PowerShell into the search field.
- Press Enter to open Windows PowerShell.
- Create an application by sending this JSON request:
$body = @{
company = 'Contoso'
role = 'Backend Intern'
status = 'APPLIED'
notes = 'Campus referral'
} | ConvertTo-Json
Invoke-RestMethod -Method 'Post' -Uri 'http://localhost:8080/applications' -Body $body -ContentType 'application/json'
What does this request send?
The first statement converts four PowerShell values into JSON. Invoke-RestMethod sends that JSON to the POST endpoint.
PowerShell deserializes the response into an object you can read. You should see Contoso with a generated id plus the status APPLIED.
That is your first complete write operation. The tracker can now validate JSON before returning the stored record with its database-generated ID.
Did the create request fail?
Check the Command Prompt from earlier to confirm the API is still running. Make sure the PowerShell request includes non-blank values for company plus role.
Compare the request URL with http://localhost:8080/applications. A different path cannot reach create().
Help me diagnose the failed POST request.
Update and delete applications
A saved application changes as your job search moves forward. A PATCH endpoint updates one field without replacing the entire record. A DELETE endpoint removes a record that no longer belongs in the tracker.
- Switch back to the Command Prompt running the API.
- Stop the API by pressing Ctrl+C.
- Return to JobApplicationController.java.
- Add this status update method directly below create().
@PatchMapping("/{id}/status")
public JobApplication updateStatus(
@PathVariable Long id,
@RequestParam ApplicationStatus status
) {
JobApplication application = repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Application not found"
));
application.setStatus(status);
return repository.save(application);
}
How does the update stay safe?
- The @PathVariable annotation reads the application ID from the URL.
- The @RequestParam annotation converts the query value into an ApplicationStatus.
- The findById() call retrieves the matching entity. A missing ID triggers 404 Not Found.
- The existing entity receives its new status before the repository saves it again.
- Save JobApplicationController.java.
- Start the API from the project-root Command Prompt:
.\mvnw.cmd spring-boot:run
What does this startup verify?
A successful startup confirms that the PATCH mapping compiles. The running server can now change the status of a saved application.
- Return to the Windows PowerShell window from the previous substep.
- Recreate application 1 in the restarted in-memory database:
Invoke-RestMethod -Method 'Post' -Uri 'http://localhost:8080/applications' -Body $body -ContentType 'application/json'
Why create the record again?
The controller restart gives you a fresh process for testing the new endpoint. This request provides application 1 for the PATCH operation.
- Change application 1 from APPLIED to INTERVIEWING by running:
Invoke-RestMethod -Method 'Patch' -Uri 'http://localhost:8080/applications/1/status?status=INTERVIEWING'
What should the update return?
You should see application 1 with its status changed to INTERVIEWING. The company plus role remain unchanged.
The same entity now reflects a new stage in your job search. Your PATCH endpoint updates only the status the request names.
- Switch back to the Command Prompt running the API.
- Stop the API by pressing Ctrl+C.
- Return to JobApplicationController.java.
- Add this deletion method directly below updateStatus().
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
if (!repository.existsById(id)) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Application not found"
);
}
repository.deleteById(id);
}
How does deletion respond?
The repository checks the ID before deletion. A missing record produces 404 Not Found instead of pretending the operation succeeded.
A successful deletion returns 204 No Content. The empty response body matches an operation that has nothing left to return.
- Save JobApplicationController.java.
✔️ Awesome, I've got everything!
Your controller now supports reading, creating, updating, plus deleting applications. Double check that JobApplicationController.java is saved.
ⓧ I'd like to double check the full code
Compare your controller with this complete version. The imports plus method order should match exactly.
package com.example.jobtracker;
import java.util.List;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/applications")
public class JobApplicationController {
private final JobApplicationRepository repository;
public JobApplicationController(JobApplicationRepository repository) {
this.repository = repository;
}
@GetMapping
public List<JobApplication> getAll() {
return repository.findAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public JobApplication create(@Valid @RequestBody JobApplicationRequest request) {
ApplicationStatus status = request.status() == null
? ApplicationStatus.WISHLIST
: request.status();
JobApplication application = new JobApplication(
request.company(),
request.role(),
status,
request.notes()
);
return repository.save(application);
}
@PatchMapping("/{id}/status")
public JobApplication updateStatus(
@PathVariable Long id,
@RequestParam ApplicationStatus status
) {
JobApplication application = repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Application not found"
));
application.setStatus(status);
return repository.save(application);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
if (!repository.existsById(id)) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Application not found"
);
}
repository.deleteById(id);
}
}
What should match?
Your file should contain one controller class with four mapped methods. The constructor-injected repository remains the single path from the controller to H2.
- Start the complete API from the project-root Command Prompt:
.\mvnw.cmd spring-boot:run
What is available now?
The running API now exposes GET, POST, PATCH, plus DELETE behavior under /applications. Keep the process running for the next requests.
- Return to Windows PowerShell.
- Create application 1 for the deletion check:
Invoke-RestMethod -Method 'Post' -Uri 'http://localhost:8080/applications' -Body $body -ContentType 'application/json'
What should the create request return?
You should receive the saved Contoso application with ID 1. Its initial request status is APPLIED.
- Delete application 1 by running:
Invoke-RestMethod -Method 'Delete' -Uri 'http://localhost:8080/applications/1'
What does the empty result mean?
PowerShell prints no response object because the endpoint returns 204 No Content. The record has been removed successfully.
The next request intentionally targets the deleted ID. This expected failure confirms that your missing-record handling works.
- Call the DELETE endpoint for application 1 again:
Invoke-RestMethod -Method 'Delete' -Uri 'http://localhost:8080/applications/1'
Why is this failure expected?
PowerShell reports a failed request carrying the 404 Not Found status. The controller checked the missing ID before attempting another deletion.
Did the update or deletion behave differently?
Confirm that each request targets application 1. Recreate the record after every server restart because the current database uses default in-memory storage.
Check the Command Prompt logs if PowerShell reports a different failure. The logs identify which controller method received the request.
Help me debug my PATCH or DELETE request.
Test what survives a restart
Your endpoints now cover the main application lifecycle. The remaining question is whether the current database preserves that lifecycle after the server process ends.
- Create one more application in the current server process:
Invoke-RestMethod -Method 'Post' -Uri 'http://localhost:8080/applications' -Body $body -ContentType 'application/json'
What has been stored?
The response shows a generated ID with the Contoso application data. This record gives you a concrete value to look for after the restart.
- Confirm that the new record is currently available by listing every application:
Invoke-RestMethod -Method 'Get' -Uri 'http://localhost:8080/applications'
What should you see before restarting?
PowerShell should display the Contoso application you just created. This proves that the record exists before the server stops.
- Switch back to the Command Prompt running the API.
- Stop the API by pressing Ctrl+C.
- Restart the API from the same project-root Command Prompt:
.\mvnw.cmd spring-boot:run
What changed during the restart?
Spring Boot has created a new application process. The GET endpoint is ready for the check that reveals what happened to the stored record.
Before you check, do you expect the Contoso application to remain after this restart?
- Return to Windows PowerShell.
- List the applications again to test your prediction:
Invoke-RestMethod -Method 'Get' -Uri 'http://localhost:8080/applications'
Why did the application disappear?
PowerShell now shows no application rows. The API has returned an empty collection because the default in-memory H2 database lost its contents when the earlier server process stopped.
In-memory storage keeps this learning loop fast. A real tracker needs storage that survives process restarts.
You have now exposed the tracker's biggest reliability gap through a real experiment. The controller works, but its current storage cannot retain your job search history.
Your API can now create, update, plus delete validated applications. Next up, you will replace temporary storage with a file-based database that keeps those records across restarts.
Persist Data Across Restarts
Your REST API now handles the full application lifecycle. The restart experiment exposed one important gap: the saved records disappear when the server stops.
The previous H2 Database lived only in memory. In this step, you will configure Spring Boot to write the same records to a local file.
In this step, get ready to:
- Configure a file-based H2 database for persistent storage.
- Restart the API to prove that a saved application survives.
- Exclude the local database from Git.
Configure a file-based H2 database
A file database stores its contents on disk under the project folder. The API can reopen that file after every restart.
- Return to the terminal running the API from earlier.
- Press Ctrl+C if the API is still running.
- In IntelliJ IDEA, select src/main/resources/application.properties in the project tree.
- Replace the file contents with this configuration:
spring.application.name=job-application-tracker
spring.datasource.url=jdbc:h2:file:./data/jobtracker;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false
What does this configuration do?
- The file URL stores the database under data/jobtracker relative to the project folder.
- The local username plus empty password let the application connect to its embedded database.
- The update setting lets JPA create the schema while preserving existing rows.
- The final setting keeps persistence work outside the web-view lifecycle.
- Save application.properties.
- Start the API with its new database configuration by running:
.\mvnw.cmd spring-boot:run
What does this command do?
The Maven Wrapper starts the API with the updated configuration. H2 creates the local database files under data during startup.
You should see the application complete its startup without a database connection failure.
Does the API fail to start?
- Check that every property is on its own line in application.properties.
- Confirm that the datasource URL matches the reference exactly.
- Stop any older API process before starting this one.
Ask for help with the startup details using this troubleshooting prompt.
Prove the record survives a restart
Persistence only counts when the same row returns after the process ends. You will create one application before restarting the server.
- Return to the separate Windows PowerShell window from the previous step.
- Repeat the same JSON POST request you used to create an application earlier.
PowerShell should display the stored application with a generated ID.
- Record the generated ID here: your generated application ID.
- List the stored applications by running:
Invoke-RestMethod -Method 'Get' -Uri 'http://localhost:8080/applications'
What does this request check?
This GET request asks the list endpoint for every stored application. PowerShell converts the returned JSON into objects you can read in the terminal.
You should see the application whose generated ID you recorded.
- Return to the terminal running the API.
- Stop the server by pressing Ctrl+C.
- Restart the API by running:
.\mvnw.cmd spring-boot:run
What happens during the restart?
The application reconnects to the H2 file under data. The existing schema plus stored rows remain available.
Wait until the application finishes its startup sequence before sending the next request.
Before you check, consider this: will the generated ID still be present after the restart? Make your prediction now.
- Return to Windows PowerShell.
- Check the restarted API by running:
Invoke-RestMethod -Method 'Get' -Uri 'http://localhost:8080/applications'
What proves persistence worked?
The response comes from a newly started application process. Seeing the earlier record proves that H2 read it from disk.
PowerShell should return the record whose ID is your generated application ID. Its company plus role plus status plus notes should match the values you created.
You have closed the data-loss gap. Your tracker can now survive a server restart without losing its applications.
Is the list empty after restarting?
- Confirm that application.properties contains the file-based datasource URL.
- Start the API from the same project folder each time.
- Confirm that you created the record after saving the new configuration.
Use this debugging prompt to trace the database location.
Protect local data from Git
The data directory can contain personal job-search information. A Git ignore rule keeps those local database files out of future commits.
- Right-click the top-level job-application-tracker folder in IntelliJ IDEA's project tree.
- Choose the option for creating a new file.
- Name the file .gitignore.
- Paste these ignore rules into the file:
target/
.idea/
*.iml
data/
What do these rules protect?
- The first rule excludes Maven build output.
- The next two rules exclude IntelliJ IDEA project files.
- The final rule excludes the local H2 database directory.
- Save .gitignore.
Use the reference below to compare the complete project state for this step.
✔️ Awesome, I've got everything!
Your persistent database configuration plus ignore rules are ready. Confirm that every file is saved before continuing.
ⓧ I'd like to double check the full code
target/
.idea/
*.iml
data/
How does this file fit?
This file keeps build output plus editor metadata plus local tracker data out of version control.
<?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>4.1.2</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>job-application-tracker</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>job-application-tracker</name>
<description>A portfolio-ready API for tracking job applications</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-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>
How does this file fit?
The Maven configuration supplies the web plus persistence plus validation plus test dependencies used by the project.
package com.example.jobtracker;
public enum ApplicationStatus {
WISHLIST,
APPLIED,
INTERVIEWING,
OFFER,
REJECTED
}
How does this file fit?
The enum limits every stored application to the five supported workflow statuses.
package com.example.jobtracker;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class JobApplication {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String company;
private String role;
@Enumerated(EnumType.STRING)
private ApplicationStatus status;
private String notes;
protected JobApplication() {
}
public JobApplication(String company, String role, ApplicationStatus status, String notes) {
this.company = company;
this.role = role;
this.status = status;
this.notes = notes;
}
public Long getId() {
return id;
}
public String getCompany() {
return company;
}
public String getRole() {
return role;
}
public ApplicationStatus getStatus() {
return status;
}
public String getNotes() {
return notes;
}
public void setStatus(ApplicationStatus status) {
this.status = status;
}
}
How does this file fit?
The entity defines the fields that JPA stores as each job application row.
package com.example.jobtracker;
import java.util.List;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/applications")
public class JobApplicationController {
private final JobApplicationRepository repository;
public JobApplicationController(JobApplicationRepository repository) {
this.repository = repository;
}
@GetMapping
public List<JobApplication> getAll() {
return repository.findAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public JobApplication create(@Valid @RequestBody JobApplicationRequest request) {
ApplicationStatus status = request.status() == null
? ApplicationStatus.WISHLIST
: request.status();
JobApplication application = new JobApplication(
request.company(),
request.role(),
status,
request.notes()
);
return repository.save(application);
}
@PatchMapping("/{id}/status")
public JobApplication updateStatus(
@PathVariable Long id,
@RequestParam ApplicationStatus status
) {
JobApplication application = repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Application not found"
));
application.setStatus(status);
return repository.save(application);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
if (!repository.existsById(id)) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Application not found"
);
}
repository.deleteById(id);
}
}
How does this file fit?
The controller translates HTTP requests into create plus list plus update plus delete repository operations.
package com.example.jobtracker;
import org.springframework.data.jpa.repository.JpaRepository;
public interface JobApplicationRepository extends JpaRepository<JobApplication, Long> {
}
How does this file fit?
The Spring Data JPA repository supplies the database operations used by the controller.
package com.example.jobtracker;
import jakarta.validation.constraints.NotBlank;
public record JobApplicationRequest(
@NotBlank(message = "Company is required") String company,
@NotBlank(message = "Role is required") String role,
ApplicationStatus status,
String notes
) {
}
How does this file fit?
The request record uses Jakarta Validation to reject blank company or role values before persistence.
package com.example.jobtracker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JobApplicationTrackerApplication {
public static void main(String[] args) {
SpringApplication.run(JobApplicationTrackerApplication.class, args);
}
}
How does this file fit?
The application class boots the embedded server plus the configured persistence layer.
spring.application.name=job-application-tracker
spring.datasource.url=jdbc:h2:file:./data/jobtracker;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false
How does this file fit?
This configuration points the application at the persistent H2 file database.
- Expand the data directory in IntelliJ IDEA's project tree.
You should see one or more H2 database files inside the directory. Their presence confirms that the tracker has written its records to disk.
- Open .gitignore.
You should see data/ as the final line. That rule keeps your local application records outside the repository.
Your tracker now keeps job applications across restarts without publishing its local database. Next up, you will add an automated test plus project documentation plus a visible GitHub history.
Test, Document, and Publish
Your persistent API now creates job applications that survive a restart. Working code alone is difficult for another developer or interviewer to evaluate.
An automated integration test proves that the create and list flow works. MockMvc performs that check through the same HTTP layer your PowerShell requests use.
A clear README makes your design decisions reproducible. Git records the finished project before GitHub makes it available to review.
In this step, get ready to:
- Add an isolated MockMvc test for the create and list flow.
- Document the API with setup instructions and engineering decisions.
- Commit the finished project and publish it to GitHub.
Add an isolated integration test
The test needs its own in-memory H2 Database configuration. This keeps automated test records away from the persistent job applications in your local file database.
- In IntelliJ IDEA's project tree, expand src/test.
- Create a resources directory inside src/test.
- Create application.properties inside src/test/resources.
- Configure the isolated test database by pasting this code:
spring.datasource.url=jdbc:h2:mem:jobtrackertest
spring.jpa.hibernate.ddl-auto=create-drop
Why use a separate test database?
- The test configuration replaces the main file database with an in-memory database.
- The create-drop setting creates a clean schema for the test run.
- The temporary test data disappears when the test database closes.
- Save src/test/resources/application.properties.
- In IntelliJ IDEA's project tree, expand src/test/java/com/example/jobtracker.
- Create JobApplicationControllerTest.java inside that package.
- Add the test imports and MockMvc field by pasting this first section:
package com.example.jobtracker;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class JobApplicationControllerTest {
@Autowired
private MockMvc mockMvc;
What does this setup provide?
- The test annotations load the Spring Boot application context.
- MockMvc sends requests through the controller without starting a separate web server.
- The static imports provide request builders and response assertions.
- Continue directly below the MockMvc field by pasting the test method:
@Test
void createsAndListsApplication() throws Exception {
mockMvc.perform(post("/applications")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"company": "Contoso",
"role": "Backend Intern",
"status": "APPLIED",
"notes": "Campus referral"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.company").value("Contoso"));
mockMvc.perform(get("/applications"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].role").value("Backend Intern"));
}
}
What does this test prove?
- The first request posts a Contoso Backend Intern application as JSON.
- The assertions require a 201 Created response with a generated ID.
- The second request lists applications and confirms the saved role appears.
- Save JobApplicationControllerTest.java.
Before you run the build, do you expect the test to touch the persistent applications you created earlier?
- Run the complete build from the project-root Command Prompt:
mvnw.cmd clean install
What does this build check?
Maven compiles the application before running the MockMvc test. It then packages the application and installs the built JAR in the local Maven repository.
You should see the test complete before BUILD SUCCESS appears. Your persistent tracker data remains separate from the test data.
Build not passing?
- Check that the test configuration is saved at src/test/resources/application.properties.
- Check that the Java test is saved in the com.example.jobtracker package.
- Compare every JSON field and assertion with the test code above.
Ask for help with the exact build output: Help me diagnose why my MockMvc build is failing.
✔️ Awesome, I've got everything!
Your isolated integration test passes. Double check that both test files are saved before continuing.
ⓧ I'd like to double check the full code
spring.datasource.url=jdbc:h2:mem:jobtrackertest
spring.jpa.hibernate.ddl-auto=create-drop
package com.example.jobtracker;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class JobApplicationControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void createsAndListsApplication() throws Exception {
mockMvc.perform(post("/applications")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"company": "Contoso",
"role": "Backend Intern",
"status": "APPLIED",
"notes": "Campus referral"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.company").value("Contoso"));
mockMvc.perform(get("/applications"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].role").value("Backend Intern"));
}
}
Write the project README
A Markdown README gives reviewers one place to understand the problem and run the API. It also captures the reasoning that makes the project useful in an interview.
- Create README.md in the project root through IntelliJ IDEA's project tree.
- Add the project purpose and technology list by pasting this first section:
# Job Application Tracker API
A Java backend API for recording companies, roles, application statuses, and notes during an internship or job search.
## Why This Project Exists
A spreadsheet can store applications, but it does not demonstrate backend engineering. This project turns the same problem into an explainable REST API with validation, persistence, automated testing, and version control.
## Tech Stack
- Java 21
- Spring Boot 4.1.2
- Spring MVC
- Spring Data JPA
- H2 Database
- Jakarta Validation
- JUnit Jupiter and MockMvc
- Maven Wrapper
Why start with the problem?
The opening connects the tracker to a real job-search need. The technology list then shows the tools used to solve it.
- Save README.md.
- Use IntelliJ IDEA's Markdown preview to confirm that the title and technology bullets render clearly.
- Add the request flow by pasting this architecture section at the end of README.md:
## Architecture
```text
PowerShell or browser
|
v
JobApplicationController
|
v
JobApplicationRepository
|
v
H2 file database
```
The controller translates HTTP requests into Java method calls. The request record defines accepted input. The entity represents stored data. The repository provides database operations. H2 writes the records to a local file.
What does the diagram explain?
The diagram traces a request from the client to the controller. It then shows how the repository reaches the H2 file database.
- Add the local setup instructions directly below the architecture section:
## Run Locally on Windows
Requirements:
- JDK 21
- Git
From Command Prompt in the project root:
```cmd
mvnw.cmd clean install
mvnw.cmd spring-boot:run
```
The API runs at `http://localhost:8080`.
Why include reproducible setup?
These instructions give another developer the exact build and run path. The Maven Wrapper keeps a separate Maven installation out of the prerequisite list.
- Save README.md.
- Refresh the Markdown preview to confirm that the architecture diagram and Windows setup appear as separate sections.
- Add the create and list examples at the end of README.md:
## Try the API from PowerShell
Create an application:
```powershell
$body = @{
company = 'Contoso'
role = 'Backend Intern'
status = 'APPLIED'
notes = 'Campus referral'
} | ConvertTo-Json
Invoke-RestMethod -Method 'Post' -Uri 'http://localhost:8080/applications' -Body $body -ContentType 'application/json'
```
List applications:
```powershell
Invoke-RestMethod -Method 'Get' -Uri 'http://localhost:8080/applications'
```
What do these examples demonstrate?
The create example shows the expected JSON fields. The list example gives reviewers a quick way to retrieve the stored records.
- Add the update and delete examples directly below the list example:
Update application 1:
```powershell
Invoke-RestMethod -Method 'Patch' -Uri 'http://localhost:8080/applications/1/status?status=INTERVIEWING'
```
Delete application 1:
```powershell
Invoke-RestMethod -Method 'Delete' -Uri 'http://localhost:8080/applications/1'
```
Why document every operation?
The four examples demonstrate the API's complete create, read, update, and delete workflow. A reviewer can now reproduce the same behaviour from PowerShell.
- Save README.md.
- Refresh the Markdown preview to confirm that all four PowerShell examples appear in code blocks.
- Add the automated test explanation and design decisions at the end of README.md:
## Run the Automated Test
From Command Prompt:
```cmd
mvnw.cmd clean install
```
Maven compiles the application, runs the MockMvc test, packages the JAR, and installs it in the local Maven repository.
## Design Decisions
- Spring Boot removes manual servlet-container wiring and provides supported starter dependencies.
- `JobApplicationRequest` keeps incoming API data separate from the database entity.
- `EnumType.STRING` stores readable status names instead of fragile numeric positions.
- The main application uses file-based H2 so records survive restarts.
- Tests override the database URL with in-memory H2 so test data never alters personal tracker data.
- The local `data` directory is ignored by Git.
What makes these decisions useful?
The test section gives reviewers one command for checking the project. The design list records the reasoning behind the DTO, enum mapping, persistence mode, and test isolation.
- Add the limitations and interview material directly below the design decisions:
## Current Limitations
- No authentication or separate users.
- H2 is suitable for local learning, not this project's production deployment path.
- No frontend yet.
- Schema changes use `ddl-auto=update`; a later production project should introduce database migrations.
## Interview Talking Points
1. Trace a request from PowerShell through the controller, DTO, repository, and database.
2. Explain why the first in-memory database lost records after restart.
3. Explain why `@Valid` and `@NotBlank` reject incomplete requests.
4. Explain why the test uses a separate in-memory database.
5. Explain what the Maven Wrapper contributes to reproducible setup.
6. Explain why the H2 file is excluded from Git.
## Resume Bullet
Built a Java 21 REST API with Spring Boot, Spring Data JPA, validation, persistent H2 storage, MockMvc integration testing, and documented Git-based setup for tracking job applications.
## Future AI Extension
A follow-on project can add a separate service that summarizes job descriptions or extracts skills with an LLM. That extension should come after the API, persistence, tests, and security boundaries are understood rather than hiding those foundations behind an AI call.
Why end with honest scope?
The limitations show that you can evaluate your own architecture. The talking points and resume bullet turn the implementation into evidence you can explain.
- Save README.md.
- Refresh the Markdown preview to confirm that the limitations and interview sections render below the design decisions.
Your README now explains the project from problem to future extension. That documentation gives a reviewer enough context to build the API and discuss its trade-offs.
✔️ Awesome, I've got everything!
Your README covers the setup, API workflow, test strategy, engineering decisions, and interview talking points.
ⓧ I'd like to double check the full code
# Job Application Tracker API
A Java backend API for recording companies, roles, application statuses, and notes during an internship or job search.
## Why This Project Exists
A spreadsheet can store applications, but it does not demonstrate backend engineering. This project turns the same problem into an explainable REST API with validation, persistence, automated testing, and version control.
## Tech Stack
- Java 21
- Spring Boot 4.1.2
- Spring MVC
- Spring Data JPA
- H2 Database
- Jakarta Validation
- JUnit Jupiter and MockMvc
- Maven Wrapper
## Architecture
```text
PowerShell or browser
|
v
JobApplicationController
|
v
JobApplicationRepository
|
v
H2 file database
```
The controller translates HTTP requests into Java method calls. The request record defines accepted input. The entity represents stored data. The repository provides database operations. H2 writes the records to a local file.
## Run Locally on Windows
Requirements:
- JDK 21
- Git
From Command Prompt in the project root:
```cmd
mvnw.cmd clean install
mvnw.cmd spring-boot:run
```
The API runs at `http://localhost:8080`.
## Try the API from PowerShell
Create an application:
```powershell
$body = @{
company = 'Contoso'
role = 'Backend Intern'
status = 'APPLIED'
notes = 'Campus referral'
} | ConvertTo-Json
Invoke-RestMethod -Method 'Post' -Uri 'http://localhost:8080/applications' -Body $body -ContentType 'application/json'
```
List applications:
```powershell
Invoke-RestMethod -Method 'Get' -Uri 'http://localhost:8080/applications'
```
Update application 1:
```powershell
Invoke-RestMethod -Method 'Patch' -Uri 'http://localhost:8080/applications/1/status?status=INTERVIEWING'
```
Delete application 1:
```powershell
Invoke-RestMethod -Method 'Delete' -Uri 'http://localhost:8080/applications/1'
```
## Run the Automated Test
From Command Prompt:
```cmd
mvnw.cmd clean install
```
Maven compiles the application, runs the MockMvc test, packages the JAR, and installs it in the local Maven repository.
## Design Decisions
- Spring Boot removes manual servlet-container wiring and provides supported starter dependencies.
- `JobApplicationRequest` keeps incoming API data separate from the database entity.
- `EnumType.STRING` stores readable status names instead of fragile numeric positions.
- The main application uses file-based H2 so records survive restarts.
- Tests override the database URL with in-memory H2 so test data never alters personal tracker data.
- The local `data` directory is ignored by Git.
## Current Limitations
- No authentication or separate users.
- H2 is suitable for local learning, not this project's production deployment path.
- No frontend yet.
- Schema changes use `ddl-auto=update`; a later production project should introduce database migrations.
## Interview Talking Points
1. Trace a request from PowerShell through the controller, DTO, repository, and database.
2. Explain why the first in-memory database lost records after restart.
3. Explain why `@Valid` and `@NotBlank` reject incomplete requests.
4. Explain why the test uses a separate in-memory database.
5. Explain what the Maven Wrapper contributes to reproducible setup.
6. Explain why the H2 file is excluded from Git.
## Resume Bullet
Built a Java 21 REST API with Spring Boot, Spring Data JPA, validation, persistent H2 storage, MockMvc integration testing, and documented Git-based setup for tracking job applications.
## Future AI Extension
A follow-on project can add a separate service that summarizes job descriptions or extracts skills with an LLM. That extension should come after the API, persistence, tests, and security boundaries are understood rather than hiding those foundations behind an AI call.
Publish the repository to GitHub
The passing build and README are ready to become a versioned project. Your existing .gitignore keeps generated build files and personal H2 data out of the commit.
- Return to the project-root Command Prompt.
- Initialize the main branch and create the project commit by running these commands:
git init -b main
git add .
git commit -m "Build job application tracker API"
What do these Git commands do?
- The first command creates a local repository whose initial branch is main.
- The second command stages the project files that are not excluded by .gitignore.
- The third command saves that staged snapshot with a descriptive message.
The terminal confirms that the first commit was created. The ignored data directory stays outside the repository.
Commit not created?
If Git reports a missing identity, confirm that the global name and email from your workspace setup are still configured. Run the commit command again after correcting the identity.
Share the exact terminal message here: Help me resolve my Git commit error.
- Open the GitHub new repository page in your browser.
- Enter job-application-tracker as the repository name.
- Set the repository visibility to public.
- Leave the README option unselected.
- Leave the license option unselected.
- Leave the gitignore option unselected.
- Finish creating the empty repository.
- Copy the repository's HTTPS remote URL.
Keeping the remote empty prevents generated files from conflicting with your local commit. The copied URL connects this project to that remote repository.
- Replace REMOTE-URL with the copied HTTPS URL before running these commands:
git remote add origin REMOTE-URL
git remote -v
What does the remote check prove?
The first command names your GitHub repository origin. The second command confirms the address used for fetching and pushing.
GitHub may ask you to authenticate when you push. Follow the browser sign-in flow if it appears.
Before you push, do you expect the GitHub repository to show the same latest commit as your local main branch?
- Push the main branch to GitHub by running:
git push -u origin main
What does the push establish?
The command uploads your local commit to the remote main branch. The upstream connection lets later pushes target the same branch.
- Return to the new repository page in your browser.
- Refresh the repository page.
- Confirm that the latest commit is named Build job application tracker API.
- Confirm that README.md appears in the repository.
- Confirm that the source and test directories appear in the repository.
You should see the README rendered below the file list. Your source files and MockMvc test are now part of the latest GitHub commit.
Push not reaching GitHub?
- Confirm that the remote check lists your new GitHub repository under origin.
- Complete the GitHub authentication flow if your browser requests it.
- Confirm that the empty remote repository was created without starter files.
Use the terminal output to get targeted help: Help me troubleshoot why my Git push failed.
Secret mission
Filter Applications by Status
Your tracker can retrieve every application, but a growing job search needs focused results. In this Secret Mission, you will add a status-specific endpoint that uses a derived database query without handwritten SQL.
Clean Up Your Resources
Clean Up Your Resources
Your Spring Boot REST API uses local H2 Database storage without ongoing cost. Choose to keep everything or pause the API or delete both copies.
Resources you used:
- The local IntelliJ IDEA project with source files plus local Git history.
- The file-based H2 Database records inside data/.
- The public GitHub repository.
Keep everything running
No action needed. Choose this if you are still building features or practicing API requests.
- Keep the project root folder on your computer.
- Keep the data/ directory so your saved applications remain available.
- Keep the public GitHub repository available for sharing.
- Leave the Spring Boot process running only while you are actively testing requests.
Pause - I'll come back to this later
Stopping the server frees port 8080. Your project files plus H2 records remain on disk.
- Switch back to the Command Prompt session from earlier if the API is running.
- Press Ctrl+C to stop the Spring Boot process.
- Keep the project root folder on your computer.
- Keep the public GitHub repository available.
- Restart the API with the Maven Wrapper command from earlier when you are ready to resume.
If no server process is running, your project is already paused.
Delete - I don't want to use this again
Deleting both copies is permanent. This option is for a project you are certain you no longer need.
Finding the exact project folder can be fiddly when several folders look similar. IntelliJ IDEA can reveal the folder that is currently open.
- Stop the API from the existing Command Prompt with Ctrl+C if it is running.
- Close the project-root Command Prompt window.
- Right-click the top-level project folder in IntelliJ IDEA's Project pane.
- Choose the context-menu option that reveals the folder in File Explorer.
- Close IntelliJ IDEA.
- Select the revealed project root folder in File Explorer.
- Use File Explorer's delete action to remove the selected folder.
The local project is now removed. Its Git history plus H2 records are gone with it.
The GitHub copy is stored separately. Removing the local folder does not delete that public repository.
- Open GitHub's official repository deletion guide in your browser.
- Follow the guide's deletion steps for your public repository.
- Return to the previous repository URL after completing the confirmation process.
GitHub now shows that the repository is unavailable. This confirms the remote copy has been removed.
Nice Work!
Nice Work!
You did it! Your public GitHub repository now presents a tested Spring Boot REST API that preserves job application records across restarts.
What you learned:
- Built a validated application workflow with meaningful HTTP responses for creating, listing, updating, and deleting records.
- Observed why database persistence matters when in-memory data disappeared after a restart. Configured a file-based H2 Database so saved applications survive future restarts.
- Added an isolated MockMvc integration test that verifies the create and list flow. Published the source code with setup instructions, architecture notes, API examples, and interview talking points.
- Completed the Secret Mission by adding status filtering through a Spring Data derived query. The API now returns only applications matching the requested status without handwritten SQL.
Ready to quiz yourself?