Build a Product Inventory REST API
Build a CRUD REST API in vanilla Java with JDBC and a local MySQL database.
Introduction
30 Second Summary
Every online store you use keeps track of its products somewhere. When you browse items, add something to your cart, or check if a product is in stock, there's an API behind the scenes managing all that data.
In this project, you will build a REST API from scratch using vanilla Java and MySQL that handles creating, reading, updating, and deleting product inventory data.
What You'll Build
A Product Inventory REST API that runs on Java 11's built-in HTTP server, connects to a local MySQL database, and responds to HTTP requests with JSON data.
By the end of this project, you'll have:
- A working REST API running on localhost that handles full CRUD operations for product inventory.
- A clean, layered Java project structure (Handler, DAO, Model, Utility) that mirrors enterprise architecture patterns.
- Hands-on experience using JDBC with prepared statements to prevent SQL injection.
- Secret Mission: Add a search endpoint with SQL LIKE queries and pagination.
Are there any prerequisites?
This project assumes basic Java knowledge (classes, interfaces, exceptions) and basic SQL knowledge (CREATE TABLE, INSERT, SELECT, UPDATE, DELETE). You do not need experience with any Java frameworks.
Set Up Your Project and Tools
Before writing any code, you need three things ready: a verified JDK, the MySQL database driver that connects Java to your database, and an API testing tool.
In this step, you'll create your project folder structure, download MySQL Connector/J (the JDBC driver), and install Postman for testing your API endpoints later.
In this step, get ready to:
- Verify your JDK and create the project folder structure.
- Download MySQL Connector/J 9.7.0 and add it to your project.
- Install Postman for API testing.
Verify your JDK and create the project folder
Your Java Development Kit includes the java runtime and the javac compiler. Both need to be working before you can build anything.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Verify your Java runtime by running this command:
java -version
- Verify your Java compiler by running this command:
javac -version
You should see version 11 or higher in both outputs (e.g. javac 11.0.31).
Command not found?
If you see an error like "command not found" or "not recognized", your JDK is not installed or not on your system PATH. Download JDK 11 from the official Adoptium site at adoptium.net and run the installer.
help me install JDK 11 on my computer
Now you'll create the project folder on your Desktop so it's easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and its subdirectories by running these commands:
mkdir product-inventory-api
cd product-inventory-api
mkdir src lib
What does this folder structure do?
The src/ folder holds all your Java source code files. The lib/ folder holds external libraries (JARs) your project depends on.
- Confirm the folders exist by running:
ls
You should see lib and src in the output.
Don't see both folders?
Make sure you ran all three commands in order. If you only see one folder, run the missing mkdir command again (e.g. mkdir src).
help me create the project folder structure for my Java API project
Download MySQL Connector/J 9.7.0
MySQL Connector/J is the official JDBC driver that lets your Java code talk to a MySQL database. You'll download it as a JAR file and place it in your lib/ folder.
- Open your browser and go to the MySQL Connector/J download page: dev.mysql.com/downloads/connector/j/.
- In the Select Operating System dropdown, select Platform Independent.
- Download the .zip archive (not the .tar.gz if you are on Windows).
- Extract the ZIP file. Inside you'll find a file called mysql-connector-j-9.7.0.jar.
- Copy mysql-connector-j-9.7.0.jar into your project's product-inventory-api/lib/ folder.
Skip the login prompt
The download page may ask you to log in or sign up. Look for the No thanks, just start my download link at the bottom of the page to skip this.
- Back in your terminal, verify the JAR is in place by running:
ls lib
You should see mysql-connector-j-9.7.0.jar listed.
Don't see the JAR file?
Make sure you copied the .jar file itself into the lib/ folder, not the extracted folder containing it. The extracted ZIP creates a nested folder. Open that folder to find the JAR inside.
help me find and move the MySQL Connector/J JAR file
Why not use Maven or Gradle?
Build tools like Maven and Gradle automate dependency management. In this project, you're managing the JAR manually to understand what those tools do under the hood.
When interviewers ask about classpath configuration, you'll know exactly how libraries are loaded because you did it yourself.
Download and install Postman
Postman is an API testing tool that lets you send HTTP requests (GET, POST, PUT, DELETE) without writing any client code. You'll use it in Step 6 to test every endpoint of your REST API.
- Open your browser and go to postman.com/downloads.
- Download the version for your operating system (macOS or Windows).
- Run the installer and follow the setup prompts.
- When Postman opens, it will ask you to create an account. Create a free account to continue.
What should I see?
After signing in, you should see the Postman workspace with options to create new requests. You won't use it until Step 6, so you can minimize it for now.
Finally, open VS Code with your project folder so you're ready to write code in the next step.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the product-inventory-api folder, then click Open.
In the Explorer sidebar on the left, you should see your src and lib folders listed.
Your project structure is ready and all your tools are in place. Next up, you'll design and create the MySQL database that your API will connect to.
Design and Create the MySQL Database
Your project folder is ready and all the tools are in place. Now it's time to create the database that your Java API will talk to.
Every REST API needs somewhere to store its data. In this step, you'll create a MySQL database and a products table with the right columns to hold your inventory data.
In this step, get ready to:
- Create a new MySQL database called inventory_db.
- Design and create a products table with proper column types.
- Insert sample data and verify it with a query.
Open the MySQL command-line client
The MySQL command-line client is an interactive shell where you can run SQL statements directly against your database server.
- Open your terminal and connect to MySQL as the root user by running this command:
mysql -u root -p
- When prompted, enter your MySQL root password and press Enter.
What does this command do?
The mysql program launches the command-line client. The -u root flag specifies the username, and -p tells MySQL to prompt you for a password instead of passing it in plain text.
You should see a welcome message and a mysql> prompt, which means you're connected and ready to run SQL.
Can't connect to MySQL?
Make sure your MySQL Server is running. On Windows, check Services (search for "Services" in the Start menu) and confirm the MySQL service shows Running. On macOS, check System Settings > MySQL or run mysql.server start in your terminal.
If you get an "Access denied" error, double-check that you're using the correct root password you set during MySQL installation.
help me connect to MySQL from my terminal
Create the inventory database and products table
Your API needs a dedicated database to keep its data separate from anything else on your MySQL server. You'll also define the table structure that maps to the Product model you'll build in Java later.
- Create the inventory_db database and switch to it by running these SQL statements:
-- Create a new database for the product inventory API
CREATE DATABASE inventory_db;
-- Switch to the new database
USE inventory_db;
What does this do?
CREATE DATABASE creates a brand new, empty database on your MySQL server. USE tells MySQL that all following statements should run against inventory_db.
- You should see Query OK after each statement and Database changed after the USE command.
- Now create the products table by running this SQL:
-- Define the products table with appropriate column types
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
quantity INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Why these column types?
- INT AUTO_INCREMENT PRIMARY KEY gives each product a unique ID that MySQL assigns automatically.
- VARCHAR(255) NOT NULL stores the product name and guarantees it can't be empty.
- TEXT allows long descriptions without a fixed length limit.
- DECIMAL(10,2) stores prices with exact precision (up to 10 digits, 2 after the decimal point). This avoids floating-point rounding errors with money.
- TIMESTAMP DEFAULT CURRENT_TIMESTAMP automatically records when each product was created.
You should see Query OK, 0 rows affected confirming the table was created.
Seeing a syntax error?
Make sure every SQL statement ends with a semicolon. If you accidentally pressed Enter without finishing the statement, you'll see a -> continuation prompt. Just type the rest of the statement and end with a semicolon.
help me fix a SQL syntax error when creating my products table
Insert sample products and verify the table
Before writing Java code, it's useful to have some data in the table. This lets you test your API's GET endpoints immediately once the server runs.
- Insert three sample products by running these statements:
-- Insert sample products for testing
INSERT INTO products (name, description, price, quantity)
VALUES ('Wireless Keyboard', 'Ergonomic wireless keyboard with backlight', 49.99, 150);
INSERT INTO products (name, description, price, quantity)
VALUES ('USB-C Hub', '7-in-1 USB-C hub with HDMI and ethernet', 34.99, 75);
INSERT INTO products (name, description, price, quantity)
VALUES ('Monitor Stand', 'Adjustable aluminum monitor stand', 89.99, 30);
What does this do?
Each INSERT INTO statement adds one row to the products table. You only specify name, description, price, and quantity because id and created_at are generated automatically by MySQL.
- Verify the data was inserted correctly by running a SELECT query:
-- Verify all products are in the table
SELECT * FROM products;
You should see a table with three rows showing all your products, their auto-generated IDs, and timestamps.
Don't see three rows?
Check that each INSERT statement returned Query OK, 1 row affected. If one failed, re-run just that statement. Make sure you're still in the inventory_db database (run USE inventory_db; if unsure).
help me troubleshoot my INSERT statements in MySQL
✔️ Awesome, I've got everything!
Great. Your database is ready with a properly structured table and sample data.
ⓧ I'd like to double check the full SQL
Here's all the SQL you should have run in order:
CREATE DATABASE inventory_db;
USE inventory_db;
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
quantity INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO products (name, description, price, quantity)
VALUES ('Wireless Keyboard', 'Ergonomic wireless keyboard with backlight', 49.99, 150);
INSERT INTO products (name, description, price, quantity)
VALUES ('USB-C Hub', '7-in-1 USB-C hub with HDMI and ethernet', 34.99, 75);
INSERT INTO products (name, description, price, quantity)
VALUES ('Monitor Stand', 'Adjustable aluminum monitor stand', 89.99, 30);
SELECT * FROM products;
Your database is designed and populated. Next up, you'll build the Java classes that connect to this database and model the product data.
Build the Product Model and Database Connection
In the previous step, you created the MySQL database and filled it with sample product data. Now you need Java classes that can represent that data and talk to the database.
In this step, you'll build two foundational classes. A Product model represents a single product as a Java object. A DatabaseConnection utility manages how your app connects to MySQL.
In this step, get ready to:
- Create a Product model with fields, constructors, and JSON serialization.
- Create a DatabaseConnection utility that connects to MySQL.
- Compile both files to verify everything builds correctly.
Create the Product model class
Your API needs a Java class to represent each product from the database. This class holds the same data as a row in your products table and provides a way to convert that data into JSON for API responses.
- In VS Code's Explorer sidebar, right-click the src folder and select New File.
- Name the file Product.java and press Enter.
- Copy the following code into Product.java:
public class Product {
// Fields matching the database columns
private int id;
private String name;
private String description;
private double price;
private int quantity;
private String createdAt;
// No-arg constructor for creating empty Product objects
public Product() {}
// Full constructor for building a Product with all data
public Product(int id, String name, String description, double price, int quantity, String createdAt) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.quantity = quantity;
this.createdAt = createdAt;
}
// Getters and setters for each field
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
// Converts this Product into a JSON string for API responses
public String toJson() {
return "{" +
"\"id\":" + id + "," +
"\"name\":\"" + escapeJson(name) + "\"," +
"\"description\":\"" + escapeJson(description) + "\"," +
"\"price\":" + price + "," +
"\"quantity\":" + quantity + "," +
"\"createdAt\":\"" + escapeJson(createdAt) + "\"" +
"}";
}
// Escapes special characters that would break JSON formatting
private String escapeJson(String value) {
if (value == null) return "";
return value.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}
What does this code do?
- The six private fields (id, name, description, price, quantity, createdAt) mirror the columns in your products table.
- Two constructors let you create an empty Product (useful when parsing input) or a fully populated one (useful when reading from the database).
- Getters and setters give other classes controlled access to each field.
- The toJson() method manually builds a JSON string from the fields. The escapeJson() helper prevents special characters (quotes, newlines, backslashes) from breaking the JSON format.
- Save src/Product.java.
Why manual JSON instead of a library?
This project is intentionally "vanilla Java" with no external dependencies beyond the JDBC driver. Building JSON by hand demonstrates that you understand serialization at a fundamental level.
In a production app, you would use a library like Jackson or Gson. For a portfolio project, showing you know what those libraries do under the hood is a differentiator in interviews.
Seeing a red underline in VS Code?
- Make sure the filename is exactly Product.java with a capital P. Java requires the filename to match the public class name.
- Check that you saved the file inside the src/ folder, not the project root.
- VS Code may show warnings before compilation. These will resolve once you compile in the final substep.
Why is VS Code showing errors in my Product.java file?
Create the database connection utility
Your Java code needs credentials and a URL to reach the MySQL database. A dedicated utility class keeps this configuration in one place so every other class can reuse it without duplicating connection details.
- In VS Code's Explorer sidebar, right-click the src folder and select New File.
- Name the file DatabaseConnection.java and press Enter.
- Copy the following code into DatabaseConnection.java:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseConnection {
// JDBC URL pointing to your local MySQL database
private static final String URL = "jdbc:mysql://localhost:3306/inventory_db";
private static final String USER = "root";
private static final String PASSWORD = "your-mysql-password-here";
// Returns a live connection to the database
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
}
What does this code do?
- The URL constant uses the JDBC protocol to point at your local MySQL server on port 3306 and the inventory_db database you created in the previous step.
- The getConnection() method is static, meaning any other class can call DatabaseConnection.getConnection() without creating an instance. It returns a live Connection object that can run SQL queries.
- The DriverManager class automatically finds the MySQL Connector/J driver on your classpath. No explicit driver loading is needed with JDK 11.
- Replace your-mysql-password-here with the password you use to log into MySQL.
- Save src/DatabaseConnection.java.
Not sure what your MySQL password is?
- It is the password you set when you installed MySQL Server. If you used the default root user during setup, try the password you chose during the MySQL installer wizard.
- If you can open the MySQL command-line client and run queries (like you did in Step 2), use the same password you typed there.
I can't remember my MySQL password. How do I reset it?
Compile and verify the build
Both files are written, but Java needs to compile them into bytecode before they can run. Compiling now catches any typos or import issues early.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, top-left of your keyboard).
- Make sure you are in the product-inventory-api folder. If not, navigate there by running:
cd ~/Desktop/product-inventory-api
- Compile both Java files by running this command:
javac -cp "lib/mysql-connector-j-9.7.0.jar" src/Product.java src/DatabaseConnection.java -d out
What does this command do?
- The -cp flag adds the MySQL Connector/J JAR to the classpath so Java can find the java.sql imports that DatabaseConnection.java uses.
- The two .java filenames tell javac exactly which files to compile.
- The -d out flag places compiled .class files into an out/ directory, keeping your source and output separate.
- Confirm the compiled files exist by running:
ls out
You should see Product.class and DatabaseConnection.class in the output. If the compile command produced no errors and these files exist, your code is correct.
Seeing a compilation error?
- If you see cannot find symbol, check that DatabaseConnection.java has the three import statements at the top (Connection, DriverManager, SQLException).
- If you see error: file not found, make sure you are running the command from inside the product-inventory-api folder, not from src/.
- If you see a classpath error, verify that lib/mysql-connector-j-9.7.0.jar exists in your project folder.
My Java files won't compile. Help me debug the error message.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved in VS Code before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for both files. Compare yours against these:
public class Product {
private int id;
private String name;
private String description;
private double price;
private int quantity;
private String createdAt;
public Product() {}
public Product(int id, String name, String description, double price, int quantity, String createdAt) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.quantity = quantity;
this.createdAt = createdAt;
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
public String toJson() {
return "{" +
"\"id\":" + id + "," +
"\"name\":\"" + escapeJson(name) + "\"," +
"\"description\":\"" + escapeJson(description) + "\"," +
"\"price\":" + price + "," +
"\"quantity\":" + quantity + "," +
"\"createdAt\":\"" + escapeJson(createdAt) + "\"" +
"}";
}
private String escapeJson(String value) {
if (value == null) return "";
return value.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseConnection {
private static final String URL = "jdbc:mysql://localhost:3306/inventory_db";
private static final String USER = "root";
private static final String PASSWORD = "your-mysql-password-here";
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
}
Your model and database connection are ready. Next up, you'll build the data access layer that uses these two classes to run real SQL queries against your products table.
Build the Data Access Layer
You have a Product model and a working database connection. The missing piece is the code that actually talks to your MySQL database to fetch, create, update, and delete products.
In this step, you will build a Data Access Object (DAO) class. This class is the single place in your project responsible for all database queries. Every method uses prepared statements to prevent SQL injection, a critical security practice tested in interviews.
In this step, get ready to:
- Create ProductDao.java with a method to retrieve all products.
- Add methods for reading, creating, updating, and deleting individual products.
- Compile the data access layer and verify it connects correctly.
Create the ProductDao class with getAllProducts
The DAO pattern separates database logic from the rest of your application. Your ProductDao class will contain one public method per database operation, keeping the code organized and testable.
- Create a new file called ProductDao.java inside the src/ folder of your product-inventory-api project.
- Copy the following code into src/ProductDao.java:
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class ProductDao {
// Retrieves every product row from the database
public List<Product> getAllProducts() throws SQLException {
List<Product> products = new ArrayList<>();
String sql = "SELECT id, name, description, price, quantity, created_at FROM products";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
products.add(mapRowToProduct(rs));
}
}
return products;
}
// Converts a single database row into a Product object
private Product mapRowToProduct(ResultSet rs) throws SQLException {
return new Product(
rs.getInt("id"),
rs.getString("name"),
rs.getString("description"),
rs.getDouble("price"),
rs.getInt("quantity"),
rs.getString("created_at")
);
}
}
What does this code do?
The getAllProducts() method runs a SELECT query and loops through every returned row. For each row, it calls the private helper mapRowToProduct() to convert the raw database row into a Product object.
The try (...) syntax is called try-with-resources. It automatically closes the Connection, PreparedStatement, and ResultSet when the block finishes. This prevents database connection leaks that crash production apps.
What is a DAO?
DAO stands for Data Access Object. It is a design pattern where all database queries live in one class. The rest of your application never writes raw SQL. It calls DAO methods instead.
This separation makes your code easier to test, debug, and swap databases later if needed.
- Save src/ProductDao.java.
Add the remaining CRUD methods
A full REST API needs to read a single product by ID, create new products, update existing ones, and delete them. Each operation maps to one method in the DAO.
- In src/ProductDao.java, find the closing brace of getAllProducts(). Add the following two methods between getAllProducts() and mapRowToProduct():
// Finds a single product by its database ID, returns null if not found
public Product getProductById(int id) throws SQLException {
String sql = "SELECT id, name, description, price, quantity, created_at FROM products WHERE id = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return mapRowToProduct(rs);
}
}
}
return null;
}
// Inserts a new product and sets the auto-generated ID on the object
public Product createProduct(Product product) throws SQLException {
String sql = "INSERT INTO products (name, description, price, quantity) VALUES (?, ?, ?, ?)";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, product.getName());
stmt.setString(2, product.getDescription());
stmt.setDouble(3, product.getPrice());
stmt.setInt(4, product.getQuantity());
stmt.executeUpdate();
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
product.setId(generatedKeys.getInt(1));
}
}
}
return product;
}
What does this code do?
The ? placeholders in the SQL strings are prepared statement parameters. You fill them in with stmt.setInt() or stmt.setString() calls. The database engine treats these values as pure data, never as executable SQL. This blocks SQL injection attacks.
The Statement.RETURN_GENERATED_KEYS flag in createProduct() tells MySQL to hand back the auto-incremented id it assigned to the new row. You then set that ID on the Product object before returning it.
- Now add the final two methods directly below createProduct() and above mapRowToProduct():
// Updates an existing product's fields, returns null if the ID doesn't exist
public Product updateProduct(int id, Product product) throws SQLException {
String sql = "UPDATE products SET name = ?, description = ?, price = ?, quantity = ? WHERE id = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, product.getName());
stmt.setString(2, product.getDescription());
stmt.setDouble(3, product.getPrice());
stmt.setInt(4, product.getQuantity());
stmt.setInt(5, id);
int rowsAffected = stmt.executeUpdate();
if (rowsAffected > 0) {
product.setId(id);
return product;
}
}
return null;
}
// Deletes a product by ID, returns true if a row was actually removed
public boolean deleteProduct(int id) throws SQLException {
String sql = "DELETE FROM products WHERE id = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, id);
int rowsAffected = stmt.executeUpdate();
return rowsAffected > 0;
}
}
What does this code do?
Both methods use executeUpdate() instead of executeQuery() because UPDATE and DELETE statements modify data rather than return rows.
The rowsAffected value tells you how many rows the statement touched. If it is 0, the requested ID does not exist. updateProduct() returns null in that case. deleteProduct() returns false.
- Save src/ProductDao.java.
Seeing red underlines in your editor?
If VS Code shows errors on the import lines or on DatabaseConnection, that is expected. Your editor does not know about the classpath JAR yet. The file will compile correctly from the terminal in the next substep.
Why am I seeing red underlines in my editor?
✔️ Awesome, I've got everything!
Great. Double check you have saved src/ProductDao.java with all five public methods and the private mapRowToProduct() helper.
ⓧ I'd like to double check the full code
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class ProductDao {
public List<Product> getAllProducts() throws SQLException {
List<Product> products = new ArrayList<>();
String sql = "SELECT id, name, description, price, quantity, created_at FROM products";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
products.add(mapRowToProduct(rs));
}
}
return products;
}
public Product getProductById(int id) throws SQLException {
String sql = "SELECT id, name, description, price, quantity, created_at FROM products WHERE id = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return mapRowToProduct(rs);
}
}
}
return null;
}
public Product createProduct(Product product) throws SQLException {
String sql = "INSERT INTO products (name, description, price, quantity) VALUES (?, ?, ?, ?)";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, product.getName());
stmt.setString(2, product.getDescription());
stmt.setDouble(3, product.getPrice());
stmt.setInt(4, product.getQuantity());
stmt.executeUpdate();
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
product.setId(generatedKeys.getInt(1));
}
}
}
return product;
}
public Product updateProduct(int id, Product product) throws SQLException {
String sql = "UPDATE products SET name = ?, description = ?, price = ?, quantity = ? WHERE id = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, product.getName());
stmt.setString(2, product.getDescription());
stmt.setDouble(3, product.getPrice());
stmt.setInt(4, product.getQuantity());
stmt.setInt(5, id);
int rowsAffected = stmt.executeUpdate();
if (rowsAffected > 0) {
product.setId(id);
return product;
}
}
return null;
}
public boolean deleteProduct(int id) throws SQLException {
String sql = "DELETE FROM products WHERE id = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, id);
int rowsAffected = stmt.executeUpdate();
return rowsAffected > 0;
}
}
private Product mapRowToProduct(ResultSet rs) throws SQLException {
return new Product(
rs.getInt("id"),
rs.getString("name"),
rs.getString("description"),
rs.getDouble("price"),
rs.getInt("quantity"),
rs.getString("created_at")
);
}
}
Compile and verify the data access layer
Your ProductDao depends on Product.java and DatabaseConnection.java. You need to compile all three source files together with the MySQL Connector/J JAR on the classpath.
- Compile all source files by running this command from the product-inventory-api folder:
javac -cp "lib/mysql-connector-j-9.7.0.jar" src/*.java -d out
What does this command do?
- -cp sets the classpath so the compiler can find the MySQL driver classes inside the JAR.
- src/*.java compiles every Java file in your src/ folder together.
- -d out places the compiled .class files into an out/ directory, keeping source and compiled code separate.
- Confirm the compiled files exist by running:
ls out
You should see Product.class, DatabaseConnection.class, and ProductDao.class listed in the output.
Seeing a compilation error?
- If you see a "cannot find symbol" error mentioning Product or DatabaseConnection, make sure all three .java files are in the src/ folder and you are running the command from the product-inventory-api root.
- If you see "package java.sql does not exist", check that you are using JDK 11 by running javac -version.
- If the out/ folder does not appear, create it manually with mkdir out and try again.
Help me fix my compilation error
Your data access layer is complete. Every database operation your API will need lives in one clean class. Next up, you will build the HTTP handler that receives web requests and calls these DAO methods.
Build the HTTP Handler and Start the Server
You have a data access layer that can talk to MySQL. Now you need a way for the outside world to talk to your data access layer.
In this step, you will build an HTTP handler that receives web requests and routes them to the correct DAO method. You will also create a server entry point that starts listening on port 8080.
In this step, get ready to:
- Create a request handler that routes HTTP methods to CRUD operations.
- Create a server class that starts your API on localhost.
- Compile and run your complete API server.
Create the HTTP handler with request routing
Java 11 includes a built-in HTTP server in the jdk.httpserver module. To use it, you create a class that implements HttpHandler and define a handle() method that receives every incoming request.
- Create a new file called ProductHandler.java inside your src/ folder.
- Add the imports, class declaration, and the main routing method by pasting this code:
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.*;
import java.net.URI;
import java.sql.SQLException;
import java.util.List;
public class ProductHandler implements HttpHandler {
// Create a single DAO instance shared by all handler methods
private final ProductDao productDao = new ProductDao();
@Override
public void handle(HttpExchange exchange) throws IOException {
// Extract the HTTP method and path from the incoming request
String method = exchange.getRequestMethod();
URI uri = exchange.getRequestURI();
String path = uri.getPath();
try {
// Route to the appropriate handler based on HTTP method
if (method.equals("GET")) {
handleGet(exchange, path);
} else if (method.equals("POST")) {
handlePost(exchange);
} else if (method.equals("PUT")) {
handlePut(exchange, path);
} else if (method.equals("DELETE")) {
handleDelete(exchange, path);
} else {
sendResponse(exchange, 405, "{\"error\":\"Method not allowed\"}");
}
} catch (SQLException e) {
sendResponse(exchange, 500, "{\"error\":\"Database error: " + e.getMessage() + "\"}");
}
}
}
What does this code do?
The handle() method is the single entry point for every HTTP request that hits your API.
- It reads the HTTP method (GET, POST, PUT, DELETE) and routes to a dedicated handler method for each one.
- If any database operation throws a SQLException, the catch block returns a 500 error response with the error message.
Next, add the handleGet method. This handles two scenarios: listing all products (no ID in the path) and getting a single product by ID.
- Inside the ProductHandler class (below the handle() method), add the following code:
private void handleGet(HttpExchange exchange, String path) throws IOException, SQLException {
// Check if the URL contains a product ID
int id = extractId(path);
if (id == -1) {
// No ID found - return all products as a JSON array
List<Product> products = productDao.getAllProducts();
StringBuilder json = new StringBuilder("[");
for (int i = 0; i < products.size(); i++) {
json.append(products.get(i).toJson());
if (i < products.size() - 1) {
json.append(",");
}
}
json.append("]");
sendResponse(exchange, 200, json.toString());
} else {
// ID found - return a single product or 404
Product product = productDao.getProductById(id);
if (product != null) {
sendResponse(exchange, 200, product.toJson());
} else {
sendResponse(exchange, 404, "{\"error\":\"Product not found\"}");
}
}
}
What does this code do?
- extractId(path) attempts to pull a product ID from the URL. If the path is /api/products it returns -1 (no ID). If the path is /api/products/3 it returns 3.
- The StringBuilder manually builds a JSON array by calling toJson() on each product and separating them with commas.
Now add the remaining CRUD handler methods for creating, updating, and deleting products.
- Below handleGet, add the handlePost, handlePut, and handleDelete methods:
private void handlePost(HttpExchange exchange) throws IOException, SQLException {
// Read the JSON body and create a new product
String body = readRequestBody(exchange);
Product product = parseProductFromJson(body);
Product created = productDao.createProduct(product);
sendResponse(exchange, 201, created.toJson());
}
private void handlePut(HttpExchange exchange, String path) throws IOException, SQLException {
int id = extractId(path);
if (id == -1) {
sendResponse(exchange, 400, "{\"error\":\"Product ID required\"}");
return;
}
// Read updated data and apply it to the existing product
String body = readRequestBody(exchange);
Product product = parseProductFromJson(body);
Product updated = productDao.updateProduct(id, product);
if (updated != null) {
sendResponse(exchange, 200, updated.toJson());
} else {
sendResponse(exchange, 404, "{\"error\":\"Product not found\"}");
}
}
private void handleDelete(HttpExchange exchange, String path) throws IOException, SQLException {
int id = extractId(path);
if (id == -1) {
sendResponse(exchange, 400, "{\"error\":\"Product ID required\"}");
return;
}
// Delete and return 204 (no content) on success
boolean deleted = productDao.deleteProduct(id);
if (deleted) {
sendResponse(exchange, 204, "");
} else {
sendResponse(exchange, 404, "{\"error\":\"Product not found\"}");
}
}
What do these methods do?
- handlePost reads the request body as JSON, creates a Product object, saves it to the database, and returns HTTP 201 (Created).
- handlePut requires an ID in the URL. It reads updated fields from the body and applies them to the matching product.
- handleDelete removes the product and returns HTTP 204 (No Content) on success.
Finally, add the helper methods that handle request parsing and response sending.
- Below handleDelete, paste all of the following helper methods:
// Extract a product ID from the URL path (e.g. /api/products/3 returns 3)
private int extractId(String path) {
String[] parts = path.split("/");
if (parts.length >= 4) {
try {
return Integer.parseInt(parts[3]);
} catch (NumberFormatException e) {
return -1;
}
}
return -1;
}
// Read the full request body as a string
private String readRequestBody(HttpExchange exchange) throws IOException {
InputStream is = exchange.getRequestBody();
StringBuilder sb = new StringBuilder();
int b;
while ((b = is.read()) != -1) {
sb.append((char) b);
}
is.close();
return sb.toString();
}
// Convert a JSON string into a Product object
private Product parseProductFromJson(String json) {
Product product = new Product();
product.setName(extractJsonString(json, "name"));
product.setDescription(extractJsonString(json, "description"));
product.setPrice(extractJsonDouble(json, "price"));
product.setQuantity(extractJsonInt(json, "quantity"));
return product;
}
// Extract a string value from JSON by key
private String extractJsonString(String json, String key) {
String searchKey = "\"" + key + "\"";
int keyIndex = json.indexOf(searchKey);
if (keyIndex == -1) return "";
int colonIndex = json.indexOf(":", keyIndex);
int startQuote = json.indexOf("\"", colonIndex + 1);
int endQuote = json.indexOf("\"", startQuote + 1);
if (startQuote == -1 || endQuote == -1) return "";
return json.substring(startQuote + 1, endQuote);
}
// Extract a double value from JSON by key
private double extractJsonDouble(String json, String key) {
String value = extractJsonValue(json, key);
try {
return Double.parseDouble(value);
} catch (NumberFormatException e) {
return 0.0;
}
}
// Extract an int value from JSON by key
private int extractJsonInt(String json, String key) {
String value = extractJsonValue(json, key);
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return 0;
}
}
// Extract a raw value (number or unquoted) from JSON by key
private String extractJsonValue(String json, String key) {
String searchKey = "\"" + key + "\"";
int keyIndex = json.indexOf(searchKey);
if (keyIndex == -1) return "0";
int colonIndex = json.indexOf(":", keyIndex);
int start = colonIndex + 1;
while (start < json.length() && json.charAt(start) == ' ') {
start++;
}
int end = start;
while (end < json.length() && json.charAt(end) != ',' && json.charAt(end) != '}') {
end++;
}
return json.substring(start, end).trim();
}
// Send an HTTP response with the given status code and JSON body
private void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
exchange.getResponseHeaders().set("Content-Type", "application/json");
if (statusCode == 204) {
exchange.sendResponseHeaders(204, -1);
} else {
byte[] bytes = response.getBytes("UTF-8");
exchange.sendResponseHeaders(statusCode, bytes.length);
OutputStream os = exchange.getResponseBody();
os.write(bytes);
os.close();
}
}
What do these helpers do?
- extractId splits the URL path by / and grabs the 4th segment as a number. This is how /api/products/3 becomes the integer 3.
- The JSON extraction methods manually parse the request body without any external library. They search for a key in the JSON string and pull out its value.
- sendResponse sets the Content-Type header to application/json and writes the response bytes. For 204 responses, it sends no body.
Why parse JSON manually instead of using a library?
This project uses zero external libraries besides the JDBC driver. Parsing JSON manually demonstrates that you understand serialization at a fundamental level. In a production app, you would use a library like Jackson or Gson for complex nested objects.
- Save src/ProductHandler.java.
✔️ Awesome, I've got everything!
Great. Double check you have saved the file before moving on.
ⓧ I'd like to double check the full code
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.*;
import java.net.URI;
import java.sql.SQLException;
import java.util.List;
public class ProductHandler implements HttpHandler {
private final ProductDao productDao = new ProductDao();
@Override
public void handle(HttpExchange exchange) throws IOException {
String method = exchange.getRequestMethod();
URI uri = exchange.getRequestURI();
String path = uri.getPath();
try {
if (method.equals("GET")) {
handleGet(exchange, path);
} else if (method.equals("POST")) {
handlePost(exchange);
} else if (method.equals("PUT")) {
handlePut(exchange, path);
} else if (method.equals("DELETE")) {
handleDelete(exchange, path);
} else {
sendResponse(exchange, 405, "{\"error\":\"Method not allowed\"}");
}
} catch (SQLException e) {
sendResponse(exchange, 500, "{\"error\":\"Database error: " + e.getMessage() + "\"}");
}
}
private void handleGet(HttpExchange exchange, String path) throws IOException, SQLException {
int id = extractId(path);
if (id == -1) {
List<Product> products = productDao.getAllProducts();
StringBuilder json = new StringBuilder("[");
for (int i = 0; i < products.size(); i++) {
json.append(products.get(i).toJson());
if (i < products.size() - 1) {
json.append(",");
}
}
json.append("]");
sendResponse(exchange, 200, json.toString());
} else {
Product product = productDao.getProductById(id);
if (product != null) {
sendResponse(exchange, 200, product.toJson());
} else {
sendResponse(exchange, 404, "{\"error\":\"Product not found\"}");
}
}
}
private void handlePost(HttpExchange exchange) throws IOException, SQLException {
String body = readRequestBody(exchange);
Product product = parseProductFromJson(body);
Product created = productDao.createProduct(product);
sendResponse(exchange, 201, created.toJson());
}
private void handlePut(HttpExchange exchange, String path) throws IOException, SQLException {
int id = extractId(path);
if (id == -1) {
sendResponse(exchange, 400, "{\"error\":\"Product ID required\"}");
return;
}
String body = readRequestBody(exchange);
Product product = parseProductFromJson(body);
Product updated = productDao.updateProduct(id, product);
if (updated != null) {
sendResponse(exchange, 200, updated.toJson());
} else {
sendResponse(exchange, 404, "{\"error\":\"Product not found\"}");
}
}
private void handleDelete(HttpExchange exchange, String path) throws IOException, SQLException {
int id = extractId(path);
if (id == -1) {
sendResponse(exchange, 400, "{\"error\":\"Product ID required\"}");
return;
}
boolean deleted = productDao.deleteProduct(id);
if (deleted) {
sendResponse(exchange, 204, "");
} else {
sendResponse(exchange, 404, "{\"error\":\"Product not found\"}");
}
}
private int extractId(String path) {
String[] parts = path.split("/");
if (parts.length >= 4) {
try {
return Integer.parseInt(parts[3]);
} catch (NumberFormatException e) {
return -1;
}
}
return -1;
}
private String readRequestBody(HttpExchange exchange) throws IOException {
InputStream is = exchange.getRequestBody();
StringBuilder sb = new StringBuilder();
int b;
while ((b = is.read()) != -1) {
sb.append((char) b);
}
is.close();
return sb.toString();
}
private Product parseProductFromJson(String json) {
Product product = new Product();
product.setName(extractJsonString(json, "name"));
product.setDescription(extractJsonString(json, "description"));
product.setPrice(extractJsonDouble(json, "price"));
product.setQuantity(extractJsonInt(json, "quantity"));
return product;
}
private String extractJsonString(String json, String key) {
String searchKey = "\"" + key + "\"";
int keyIndex = json.indexOf(searchKey);
if (keyIndex == -1) return "";
int colonIndex = json.indexOf(":", keyIndex);
int startQuote = json.indexOf("\"", colonIndex + 1);
int endQuote = json.indexOf("\"", startQuote + 1);
if (startQuote == -1 || endQuote == -1) return "";
return json.substring(startQuote + 1, endQuote);
}
private double extractJsonDouble(String json, String key) {
String value = extractJsonValue(json, key);
try {
return Double.parseDouble(value);
} catch (NumberFormatException e) {
return 0.0;
}
}
private int extractJsonInt(String json, String key) {
String value = extractJsonValue(json, key);
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return 0;
}
}
private String extractJsonValue(String json, String key) {
String searchKey = "\"" + key + "\"";
int keyIndex = json.indexOf(searchKey);
if (keyIndex == -1) return "0";
int colonIndex = json.indexOf(":", keyIndex);
int start = colonIndex + 1;
while (start < json.length() && json.charAt(start) == ' ') {
start++;
}
int end = start;
while (end < json.length() && json.charAt(end) != ',' && json.charAt(end) != '}') {
end++;
}
return json.substring(start, end).trim();
}
private void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
exchange.getResponseHeaders().set("Content-Type", "application/json");
if (statusCode == 204) {
exchange.sendResponseHeaders(204, -1);
} else {
byte[] bytes = response.getBytes("UTF-8");
exchange.sendResponseHeaders(statusCode, bytes.length);
OutputStream os = exchange.getResponseBody();
os.write(bytes);
os.close();
}
}
}
Create the server entry point
The handler knows how to process requests, but something needs to actually listen for incoming connections and pass them to the handler. That is the job of HttpServer.
- Create a new file called ProductApiServer.java inside your src/ folder.
- Paste the following code:
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
public class ProductApiServer {
public static void main(String[] args) throws IOException {
int port = 8080;
// Create an HTTP server bound to port 8080
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
// Map the /api/products path to our handler
server.createContext("/api/products", new ProductHandler());
// Use the default single-threaded executor
server.setExecutor(null);
server.start();
System.out.println("Server started on http://localhost:" + port);
System.out.println("Endpoints:");
System.out.println(" GET /api/products - List all products");
System.out.println(" GET /api/products/{id} - Get product by ID");
System.out.println(" POST /api/products - Create a product");
System.out.println(" PUT /api/products/{id} - Update a product");
System.out.println(" DELETE /api/products/{id} - Delete a product");
}
}
What does this code do?
- HttpServer.create(new InetSocketAddress(port), 0) creates a server that listens on port 8080. The second argument (0) means use the system default backlog for queued connections.
- createContext("/api/products", new ProductHandler()) tells the server that any request starting with /api/products should be handled by your ProductHandler.
- setExecutor(null) tells the server to use a default single-threaded executor. For a local development API, this is sufficient.
- Save src/ProductApiServer.java.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before compiling.
ⓧ I'd like to double check the full code
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
public class ProductApiServer {
public static void main(String[] args) throws IOException {
int port = 8080;
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/api/products", new ProductHandler());
server.setExecutor(null);
server.start();
System.out.println("Server started on http://localhost:" + port);
System.out.println("Endpoints:");
System.out.println(" GET /api/products - List all products");
System.out.println(" GET /api/products/{id} - Get product by ID");
System.out.println(" POST /api/products - Create a product");
System.out.println(" PUT /api/products/{id} - Update a product");
System.out.println(" DELETE /api/products/{id} - Delete a product");
}
}
Compile and run the server
All your source files are ready. Now you need to compile them together with the MySQL Connector/J JAR on the classpath so the compiler can find the JDBC classes.
- Make sure your terminal is in the product-inventory-api folder (the folder that contains src/ and lib/).
- Compile all Java files by running this command:
javac -cp "lib/mysql-connector-j-9.7.0.jar" src/*.java -d out
What does this command do?
- -cp "lib/mysql-connector-j-9.7.0.jar" adds the MySQL driver to the classpath so the compiler can resolve JDBC imports.
- src/*.java compiles every Java file in the src/ folder at once.
- -d out places the compiled .class files into an out/ directory, keeping your source folder clean.
- If compilation succeeds, you will see no output. Verify the out/ folder was created by running:
ls out
You should see .class files for each of your Java source files.
Seeing compilation errors?
- If you see "cannot find symbol" for ProductDao or Product, make sure all your .java files are in the src/ folder and you are running the command from the product-inventory-api root.
- If you see errors about com.sun.net.httpserver not being found, verify you are using JDK 11 or higher by running javac -version.
- Check that mysql-connector-j-9.7.0.jar is in your lib/ folder. The filename must match exactly.
Help me debug my Java compilation error.
- Start your API server by running:
macOS
java -cp "out:lib/mysql-connector-j-9.7.0.jar" ProductApiServer
Windows
java -cp "out;lib/mysql-connector-j-9.7.0.jar" ProductApiServer
Why is the classpath separator different?
macOS and Linux use a colon (:) to separate classpath entries. Windows uses a semicolon. The classpath tells Java where to find both your compiled classes (out/) and the MySQL driver JAR.
You should see the following output in your terminal:
Server started on http://localhost:8080
Endpoints:
GET /api/products - List all products
GET /api/products/{id} - Get product by ID
POST /api/products - Create a product
PUT /api/products/{id} - Update a product
DELETE /api/products/{id} - Delete a product
Your server is now running and listening for requests. Keep this terminal window open.
Server not starting?
- If you see a database connection error, make sure your MySQL server is running and the password in DatabaseConnection.java is correct.
- If you see "Address already in use", another process is using port 8080. Close it or change the port number in ProductApiServer.java.
Help me fix my server startup error.
Your API is live and waiting for requests. Next up, you will test every endpoint using Postman to make sure the full CRUD cycle works end to end.
Test Your API with Postman
Your server is running and waiting for requests. The real test of any API is whether it responds correctly to actual HTTP requests from clients.
Postman lets you send any HTTP method (GET, POST, PUT, DELETE) to your API and inspect the full response. You will use it to exercise every endpoint and confirm the correct status codes come back.
In this step, get ready to:
- Send GET requests to retrieve products from the database.
- Create, update, and delete products using POST, PUT, and DELETE.
- Verify correct HTTP status codes and JSON responses for each operation.
Send a GET request to retrieve all products
A GET request reads data without modifying it. Your API exposes two GET endpoints: one for all products and one for a single product by ID.
- Open Postman.
- Click the + tab at the top to create a new request.
- Make sure the dropdown to the left of the URL bar is set to GET.
- Enter http://localhost:8080/api/products in the URL bar.
- Click Send.
You should see a JSON array in the response body containing the 3 sample products you inserted earlier. The status code in the top-right of the response panel should read 200 OK.
What does 200 OK mean?
HTTP status codes tell the client what happened with their request. 200 means the server processed the request successfully and returned data.
Now test retrieving a single product by ID.
- Change the URL to http://localhost:8080/api/products/1.
- Click Send.
You should see a single product JSON object instead of an array. The status code should still be 200 OK.
Seeing an empty array or connection error?
Make sure your server is still running in the terminal. You should see the Server started on http://localhost:8080 message. If the terminal is closed, recompile and run the server again.
If you see an empty array [], your database may not have the sample products. Go back to your MySQL client and re-run the INSERT statements from Step 2.
Still stuck? Help me troubleshoot why my GET request is not returning products.
Test POST, PUT, and DELETE operations
GET is the simplest HTTP method because it only reads data. POST, PUT, and DELETE modify the database, and they require you to send a JSON body with the request.
Start by creating a new product with POST.
- Change the method dropdown from GET to POST.
- Set the URL back to http://localhost:8080/api/products.
- Click the Body tab below the URL bar.
- Select the raw radio button.
- Change the dropdown to the right of raw from Text to JSON.
- Paste the following JSON into the body text area:
{
"name": "Bluetooth Speaker",
"description": "Portable wireless speaker with 10-hour battery",
"price": 49.99,
"quantity": 30
}
- Click Send.
You should see a 201 Created status code. The response body shows the product you just created, now with an auto-generated id field.
Why 201 instead of 200?
Status code 201 specifically means a new resource was created. It tells the client that something was added to the database, not just that the request succeeded.
Note the id value in the response. You will use it for the PUT and DELETE requests next. It is likely 4 since you already have 3 sample products.
Now update the product you just created using PUT.
- Change the method dropdown to PUT.
- Change the URL to http://localhost:8080/api/products/4 (replace 4 with the actual ID from your POST response).
- Replace the JSON body with:
{
"name": "Bluetooth Speaker Pro",
"description": "Premium portable wireless speaker with 20-hour battery",
"price": 79.99,
"quantity": 25
}
- Click Send.
You should see a 200 OK status code. The response body shows the updated product with the new name, description, price, and quantity.
Finally, delete the product.
- Change the method dropdown to DELETE.
- Keep the same URL (http://localhost:8080/api/products/4 or your actual product ID).
- Click Send.
You should see a 204 No Content status code with an empty response body. This means the deletion succeeded and there is nothing to return.
Getting a 404 or 500 error?
A 404 on PUT or DELETE means the product ID in your URL does not exist in the database. Double-check the id value from your earlier POST response.
A 500 error means something went wrong on the database side. Check your terminal for the full error message printed by the server.
Still stuck? Help me debug my REST API responses in Postman.
Verify error handling with invalid requests
A well-built API does not just handle happy paths. It also responds gracefully when a client asks for something that does not exist.
- Change the method dropdown back to GET.
- Change the URL to http://localhost:8080/api/products/999.
- Click Send.
You should see a 404 status code with the response body {"error":"Product not found"}.
- Now try deleting the same product you already deleted. Change the method to DELETE and set the URL to your previously deleted product's URL (e.g. http://localhost:8080/api/products/4).
- Click Send.
You should see a 404 status code again. Your API correctly reports that the product no longer exists.
Why proper status codes matter
In a real application, frontend code and mobile apps rely on status codes to decide what to show the user. A 200 displays the data. A 404 shows a "not found" page. A 201 confirms a creation succeeded.
Returning the wrong status code (like 200 for everything) forces the client to parse the body to figure out what happened. Correct status codes are a sign of professional API design.
- Send one final GET request to http://localhost:8080/api/products to confirm the full product list is back to your original 3 sample products.
You should see a JSON array with your 3 original products. The Bluetooth Speaker you created and then deleted is gone.
Secret mission
Add Search with Pagination
Your API handles basic CRUD, but real-world APIs need search and pagination. Add a /api/products/search endpoint that accepts query parameters for filtering by name and paginating results.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- MySQL database inventory_db (created during the project).
- Project folder product-inventory-api/ with all source files and the Connector/J JAR.
- Postman (installed for API testing).
Keep everything running
No action needed. Choose this if you're still actively building or plan to extend the project with the Secret Mission.
- Your MySQL database stays available for queries whenever MySQL Server is running.
- Your project folder and compiled files remain on your Desktop.
- Postman stays installed for future API testing.
Pause - I'll come back to this later
Shut down running processes to free up memory, but keep your files and database intact.
- Stop the Java API server by pressing Ctrl+C in the terminal where it is running.
- Stop the MySQL service if you want to free memory. On macOS, run:
- On Windows, open Services (search for it in the Start menu), find MySQL, and click Stop.
- Your database and project files stay on disk until you're ready to resume.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Drop the MySQL database by opening the MySQL command-line client and running:
DROP DATABASE inventory_db;
- Delete the project folder. On macOS/Linux, run:
rm -rf ~/Desktop/product-inventory-api
- On Windows, run:
rmdir /s /q %USERPROFILE%\Desktop\product-inventory-api
- Uninstall Postman. On macOS, open Finder, go to Applications, right-click Postman, and select Move to Trash.
- On Windows, open Settings, select Apps, find Postman in the list, and click Uninstall.
Nice Work!
Nice Work!
You just built a production-style REST API from scratch with zero frameworks. This is a fully functional backend that handles CRUD operations, connects to a real database, and responds with proper HTTP status codes and JSON.
Here's what you accomplished:
- Built a fully functional CRUD REST API using Java 11's built-in HttpServer with proper HTTP status codes and JSON responses.
- Designed a layered architecture (Model, DAO, Handler, Server) that mirrors how enterprise Java applications are structured.
- Wrote secure database queries using JDBC prepared statements to prevent SQL injection, connecting Java to MySQL with Connector/J.
- Secret Mission: Added search functionality with SQL LIKE queries and pagination.
Ready to quiz yourself?