AI-Assisted Network Health API
Build a Spring Boot API that checks DNS and TCP reachability with AI review.
Introduction
30 Second Summary
Your computer can find a destination online even when the service there is unavailable. A healthy-looking check can still hide the real problem.
In this project, you will build a Spring Boot network health API that checks DNS resolution separately from TCP service reachability. You will use ChatGPT or Claude to define acceptance criteria before reviewing the code against them.
What You'll Build
You'll open a browser URL to see a JSON result showing whether localhost:8080 accepts a connection.
By the end of this project, you'll have:
- A working network health API that shows whether localhost:8080 accepts a TCP connection.
- Visible input guardrails that reject port 0 instead of reporting a false positive.
- A repeatable build check that runs JUnit tests through the Apache Maven Wrapper before GitHub Actions repeats the build on Windows.
- Secret Mission: Add a deterministic test that opens a temporary local TCP listener to prove the API detects an open port without the public internet.
Are there any prerequisites?
This project requires a Windows 10 or later computer with internet access. You need permission to install desktop software plus existing access to ChatGPT or Claude.
Professional familiarity with Java 21, Spring Boot, and Maven keeps the focus on networking.
Before We Start
Start by committing to the API you are building. This gives your AI assistant a clear specification while keeping you responsible for reviewing its networking assumptions.
Set Up Your Windows Java Workspace
Your API will combine application logic with DNS resolution. It will also test TCP ports.
A reproducible workspace gives those checks a stable baseline. This keeps tool problems from looking like networking problems.
In this step, get ready to:
- Install the Java toolchain with editor support.
- Configure source control with your GitHub account.
- Generate a Spring Boot project that responds on your computer.
Install Java and Visual Studio Code
Microsoft Build of OpenJDK provides the Java runtime plus compiler for this project. Visual Studio Code adds editing tools that understand the generated application.
- Press the Windows key to open Windows search.
- Type Command Prompt into the search field.
- Press Enter to open Command Prompt.
- Check the installed Java version by running this command:
java -version
What does this command do?
The java command starts the installed Java runtime. The version option prints the release that Windows currently finds.
Choose the tab that matches the result in your terminal.
✔️ I see the required Java release
Your output identifies Java 21. The Java runtime is ready for this project.
ⓧ I see an older Java release
Windows is finding a Java release below 21. Install the required JDK before continuing.
- Open the Microsoft Build of OpenJDK download page.
- Download microsoft-jdk-21.0.12.1-windows-x64.exe from the Windows x64 EXE option for 21.0.12.1 LTS.
- Run the downloaded installer.
- Keep the default options throughout the installation wizard.
- Press the Windows key to open Windows search.
- Type Command Prompt into the search field.
- Press Enter to open a new Command Prompt.
- Repeat the Java version check shown above.
The new terminal should report Java 21 from Microsoft Build of OpenJDK.
ⓧ Command not found
Windows cannot find a Java installation yet. Install the required JDK from Microsoft.
- Open the Microsoft Build of OpenJDK download page.
- Download microsoft-jdk-21.0.12.1-windows-x64.exe from the Windows x64 EXE option for 21.0.12.1 LTS.
- Run the downloaded installer.
- Keep the default options throughout the installation wizard.
- Press the Windows key to open Windows search.
- Type Command Prompt into the search field.
- Press Enter to open a new Command Prompt.
- Repeat the Java version check shown above.
The new terminal should now report Java 21 from Microsoft Build of OpenJDK.
Java is ready. Your editor now needs Java language support so it can import the project and run the application.
- Open the Visual Studio Code download page.
- Download the Windows installer.
- Run the downloaded installer.
- Keep the default options throughout the installation wizard.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
Why add Java editor support?
The Extension Pack for Java adds language support plus debugging. It also adds testing plus Maven project tools.
- Select the Extensions icon in the left Activity Bar.
- Search for Extension Pack for Java.
- Select Extension Pack for Java from Microsoft.
- Click Install.
You should see the extension pack listed as installed. Visual Studio Code can now import Java projects and start them with the debugger.
Java still unavailable?
Close every open terminal after installing the JDK. Open a new Command Prompt so Windows reloads the updated environment.
If the Java check still fails, restart Windows before trying again.
Help me troubleshoot my Windows Java installation.
Configure Git and GitHub
Git records each project revision on your computer. GitHub provides the remote repository that you will publish later.
- Return to the new Command Prompt from your Java check.
- Check the installed Git version by running this command:
git --version
What does this command do?
The git command starts Git. The version option prints the release currently available to Windows.
Choose the tab that matches the result in your terminal.
✔️ I see the required Git release
Your output corresponds to Git for Windows 2.56.0(2). Git is ready for configuration.
ⓧ I see an older Git release
Windows is finding an older Git release. Install Git for Windows 2.56.0(2) before continuing.
- Open the Git for Windows download page.
- Download the maintained x64 build for release 2.56.0(2).
- Run the downloaded installer.
- Keep the default options throughout the installation wizard.
- Press the Windows key to open Windows search.
- Type Command Prompt into the search field.
- Press Enter to open a new Command Prompt.
- Repeat the Git version check shown above.
The new terminal should identify Git for Windows from the required release.
ⓧ Command not found
Windows cannot find Git yet. Install Git for Windows before continuing.
- Open the Git for Windows download page.
- Download the maintained x64 build for release 2.56.0(2).
- Run the downloaded installer.
- Keep the default options throughout the installation wizard.
- Press the Windows key to open Windows search.
- Type Command Prompt into the search field.
- Press Enter to open a new Command Prompt.
- Repeat the Git version check shown above.
The new terminal should identify Git for Windows from release 2.56.0(2).
Every Git commit records an author name plus email address. Configure the identity you want attached to this project.
- Replace YOUR NAME with your preferred commit author name.
- Replace YOUR EMAIL ADDRESS with the email address associated with your GitHub account.
- Configure your global Git identity by running the edited commands:
git config --global user.name "YOUR NAME"
git config --global user.email "YOUR EMAIL ADDRESS"
What do these commands do?
- The user.name value becomes the author name on new commits.
- The user.email value becomes the author email on new commits.
- The global option applies these values to future repositories on this Windows account.
A successful configuration returns control to the prompt without reporting a failure.
- Display the stored identity values by running these commands:
git config user.name
git config user.email
What do these commands check?
Each command reads one identity value that Git resolves for future commits. The output lets you catch spelling mistakes before creating the repository history.
You should see your chosen name on the first line. You should see your GitHub email address on the second line.
- Switch back to your browser.
- Open the GitHub website.
- Create a GitHub account if you do not already have one.
- Complete any account verification requested by GitHub.
- Sign in to GitHub.
You should reach your signed-in GitHub home page. Your account menu confirms which profile is active.
Git identity looks wrong?
Run the two global identity commands again with the corrected name or email. The new values replace the previous global settings.
Help me correct my Git author identity.
Generate and run the Spring Boot project
Spring Initializr creates a consistent project structure from a small set of choices. Its generated Maven Wrapper lets the project use Maven without a separate installation.
The first application run proves that your JDK plus editor tooling can start Spring Boot before you add networking logic.
- Open Spring Initializr in your browser.
- Select Maven as the project type.
- Select Java as the language.
- Select Spring Boot 4.1.1.
- Enter com.nextwork in the Group field.
- Enter network-health-api in the Artifact field.
- Enter network-health-api in the Name field.
- Enter com.nextwork.networkhealth in the Package name field.
Why do these project coordinates matter?
The group identifies the project namespace. The artifact becomes the generated project name.
The package value places every Java class under one consistent namespace.
- Select Jar for Packaging.
- Select Java 21.
- Click Add Dependencies.
- Search for Spring Web.
- Select Spring Web.
- Click Generate to download the project ZIP.
Why use Spring Initializr?
Spring Initializr creates the application class plus package structure for your selected Spring Boot release. It also includes the Maven Wrapper scripts needed for repeatable builds.
- Press the Windows key to open Windows search.
- Type File Explorer into the search field.
- Press Enter to open File Explorer.
- Select the Downloads folder in the left sidebar.
- Right-click the downloaded network-health-api.zip file.
- Select Extract All.
- Click Extract to create the network-health-api folder.
You should see the extracted network-health-api folder in Downloads.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Click File in the top menu bar.
- Click Open Folder.
- Select the extracted network-health-api folder inside Downloads.
- Click Select Folder.
Visual Studio Code may ask whether you trust the downloaded project folder. Confirm the prompt because you generated this project yourself.
The first Java project import can take a few minutes while the editor prepares Maven dependencies. Wait until the Java status indicators stop showing import activity.
- Expand the network-health-api project in the Explorer sidebar.
- Confirm the file tree contains .gitignore, HELP.md, mvnw, mvnw.cmd, and pom.xml.
- Expand src/main/java/com/nextwork/networkhealth in the Explorer sidebar.
- Select NetworkHealthApiApplication.java.
The generated application class should match the project artifact below.
✔️ Awesome, I've got everything!
Great. Keep NetworkHealthApiApplication.java open for the first run.
ⓧ I'd like to double check the full code
Compare your generated NetworkHealthApiApplication.java with this complete file.
package com.nextwork.networkhealth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class NetworkHealthApiApplication {
public static void main(String[] args) {
SpringApplication.run(NetworkHealthApiApplication.class, args);
}
}
The first run can take a few minutes while Spring dependencies are prepared. Before you start it, what should prove that both Java and Spring Boot are working?
- Press F5 while NetworkHealthApiApplication.java is active.
- Wait for the integrated terminal to report that the application is running.
You should see Spring Boot startup logs in the Visual Studio Code terminal. The application remains active in that terminal.
- Switch back to the browser tab you used for Spring Initializr.
- Enter http://localhost:8080 in the address bar.
- Press Enter to request the local application.
Your browser should receive a response from localhost:8080. The page may report that no custom route exists because the generated project does not have a controller yet.
That is the first workspace win. Your JDK, editor support, generated Spring Boot project, and local web server now work together.
Browser cannot reach the app?
Return to Visual Studio Code to confirm the application is still running in the terminal. Start it again with F5 if the process stopped.
If F5 does not start the class, confirm the Extension Pack for Java is installed. Wait for the Java project import to finish before retrying.
Help me start my generated Spring Boot app in Visual Studio Code.
Your Windows Java workspace now runs the generated Spring Boot application. Next, you'll turn this scaffold into its first network health response.
Ship a DNS-Only Health Check
Your Windows Java workspace is ready. Your Spring Boot application already responds at http://localhost:8080.
This first endpoint uses DNS resolution to produce a JSON result. It deliberately treats a resolved hostname as proof of reachability, which creates the false positive you will inspect.
In this step, get ready to:
- Define acceptance criteria with an AI assistant.
- Pin the Maven build configuration.
- Expose a DNS-only endpoint that demonstrates a false positive.
Define the acceptance criteria
Acceptance criteria give you a review checklist before generated code can shape the solution. They keep DNS resolution separate from TCP service reachability.
Use ChatGPT or Claude to turn the specification into testable statements. Keep the resulting checklist visible beside Visual Studio Code while you review the endpoint.
- Return to your ChatGPT or Claude conversation.
- Request the acceptance criteria by sending this prompt:
For Java 21 and Spring Boot 4.1.1, list acceptance criteria for a GET `/api/check` endpoint with required `host` and `port` query parameters. Separate DNS resolution from TCP reachability, require a finite timeout, and do not use an external API.
What should the checklist cover?
- The endpoint requires both input parameters.
- Hostname resolution has its own success or failure result.
- Service reachability requires a separate TCP connection attempt.
- The connection attempt uses a finite timeout without calling an external API.
- Send the prompt to your AI assistant.
- Confirm the response requires the host parameter.
- Confirm the response requires the port parameter.
- Confirm the response separates DNS resolution from TCP reachability.
- Confirm the response requires a finite timeout.
- Confirm the response avoids external APIs.
- Keep the conversation open beside VS Code as your review checklist.
Pin the build and add the controller
The temporary controller turns a hostname into an IP address through Java's networking library. Its response records the requested port without connecting to that port.
- Expand src/main/java/com/nextwork/networkhealth in the Explorer sidebar.
- Right-click the com.nextwork.networkhealth package folder.
- Select New File.
- Enter NetworkHealthController.java as the file name.
- Press Enter.
- Add the temporary controller by pasting this code:
package com.nextwork.networkhealth;
import java.net.InetAddress;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NetworkHealthController {
@GetMapping("/api/check")
public Map<String, Object> check(
@RequestParam("host") String host,
@RequestParam("port") int port) throws Exception {
InetAddress address = InetAddress.getByName(host);
return Map.of(
"host", host,
"address", address.getHostAddress(),
"port", port,
"reachable", true,
"message", "DNS lookup succeeded");
}
}
What does this controller do?
- The @GetMapping("/api/check") annotation maps browser requests to the check() method.
- The two @RequestParam annotations require a hostname plus a port.
- InetAddress.getByName(host) resolves the hostname to an IP address.
- Map.of() builds the JSON response fields.
- The reachable value becomes true whenever the DNS lookup completes.
- Save NetworkHealthController.java.
The Maven project file fixes the framework version plus every direct dependency used across this project. This prevents changing defaults from altering the build.
- Select pom.xml in the Explorer sidebar.
- Use the comparison tabs below to check the complete project state.
- Open the second tab if any file differs from the reference.
- Update every difference so your files match the reference exactly.
✔️ Awesome, I've got everything!
Your pinned build file plus both Java files match the target project state.
ⓧ I'd like to double check the full code
Compare your complete pom.xml with this pinned reference:
<?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.1</version>
<relativePath/>
</parent>
<groupId>com.nextwork</groupId>
<artifactId>network-health-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>network-health-api</name>
<description>AI-assisted TCP network health checker</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-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</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>
What does this build file control?
- The parent pins Spring Boot to 4.1.1.
- The properties section sets Java to 21.
- The dependencies provide web routing plus JSON responses.
- The dependencies also provide application health support plus the test stack.
- The build plugin packages the application as a Spring Boot project.
Your existing NetworkHealthApiApplication.java remains the application entry point:
package com.nextwork.networkhealth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class NetworkHealthApiApplication {
public static void main(String[] args) {
SpringApplication.run(NetworkHealthApiApplication.class, args);
}
}
This class starts the application through SpringApplication.run().
Your complete temporary NetworkHealthController.java should look like this:
package com.nextwork.networkhealth;
import java.net.InetAddress;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NetworkHealthController {
@GetMapping("/api/check")
public Map<String, Object> check(
@RequestParam("host") String host,
@RequestParam("port") int port) throws Exception {
InetAddress address = InetAddress.getByName(host);
return Map.of(
"host", host,
"address", address.getHostAddress(),
"port", port,
"reachable", true,
"message", "DNS lookup succeeded");
}
}
This temporary controller resolves the requested hostname before returning the response map.
- Save pom.xml.
Expose the DNS false positive
Port 0 gives the endpoint a clear chance to prove whether it checks the requested service. You will compare the response with your acceptance criteria after the request completes.
- Stop the current application using the square stop control in VS Code's debug toolbar.
- Select NetworkHealthApiApplication.java in the Explorer sidebar.
- Press F5 to run the updated application.
The VS Code terminal shows the application running once startup finishes.
Before you make the request, do you think port 0 will change the endpoint's reachability result?
- Open http://localhost:8080/api/check?host=localhost&port=0 in your browser.
You will see a JSON response containing "port":0 plus "reachable":true. The message reports DNS lookup succeeded.
That is the intended false positive. The hostname resolved successfully even though the endpoint never tested whether port 0 accepts a connection.
Endpoint not returning the response?
Confirm NetworkHealthController.java sits inside src/main/java/com/nextwork/networkhealth. Its package declaration must match that folder.
If the application does not start, compare pom.xml plus the controller with the complete references above.
Help me debug my DNS-only endpoint.
- Return to the AI conversation containing your acceptance criteria.
- Ask the AI assistant to critique the visible response by sending:
What did this endpoint actually prove, and what did it fail to prove?
What should the review distinguish?
The successful response proves that Java resolved localhost to an IP address. It does not prove that a service accepts TCP connections on the requested port.
The browser result now gives you evidence for rejecting the endpoint's reachability claim. Your acceptance criteria define the gap that the next step must close.
- Compare the AI response with the false positive in your browser.
- Keep only conclusions that preserve the distinction between DNS resolution and TCP reachability.
You caught the networking assumption in action. Next, you will replace that guess with input guardrails plus a timed TCP connection.
Replace the Guess with a TCP Probe
Your Spring Boot API has exposed a false positive. The DNS lookup succeeded even though port 0 could not host the requested service.
A TCP connection tests whether a service accepts traffic on the requested port. A finite timeout keeps that test from waiting forever.
In this step, get ready to:
- Create a timed TCP checker with input guardrails.
- Delegate the API endpoint to the new checker.
- Review the code with AI before verifying target reachability and application health.
Create the timed TCP checker
The new NetworkChecker service becomes the single place that decides whether a target is reachable. It separates rejected input from DNS failure and TCP failure.
The file is split into four connected sections for review. Add every section in order before saving the completed class.
- Return to the file tree in Visual Studio Code.
- Create NetworkChecker.java inside src/main/java/com/nextwork/networkhealth using the new-file control.
- Paste this package declaration and import section into NetworkChecker.java:
package com.nextwork.networkhealth;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class NetworkChecker {
What does this section provide?
- The networking imports provide hostname resolution and socket connection types.
- The exception imports let the checker distinguish DNS failures from other connection failures.
- The @Service annotation registers NetworkChecker with Spring.
- Place the cursor after the opening brace of NetworkChecker.
- Paste the timeout and connection section below the opening brace:
private static final int CONNECT_TIMEOUT_MS = 1_000;
public Map<String, Object> check(String host, int port) {
String safeHost = host == null ? "" : host;
if (safeHost.isBlank()) {
return result(safeHost, "", port, false, 0, "Host is required");
}
if (port < 1 || port > 65_535) {
return result(safeHost, "", port, false, 0,
"Port must be between 1 and 65535");
}
long startedAt = System.nanoTime();
String addressText = "";
try {
InetAddress address = InetAddress.getByName(safeHost);
addressText = address.getHostAddress();
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(address, port), CONNECT_TIMEOUT_MS);
}
return result(safeHost, addressText, port, true,
elapsedMillis(startedAt), "TCP connection succeeded");
}
How does the connection work?
- The host guardrail rejects blank input before a network operation begins.
- The port guardrail accepts values from 1 through 65535.
- The System.nanoTime() reading starts the latency measurement.
- The InetAddress.getByName() call resolves the hostname.
- The Socket attempts a connection using the finite CONNECT_TIMEOUT_MS value.
- Try-with-resources closes the socket after the connection attempt.
- Place the cursor after the closing brace of the successful connection path.
- Paste the failure handling and latency helper below it:
catch (UnknownHostException exception) {
return result(safeHost, addressText, port, false,
elapsedMillis(startedAt), "DNS lookup failed");
}
catch (IOException exception) {
return result(safeHost, addressText, port, false,
elapsedMillis(startedAt),
"TCP connection failed: " + exception.getClass().getSimpleName());
}
}
private long elapsedMillis(long startedAt) {
return (System.nanoTime() - startedAt) / 1_000_000;
}
How are failures separated?
- The UnknownHostException path reports a DNS lookup failure.
- The IOException path reports a failed TCP connection.
- The elapsedMillis() helper converts the elapsed time into milliseconds.
- Place the cursor after the elapsedMillis() method.
- Paste the response helper and final class brace below it:
private Map<String, Object> result(
String host,
String address,
int port,
boolean reachable,
long latencyMs,
String message) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("host", host);
result.put("address", address);
result.put("port", port);
result.put("reachable", reachable);
result.put("latencyMs", latencyMs);
result.put("message", message);
return result;
}
}
How is each response assembled?
- The result() helper gives every outcome the same response fields.
- The LinkedHashMap keeps those fields in insertion order.
- The latencyMs field records the duration of each completed network attempt.
- Save NetworkChecker.java.
- Stop the running application using the square control in the debug toolbar.
- Return to NetworkHealthApiApplication.java.
- Press F5 to compile and run the application.
You should see the application running in the terminal without compilation errors. Good progress. Your timed checker is now registered as a Spring service.
Does the checker fail to compile?
Confirm that NetworkChecker.java sits inside src/main/java/com/nextwork/networkhealth. Check that all four sections appear in the same order.
Make sure each catch block follows the matching try block.
Help me debug my NetworkChecker class.
✔️ Awesome, I've got everything!
Your completed NetworkChecker.java compiles when you run the application with F5.
ⓧ I'd like to double check the full code
Compare your complete NetworkChecker.java with this file:
package com.nextwork.networkhealth;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class NetworkChecker {
private static final int CONNECT_TIMEOUT_MS = 1_000;
public Map<String, Object> check(String host, int port) {
String safeHost = host == null ? "" : host;
if (safeHost.isBlank()) {
return result(safeHost, "", port, false, 0, "Host is required");
}
if (port < 1 || port > 65_535) {
return result(safeHost, "", port, false, 0,
"Port must be between 1 and 65535");
}
long startedAt = System.nanoTime();
String addressText = "";
try {
InetAddress address = InetAddress.getByName(safeHost);
addressText = address.getHostAddress();
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(address, port), CONNECT_TIMEOUT_MS);
}
return result(safeHost, addressText, port, true,
elapsedMillis(startedAt), "TCP connection succeeded");
}
catch (UnknownHostException exception) {
return result(safeHost, addressText, port, false,
elapsedMillis(startedAt), "DNS lookup failed");
}
catch (IOException exception) {
return result(safeHost, addressText, port, false,
elapsedMillis(startedAt),
"TCP connection failed: " + exception.getClass().getSimpleName());
}
}
private long elapsedMillis(long startedAt) {
return (System.nanoTime() - startedAt) / 1_000_000;
}
private Map<String, Object> result(
String host,
String address,
int port,
boolean reachable,
long latencyMs,
String message) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("host", host);
result.put("address", address);
result.put("port", port);
result.put("reachable", reachable);
result.put("latencyMs", latencyMs);
result.put("message", message);
return result;
}
}
Delegate requests to the checker
The temporary controller declares success immediately after DNS resolution. The final controller delegates that decision to the testable NetworkChecker service.
- Return to NetworkHealthController.java in the file tree.
- Select the entire existing file.
- Replace the selected code with this final controller:
package com.nextwork.networkhealth;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NetworkHealthController {
private final NetworkChecker networkChecker;
public NetworkHealthController(NetworkChecker networkChecker) {
this.networkChecker = networkChecker;
}
@GetMapping("/api/check")
public Map<String, Object> check(
@RequestParam("host") String host,
@RequestParam("port") int port) {
return networkChecker.check(host, port);
}
}
What does this controller do?
- The constructor receives the registered NetworkChecker service.
- The endpoint continues to require the host query parameter.
- The endpoint continues to require the port query parameter.
- The check() method returns the service result without duplicating network logic.
- Save NetworkHealthController.java.
- Stop the running application using the square control in the debug toolbar.
- Return to NetworkHealthApiApplication.java.
- Press F5 to run the application with the final controller.
You should see the application running without compilation errors. Requests to /api/check now pass through NetworkChecker.check().
Does the application fail after the controller change?
Confirm that both Java files use the com.nextwork.networkhealth package. Check that both files spell NetworkChecker identically.
Make sure the old InetAddress import and DNS-only response code are gone from the controller.
Help me debug the controller delegation.
✔️ Awesome, I've got everything!
Your controller now delegates each network health request to NetworkChecker.
ⓧ I'd like to double check the full code
Compare your complete NetworkHealthController.java with this file:
package com.nextwork.networkhealth;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NetworkHealthController {
private final NetworkChecker networkChecker;
public NetworkHealthController(NetworkChecker networkChecker) {
this.networkChecker = networkChecker;
}
@GetMapping("/api/check")
public Map<String, Object> check(
@RequestParam("host") String host,
@RequestParam("port") int port) {
return networkChecker.check(host, port);
}
}
Review and verify the probe
Your earlier ChatGPT or Claude conversation can now review working code against fixed constraints. The acceptance checklist keeps that review focused on evidence.
What must the review preserve?
- Every connection attempt uses the finite CONNECT_TIMEOUT_MS value.
- Try-with-resources closes every created socket.
- A DNS failure has its own response path.
- Port 0 is rejected before networking begins.
- The implementation has no external service dependency.
- Return to the AI conversation from the previous step.
- Share the completed NetworkChecker.java with the assistant.
- Share the completed NetworkHealthController.java with the assistant.
- Ask the assistant to review both files against the five constraints above.
- Compare each review comment with your saved acceptance checklist.
- Reject any suggestion that weakens a constraint.
- Keep the implementation unchanged when it already satisfies the checklist.
Before you retry port 0, do you expect successful name resolution to make the target reachable again?
- Return to the browser from the earlier false-positive check.
- Enter http://localhost:8080/api/check?host=localhost&port=0 in the address bar.
- Press Enter.
You should see a JSON response containing "reachable":false. The message should say that the port must be between 1 and 65535.
That false positive is fixed. The API now rejects an invalid service port before DNS can produce a misleading result.
Before you check port 8080, do you expect the application to accept a TCP connection to its own listening port?
- Enter http://localhost:8080/api/check?host=localhost&port=8080 in the browser address bar.
- Press Enter.
You should see "reachable":true with the message "TCP connection succeeded". The response also includes the resolved address and measured latency.
Before the final check, which endpoint do you expect to describe the Spring Boot application itself?
- Enter http://localhost:8080/actuator/health in the browser address bar.
- Press Enter.
You should see the Actuator health response report that the application is healthy.
What did these checks prove?
The /api/check endpoint reports whether the requested target service accepts a TCP connection. The /actuator/health endpoint reports the application's own health.
These results keep target reachability separate from application health.
Do the browser results look different?
Confirm that the terminal still shows the application running before checking port 8080. Restart it with F5 if the process stopped.
Check that each API URL includes both the host parameter and the port parameter.
Help me compare my API responses with the expected results.
Your API now distinguishes a resolved hostname from a reachable TCP service. Next, you will turn these corrected networking rules into repeatable guardrail tests.
Lock the Behavior with Tests
Your TCP probe now handles invalid input correctly. Manual browser checks can be forgotten after the next code change.
JUnit tests preserve those corrected assumptions as executable rules. The Apache Maven Wrapper turns those tests into repeatable build evidence.
In this step, get ready to:
- Replace the generated application test with focused guardrail tests.
- Review each assertion with your AI assistant.
- Produce a tested packaged JAR with the Maven Wrapper.
Create the port guardrail test
A guardrail test gives the checker an invalid value. It then confirms that the application rejects the value before starting any network work.
Deleting the generated test can feel risky. The new tests replace its broad startup check with direct evidence for the network rules.
- In the VS Code Explorer sidebar, expand src/test/java/com/nextwork/networkhealth.
- Select NetworkHealthApiApplicationTests.java.
- Press the Delete key.
- Confirm the deletion if VS Code asks.
- Right-click the com.nextwork.networkhealth package folder.
- Select New File.
- Enter NetworkCheckerTest.java as the file name.
- Create the port guardrail by pasting this code into NetworkCheckerTest.java:
package com.nextwork.networkhealth;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.util.Map;
import org.junit.jupiter.api.Test;
class NetworkCheckerTest {
private final NetworkChecker checker = new NetworkChecker();
@Test
void rejectsPortZero() {
Map<String, Object> result = checker.check("localhost", 0);
assertFalse((Boolean) result.get("reachable"));
assertEquals("Port must be between 1 and 65535", result.get("message"));
}
}
What Does This Test Prove?
- The checker field creates the service directly for a focused test.
- rejectsPortZero() passes port 0 into the same method used by the controller.
- assertFalse confirms that the result marks the target as unreachable.
- assertEquals confirms that the result explains the rejected port.
- Save NetworkCheckerTest.java by pressing Ctrl+S.
- Check the test package in the Explorer sidebar.
You'll see NetworkCheckerTest.java in the test package. The generated test file is no longer listed.
Test File Showing Errors?
Confirm that NetworkCheckerTest.java sits inside src/test/java/com/nextwork/networkhealth. Its package must match the NetworkChecker package.
Check that the generated test file was deleted. Leaving it behind adds an unrelated test to the build.
Help me fix errors in my NetworkCheckerTest file.
Add the blank-host guardrail
A blank host should fail through an application rule. This keeps the test independent from the computer's current network state.
- In NetworkCheckerTest.java, place your cursor immediately before the final }.
- Complete the second guardrail by pasting this method at that position:
@Test
void rejectsBlankHost() {
Map<String, Object> result = checker.check(" ", 8080);
assertFalse((Boolean) result.get("reachable"));
assertEquals("Host is required", result.get("message"));
}
How Does This Stay Deterministic?
- rejectsBlankHost() passes a single blank character into check().
- The blank-host guardrail returns before hostname resolution starts.
- assertFalse checks the returned reachability value.
- assertEquals checks the application-owned status message.
- Save NetworkCheckerTest.java.
- Check that both test methods remain inside the NetworkCheckerTest class.
Good work. Your test file now protects both input guardrails without contacting a public host.
Second Test Outside the Class?
Make sure rejectsBlankHost() appears before the final class brace. A method placed after that brace sits outside NetworkCheckerTest.
Help me place the blank-host test correctly.
✔️ Awesome, I've got everything!
Your NetworkCheckerTest.java file now contains both guardrail tests. Make sure the file is saved before continuing.
ⓧ I'd like to double check the full code
Compare your complete NetworkCheckerTest.java file with this reference:
package com.nextwork.networkhealth;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.util.Map;
import org.junit.jupiter.api.Test;
class NetworkCheckerTest {
private final NetworkChecker checker = new NetworkChecker();
@Test
void rejectsPortZero() {
Map<String, Object> result = checker.check("localhost", 0);
assertFalse((Boolean) result.get("reachable"));
assertEquals("Port must be between 1 and 65535", result.get("message"));
}
@Test
void rejectsBlankHost() {
Map<String, Object> result = checker.check(" ", 8080);
assertFalse((Boolean) result.get("reachable"));
assertEquals("Host is required", result.get("message"));
}
}
The assertions should protect rules owned by your application. An AI review helps you check that distinction before trusting the tests.
- Return to the ChatGPT or Claude conversation from earlier.
- Ask the assistant to explain why each assertion tests an application rule instead of the operating system's current network state.
- Compare the explanation with the two early guard clauses in NetworkChecker.java.
- Reject any proposed test that depends on a public hostname.
What Should the Review Identify?
Both invalid inputs return before hostname resolution or socket creation. The expected values come from your application code.
A public hostname would make the result depend on connectivity outside the test. These guardrails stay repeatable on every machine.
Build and inspect the project
The final check runs both tests through the same build path used to package the application. A successful build proves that the guardrails compile with the rest of the project.
- Switch back to the VS Code terminal running the Spring Boot application.
- Press Ctrl+C to stop the application.
You'll see the command prompt return in the network-health-api project folder.
- Start a Windows Command Prompt in the same terminal by running this command:
cmd
What Does This Command Do?
cmd starts a new instance of the Windows command interpreter. You can now run the Windows Maven Wrapper script from the current project folder.
A longer first build is normal because the wrapper may download its configured Maven version.
Before you run the build, think about whether both guardrails can pass without consulting the network.
- Build the project from the network-health-api folder by running:
mvnw.cmd clean install
What Does This Build Prove?
- The Maven Wrapper runs the project build with its configured Maven version.
- The build executes both methods in NetworkCheckerTest.
- A successful build packages the application as a JAR under target.
You'll see BUILD SUCCESS after Maven reports two passing tests.
That's the behavior locked down. Invalid ports and blank hosts now have repeatable evidence behind them.
- In the VS Code Explorer sidebar, expand the new target folder.
You'll see the packaged JAR inside target. This confirms that the application passed its tests before packaging.
Build Not Succeeding?
If a test fails, compare the expected messages in NetworkCheckerTest.java with the guardrail messages in NetworkChecker.java.
If the wrapper download cannot finish, confirm that your internet connection is available. Run the same build command again after the connection recovers.
Help me diagnose my Maven test or build failure.
Your corrected networking assumptions now survive a repeatable local build. Next, you'll publish the project so a clean Windows runner can prove the same behavior after every push.
Publish a Windows CI Pipeline
Your guardrail tests now pass in a local Maven build. The next goal is to make every pushed revision prove itself on a clean Windows machine.
A GitHub Actions workflow runs continuous integration away from your configured workspace. That clean runner exposes dependencies or assumptions hidden by a one-time local success.
In this step, get ready to:
- Define a workflow that verifies the project on a Windows runner.
- Publish the local project to a public repository.
- Compare the hosted build with your successful local build.
Create the Windows workflow
A YAML workflow tells GitHub which events should start a build. It also defines the environment and command used for verification.
- Switch back to Visual Studio Code with the network-health-api project from earlier.
- Click the new-file control in the file tree.
You should see a field where you can enter the new file path.
- Enter .github/workflows/maven.yml to create the workflow inside the .github/workflows folders.
You should now see maven.yml nested under .github/workflows in the file tree.
- Define the Windows verification workflow by pasting this configuration into maven.yml:
name: Java CI with Maven
on:
push:
pull_request:
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Verify with Maven
run: mvn --batch-mode --update-snapshots verify
What does this workflow do?
- The name value labels the workflow as Java CI with Maven.
- The push trigger starts the workflow when committed changes reach GitHub.
- The pull_request trigger checks proposed changes before they are merged.
- The windows-latest runner gives the build a clean Windows environment.
- The actions/checkout@v6 action places the repository files on that runner.
- The actions/setup-java@v4 action configures Temurin Java 21.
- The Maven cache reuses downloaded build dependencies across workflow runs.
- The verification command compiles the project and runs its tests.
- Save .github/workflows/maven.yml.
- Return to your ChatGPT or Claude conversation from earlier.
- Ask the assistant to explain the trigger, Windows runner, pinned actions, Java setup, Maven cache, and verification phase one line at a time.
The explanation should connect every workflow line to a specific build responsibility. Preserve the exact runner, action pins, Java version, distribution, cache, and command shown above.
Workflow file look different?
YAML uses indentation to group settings. Compare the spaces in your file with the full reference below if a line appears under the wrong section.
Keep the workflow values unchanged if the AI assistant suggests a different runner or action version.
Help me compare my Maven workflow with the required configuration.
✔️ Awesome, I've got everything!
Great. Confirm that .github/workflows/maven.yml is saved before publishing the project.
ⓧ I'd like to double check the full code
Compare your complete .github/workflows/maven.yml file with this reference:
name: Java CI with Maven
on:
push:
pull_request:
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Verify with Maven
run: mvn --batch-mode --update-snapshots verify
Publish the project to GitHub
A Git remote connects your local commit history to a shared repository. GitHub then reads the workflow from each pushed revision.
Publishing this repository makes every committed file visible to anyone. This project contains no credentials or API keys, so the files built here are safe to publish.
- Switch back to the GitHub account from earlier.
- Select the menu in the upper-right corner of the page.
- Click New repository.
You should see the form for creating a repository.
- Choose your account from the Owner menu.
- Enter network-health-api in the Repository name field.
The form should now show your account as the owner of network-health-api.
- Choose Public as the repository visibility.
- Keep all repository initialization options unselected.
The repository is now configured as public with no initial README, license, or additional .gitignore file.
- Click Create repository.
Your empty public repository is ready to receive the local project. The next command sequence preserves the files and commit history you already created.
- Capture the repository URL shown on the setup page here: <REMOTE_URL>.
- Return to the Command Prompt at the network-health-api project folder from earlier.
- Replace the remote placeholder in the fifth command with <REMOTE_URL>.
- Publish the local project by running the completed command sequence:
git init -b main
git add .
git commit -m "First commit"
git remote add origin <REMOTE_URL>
git remote -v
git push -u origin main
What does this command sequence do?
- The first command initializes the local repository with main as its branch.
- The second command stages the project files for a commit.
- The third command records the staged files as First commit.
- The fourth command connects the local repository to origin.
- The fifth command prints the configured remote for verification.
- The final command pushes main to GitHub and links the local branch to its remote counterpart.
You should see two origin entries pointing to your repository URL. The final command should upload the project to GitHub.
Project not pushing?
Complete the GitHub authentication flow if Git asks you to verify your account. Return to Command Prompt after the browser confirms access.
Confirm that the saved remote URL belongs to the empty network-health-api repository.
Help me diagnose why my Git push is failing.
Your API now has a public home with its workflow committed beside the code. That push also gives GitHub Actions its first revision to verify.
Verify the clean Windows build
The hosted run repeats your build without relying on the Java installation or cached files on your computer. Matching results prove that the repository carries everything needed to compile and test the API.
Before you check, predict whether the clean Windows runner will match your local result.
- Open the published network-health-api repository on GitHub.
- Select the Actions tab.
- Select Java CI with Maven.
- Open the workflow run triggered by your push.
- Select the build job running on windows-latest.
- Expand Verify with Maven in the job log.
You should see a green check after mvn --batch-mode --update-snapshots verify completes. Its log should report two passing tests and BUILD SUCCESS.
- Compare the hosted test count with the two tests from your local mvnw.cmd clean install result.
- Compare the hosted build status with your local BUILD SUCCESS result.
That green check is the proof you were aiming for. Your API now rebuilds and retests successfully on a clean Windows runner after every push or pull request.
Windows job not passing?
Expand the first failed workflow step and use its log as the source of truth. Compare that step with the matching part of your local build.
Check that .github/workflows/maven.yml still matches the full reference above. Pay close attention to indentation and pinned values.
Help me compare my failed GitHub Actions log with my successful local Maven build.
Secret mission
Prove an Open Port Without the Internet
Your current tests reject invalid input. Extend them with a temporary local TCP listener that proves NetworkChecker detects a real open port without contacting the public internet.
Clean Up Your Resources
Clean Up Your Resources
Your workspace can stay available, sit idle, or be deleted entirely. This project adds no ongoing cost because its files are local and standard hosted runners are free for public repositories on GitHub.
Resources you used:
- A local network-health-api project folder containing your final Spring Boot API, tests, Maven Wrapper files, and packaged JAR under target.
- A public network-health-api repository containing your source code and passing GitHub Actions workflow history.
Keep everything running
No cleanup is required. Choose this option while you are still testing the API or extending its networking behavior.
- Keep the network-health-api folder on your computer.
- Leave the public repository available for future changes.
- Press Ctrl+C in the Visual Studio Code terminal when you finish testing the running API.
Pause - I'll come back to this later
Pause active work to free local memory while preserving every file and workflow run for later.
- Switch back to the Visual Studio Code terminal from earlier if the application is still running.
- Press Ctrl+C to stop the application.
- Avoid pushing new changes until you are ready to resume work.
- Return to NetworkHealthApiApplication.java when you want to continue.
- Press F5 to start the API again.
Your public repository remains available. Its workflow has nothing to run until new repository activity occurs.
Delete - I don't want to use this again
Deleting both copies is permanent. Choose this option only when you no longer need the code or its workflow history.
- Switch back to the Visual Studio Code terminal from earlier.
- Press Ctrl+C if the API is still running.
- Return to the Command Prompt you used for the local Maven build.
- Move to the parent folder and remove the local project by running these commands:
cd ..
rmdir /s /q network-health-api
What do these commands do?
The first line moves Command Prompt out of the project folder. The second line removes the project folder plus every file inside it.
- Switch back to Visual Studio Code.
- Confirm the network-health-api files are no longer available in the open workspace.
Folder Deletion Blocked?
- Close Visual Studio Code if Windows reports that the folder is in use.
- Return to the Command Prompt at the parent folder.
- Run the deletion commands again.
Help me unblock the folder deletion.
- Return to the network-health-api repository page on GitHub.
- Select the Settings tab.
- Scroll to Danger Zone.
- Choose Delete this repository.
- Acknowledge the deletion warnings.
- Enter network-health-api when GitHub requests the repository name.
- Confirm the repository deletion.
Cleanup complete. Your local project and public repository are now removed.
Nice Work!
Nice Work!
Excellent work. Your Spring Boot network health API now reports DNS resolution separately from TCP service reachability.
You've learned how to:
- Build a network health API with input guardrails that reject invalid hosts or ports. Measure timed connection attempts with latency values plus clear failure messages.
- Use ChatGPT or Claude to create acceptance criteria. Apply that checklist through an AI review loop that tests generated networking assumptions against visible results.
- Run three JUnit guardrail tests in a successful local Maven build. Repeat the same proof on a clean Windows runner through GitHub Actions.
- Secret Mission: Build a deterministic open-port test with a temporary ServerSocket. Close the listener automatically without using the public internet.
Ready to quiz yourself?