Build a Xerox Shop Order API

Build a Spring Boot REST API with full CRUD, JPA, and error handling.

Introduction

30 Second Summary

Walk into any Xerox or print shop and you will see the same scene. Customers crowding the counter asking if their prints are ready, while the shop owner flips through a stack of handwritten slips trying to remember whose order is next.

In this project, you will build SmartQueue, a REST API powered by Spring Boot that manages a print shop's order queue. You will create endpoints that accept print orders, track them from received to collected, and store everything in a MySQL database.

What You'll Build

You send a print order from your terminal, the API saves it to MySQL with a generated ID, and from that point on you can pull up any order, advance its status through the queue, or delete it when the customer picks it up.

By the end of this project, you'll have:

  • A working order management API where you POST a print order and GET it back from a real MySQL database, with each order tracked through five statuses from RECEIVED to COLLECTED.
  • A cleanly layered Spring Boot application where your controller handles HTTP, your service owns business logic, and your repository manages database access, all wired together with dependency injection.
  • Production-quality error handling where you send invalid data on purpose and watch the API respond with structured 400 and 404 error messages instead of crashing with a stack trace.
  • An optional Secret Mission to push your skills further.

Are there any prerequisites?

This project assumes basic Java knowledge such as classes, interfaces, and methods. You should have JDK 21, IntelliJ IDEA Community Edition, and MySQL already installed on your machine.

Before We Start

Before any hands-on work, let's commit to what you're building and why. You're about to create SmartQueue, a backend REST API that manages a Xerox shop's print order queue from submission to collection.

Set Up the Project and MySQL Database

Before you can write any API code, you need two things: a runnable Spring Boot project with the right dependencies, and a MySQL database where orders will be stored.

Spring Initializr is a web tool that generates a ready-to-go Spring Boot project skeleton with exactly the dependencies you pick. In this step, you will generate the project, create your MySQL database, and confirm the app starts successfully.

In this step, get ready to:
  • Generate a Spring Boot project using Spring Initializr.
  • Create the smartqueue MySQL database and configure the connection.
  • Run the application and verify the server starts on port 8080.
Generate the project on Spring Initializr

Spring Initializr builds a project folder with your build tool, dependencies, and main class already wired together. You tell it what you need, and it gives you a ZIP file ready to open in your IDE.

  • Open https://start.spring.io in your browser.
  • Set Project to Maven.
  • Set Language to Java.
  • Set Spring Boot to 4.1.0.
  • Under Project Metadata, set Group to com.smartqueue.
  • Set Artifact to smartqueue.
  • Set Name to smartqueue.
  • Confirm Package name reads com.smartqueue.
  • Set Packaging to Jar.
  • Set Java to 21.

Now you need to add four dependencies. These are libraries that Spring Boot will pull into your project automatically.

  • Click the ADD DEPENDENCIES button on the right side of the page.
  • Search for Spring Web and select it. This provides REST controller support and an embedded Tomcat server.
  • Search for Spring Data JPA and select it. This provides JPA and Hibernate for database access.
  • Search for MySQL Driver and select it. This is the JDBC driver that connects Java to MySQL.
  • Search for Validation and select it. This provides annotations like @NotBlank and @Min for input validation.
  • Click GENERATE at the bottom of the page. A ZIP file named smartqueue.zip downloads to your computer.
  • Extract the ZIP file. You now have a smartqueue folder containing the full project skeleton.

Now open this project in IntelliJ IDEA.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type IntelliJ IDEA and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open.
  • Navigate to the extracted smartqueue folder and select it, then click OK.

IntelliJ will take a moment to import the project and download dependencies via Maven. Wait until the progress bar in the bottom-right corner finishes.

What did Spring Initializr create?

In the Project sidebar on the left, expand src/main/java/com/smartqueue. You should see SmartqueueApplication.java. This is the entry point of your Spring Boot application.

You should also see pom.xml at the root of the project. This is the Maven configuration file that lists your four dependencies.

Don't see the project files?

Make sure you opened the smartqueue folder itself, not a parent folder containing it. The pom.xml file should be at the top level of what you opened.

If IntelliJ is still downloading dependencies, wait for the progress bar to finish before looking for files.

Help me troubleshoot my IntelliJ project import

Create the MySQL database and configure the connection

Your Spring Boot project needs a database to store orders. You will create an empty MySQL database now, then tell Spring Boot how to connect to it.

  • Open a terminal window. In IntelliJ, click Terminal in the bottom panel bar, or press Alt+F12 (Windows) or Option+F12 (macOS).
  • Connect to MySQL by running this command:
mysql -u root -p

What happens when you run this?

The -p flag tells MySQL to prompt you for your root password. Type your MySQL root password and press Enter. The characters will not appear as you type. Once you are in, you will see a mysql> prompt.

  • Create the database and exit the MySQL shell by running these two commands one at a time:
CREATE DATABASE smartqueue;
EXIT;
  • Verify the database was created by reconnecting and listing all databases:
mysql -u root -p -e "SHOW DATABASES;"

You should see smartqueue in the list of databases.

Getting 'Access denied' or 'command not found'?

If you see 'Access denied', double-check the root password you set when you installed MySQL. If you see 'command not found', the mysql command is not in your system's PATH. Try using the full path to the MySQL binary (e.g., C:\Program Files\MySQL\MySQL Server 8.4\bin\mysql on Windows).

Help me connect to MySQL from my terminal

Now you need to tell Spring Boot how to connect to this database.

  • In IntelliJ's Project sidebar, expand src/main/resources and double-click application.properties to open it.
  • Replace the entire contents of the file with this configuration:
spring.application.name=smartqueue

# MySQL database connection settings
spring.datasource.url=jdbc:mysql://localhost:3306/smartqueue
spring.datasource.username=root
spring.datasource.password=your-mysql-password-here

# JPA settings: auto-create tables and log SQL queries
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
  • Replace your-mysql-password-here with the actual root password you used when connecting to MySQL a moment ago.
  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).

What do these properties do?

  • spring.datasource.url tells Spring Boot where your MySQL database lives. The 3306 is MySQL's default port.
  • spring.jpa.hibernate.ddl-auto=update tells Hibernate to automatically create or update database tables based on your Java classes. You will not need to write any SQL to create tables.
  • spring.jpa.show-sql=true prints every SQL query Hibernate runs to the console. This is useful for debugging.

✔️ Awesome, I've got everything!

Great. Double check you have saved application.properties and that your MySQL password is filled in.

ⓧ I'd like to double check the full code

Here is the complete application.properties file. Make sure yours matches (with your own password in place of the placeholder).

spring.application.name=smartqueue

spring.datasource.url=jdbc:mysql://localhost:3306/smartqueue
spring.datasource.username=root
spring.datasource.password=your-mysql-password-here

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
Run the application and verify Tomcat starts

Everything is configured. Time to see if your app actually starts and connects to MySQL.

  • In the Project sidebar, navigate to src/main/java/com/smartqueue and double-click SmartqueueApplication.java to open it.
  • Click the green Run triangle in the gutter next to public class SmartqueueApplication, then click Run 'SmartqueueApplication'.

Prefer the terminal?

You can also run the application from the terminal inside IntelliJ. Make sure you are in the smartqueue project root folder and run:

.\mvnw.cmd spring-boot:run

On macOS or Linux, use ./mvnw spring-boot:run instead.

Before you look at the console output, what do you think will happen when the app starts? Will it succeed or fail?

Watch the Run panel at the bottom of IntelliJ. After a few seconds, you should see a line containing Tomcat started on port 8080 in the console output. That means your embedded web server is running.

  • Open your browser and go to http://localhost:8080.

You should see a page titled Whitelabel Error Page. This is not a real error. It is Spring Boot's default page confirming the server is running and handling requests. There are no endpoints yet, so Spring has nothing to show you.

Application failed to start?

  • Check your MySQL password in application.properties. It must match the password you used with the mysql -u root -p command earlier.
  • Make sure the MySQL service is running. On Windows, open Services (search for it in the Start menu) and confirm MySQL shows a status of Running.
  • If the error mentions SSL or public key retrieval, add ?sslMode=DISABLED&allowPublicKeyRetrieval=true to the end of your datasource URL so it reads jdbc:mysql://localhost:3306/smartqueue?sslMode=DISABLED&allowPublicKeyRetrieval=true.

Help me fix my Spring Boot startup error

Your project skeleton is ready and your database is connected. Next up, you will build your first REST endpoint and see Spring Boot turn a plain Java class into a web API.

Build Your First REST Endpoint

Your Spring Boot project is set up, your MySQL database is ready, and Tomcat is running on port 8080. That Whitelabel Error Page at http://localhost:8080 means the server has no endpoints to handle requests yet.

Before connecting to the database or designing entities, you need to see Spring Boot handle a request and return JSON. In this step, you will create a REST controller with two endpoints and test them both.

In this step, get ready to:
  • Create an OrderController class with GET and POST endpoints.
  • Test both endpoints to confirm Spring Boot returns JSON responses.
Create the OrderController class

Every REST API needs a controller. A controller is a class that Spring Boot scans at startup, looking for annotations that map URLs to methods.

When a request arrives at a matching URL, Spring calls the corresponding method and converts its return value to JSON automatically.

  • In IntelliJ, find the com.smartqueue package in the Project panel on the left side.
  • Right-click the com.smartqueue package, select New, then Package.
  • Name the new package controller and press Enter.
  • Right-click the new controller package, select New, then Java Class.
  • Name the class OrderController and press Enter.
  • Replace everything in the new file with this code:
package com.smartqueue.controller;

import org.springframework.web.bind.annotation.*;
import java.util.Map;

// Marks this class as a REST API controller
@RestController
// Every endpoint in this class starts with /api/orders
@RequestMapping("/api/orders")
public class OrderController {

    // Responds to GET requests at /api/orders
    @GetMapping
    public Map<String, String> getAllOrders() {
        // Returns a key-value pair that Spring converts to JSON
        return Map.of("message", "Welcome to SmartQueue API");
    }

    // Responds to POST requests at /api/orders
    @PostMapping
    public Map<String, String> createOrder(@RequestBody Map<String, Object> body) {
        // Hardcoded response for now
        return Map.of("status", "Order received", "id", "1");
    }
}

What does this code do?

@RestController tells Spring Boot this class handles HTTP requests and returns data, not HTML pages. @RequestMapping("/api/orders") sets the base URL so every endpoint in this class lives under /api/orders.

@GetMapping maps the getAllOrders() method to GET requests. @PostMapping maps createOrder() to POST requests. Both methods return a Map, which Spring automatically serializes into JSON for the response body.

  • Save OrderController.java by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Stop the running application by clicking the red Stop button in IntelliJ's toolbar at the top right.
  • Run the application again by clicking the green Run button on SmartqueueApplication.java.
  • In the console output at the bottom, confirm you see Tomcat started on port 8080 with no errors.

App won't start or showing errors?

  • Make sure OrderController.java is inside the com.smartqueue.controller package. If it is outside the com.smartqueue package tree, Spring Boot will not detect it during component scanning.
  • Check for red underlines in IntelliJ. A typo in an annotation name or a missing import causes compile errors.
  • My Spring Boot app won't start after adding OrderController. What could be wrong?

✔️ Awesome, I've got everything!

Great. Double check you have saved OrderController.java and that the app restarted without errors.

ⓧ I'd like to double check the full code

Here is the complete OrderController.java file. Make sure yours matches:

package com.smartqueue.controller;

import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping
    public Map<String, String> getAllOrders() {
        return Map.of("message", "Welcome to SmartQueue API");
    }

    @PostMapping
    public Map<String, String> createOrder(@RequestBody Map<String, Object> body) {
        return Map.of("status", "Order received", "id", "1");
    }
}
Test both endpoints

Your controller is live. Now you need to prove that Spring Boot is routing HTTP requests to your methods and converting Java objects to JSON.

Before you test, think about this: what do you expect to see when you visit http://localhost:8080/api/orders in your browser?

  • In your browser, navigate to http://localhost:8080/api/orders.

Instead of the Whitelabel Error Page, you now see JSON in the browser: {"message":"Welcome to SmartQueue API"}. That is the Map your getAllOrders() method returned, automatically converted to JSON by Spring Boot.

Why does the browser show JSON?

When you type a URL in the browser and press Enter, the browser sends a GET request. Spring Boot matches the URL /api/orders to your @GetMapping method, calls it, and sends the returned Map back as JSON. No extra configuration needed.

Browsers send GET requests by default. To test a POST endpoint, you need a tool that lets you choose the HTTP method. curl does this from the terminal.

  • In IntelliJ, click the Terminal tab at the bottom of the screen to open a terminal panel.
  • Send a POST request to your controller by running this command:
curl -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d "{\"test\": \"hello\"}"

The terminal prints {"id":"1","status":"Order received"}. This is the hardcoded response from your createOrder() method.

What does this curl command do?

  • -X POST tells curl to send a POST request instead of the default GET.
  • -H "Content-Type: application/json" sets a header telling the server the body contains JSON.
  • -d sends a JSON body with the request. The backslashes escape the double quotes inside the JSON string.

Curl not returning JSON?

  • If you see a 404 error or the Whitelabel Error Page, make sure the URL is exactly http://localhost:8080/api/orders. A missing /api or /orders sends the request to a path with no mapping.
  • On Windows, PowerShell may alias curl to a different command. Try using curl.exe instead of curl in your terminal.
  • My curl POST to /api/orders is not working. What should I check?

Both endpoints work. The GET at /api/orders returns your welcome message, and the POST returns a hardcoded order confirmation. Spring Boot is routing HTTP requests to your controller and serializing Java objects to JSON automatically.

Your web layer is proven. Next, you will design the Order entity and connect it to MySQL so the API returns real data from a database instead of hardcoded values.

Design the Order Entity and Connect to MySQL

Your endpoints return JSON, but it is all fake data. Every POST returns the same hardcoded confirmation, and every GET returns the same welcome message. A real Xerox shop needs orders that actually persist in the database.

In this step, you will define what an order looks like as a Java class, map it to a MySQL table with JPA annotations, and create a repository that handles database operations. By the end, orders will live in MySQL instead of your imagination.

In this step, get ready to:
  • Create enum types and an Order entity class mapped to a MySQL table.
  • Create a repository interface that gives you database operations for free.
  • Wire the repository into the controller and test with real data.
Create the enum types and Order entity

A print order has several fields that only accept a fixed set of values. The print type is either black-and-white or color. The service type is one of a few preset options. The order status follows a defined lifecycle. Java enums are the perfect fit for these constrained fields.

  • In IntelliJ's Project tool window on the left, right-click the com.smartqueue package inside src/main/java.
  • Select New, then Package.
  • Type model and press Enter to create the com.smartqueue.model package.
  • Right-click the new model package, select New, then Java Class.
  • Choose Enum from the kind dropdown, name it PrintType, and press Enter.
  • Replace the file contents with this code:
package com.smartqueue.model;

public enum PrintType {
    BLACK_AND_WHITE,
    COLOR
}
  • Create a second enum in the same model package. Right-click model, select New > Java Class > Enum, and name it ServiceType:
package com.smartqueue.model;

public enum ServiceType {
    PRINT_ONLY,
    PRINT_AND_BINDING,
    LAMINATION,
    PROJECT_REPORT
}
  • Create one more enum in the model package. Name it OrderStatus:
package com.smartqueue.model;

public enum OrderStatus {
    RECEIVED,
    IN_QUEUE,
    PRINTING,
    READY_FOR_COLLECTION,
    COLLECTED
}

What do these enums represent?

Each enum maps to a real concept in the Xerox shop. PrintType tracks whether the job is black-and-white or color. ServiceType captures what kind of service the customer wants (plain print, binding, lamination, or a full project report).

OrderStatus represents the lifecycle of a print order from RECEIVED all the way through to COLLECTED. Using enums instead of raw strings prevents typos and makes the allowed values explicit.

  • Check the Project tool window. You should see all three enum files listed under com.smartqueue.model.

Now you need the main data class that uses these enums. The Order class will represent a single print order and map directly to a row in the MySQL database.

  • Right-click the model package, select New > Java Class, and name it Order. Leave the kind as Class this time.
  • Replace the file contents with this code:
package com.smartqueue.model;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "orders") // Avoids SQL reserved word "order"
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String customerName;
    private String mobileNumber;
    private Integer pages;

    @Enumerated(EnumType.STRING) // Store as string, not ordinal
    private PrintType printType;

    @Enumerated(EnumType.STRING)
    private ServiceType serviceType;

    @Enumerated(EnumType.STRING)
    private OrderStatus status = OrderStatus.RECEIVED;

    private String notes;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

What do these annotations do?

@Entity tells Hibernate (the JPA implementation) that this class maps to a database table. @Table(name = "orders") sets the table name explicitly because order is a reserved word in SQL.

@Id marks the primary key. @GeneratedValue(strategy = GenerationType.IDENTITY) tells MySQL to auto-increment the id for each new row.

@Enumerated(EnumType.STRING) stores enum values as their name (COLOR, RECEIVED) instead of a number. This makes the database readable and safe from enum reordering bugs.

  • Save Order.java.
  • Restart your application by clicking the green Run button on SmartqueueApplication.java (or stop and re-run it).

Check the console output in IntelliJ. You should see a line starting with Hibernate: create table orders followed by the column definitions. This confirms JPA read your entity annotations and created the table in MySQL automatically.

Don't see the Hibernate SQL?

  • Make sure spring.jpa.show-sql=true is in your application.properties file.
  • If you see a connection error, double-check that MySQL is running and that the spring.datasource.password in application.properties matches your actual MySQL root password.
  • If you see an SSL or public key retrieval error, append ?sslMode=DISABLED&allowPublicKeyRetrieval=true to the end of your spring.datasource.url value.

Help me debug my Spring Boot MySQL connection error.

The table exists, but the entity is still missing a few things. It needs lifecycle callbacks to set timestamps automatically and getters/setters so Spring can read and write the fields.

  • In Order.java, find the closing } brace of the class. Add the following code just above it:
    // Automatically set timestamps when a new order is saved
    @PrePersist
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    // Automatically update the timestamp on changes
    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    // Required no-arg constructor for JPA
    public Order() {}

What are lifecycle callbacks?

@PrePersist runs automatically right before JPA inserts a new row. @PreUpdate runs right before JPA updates an existing row. Together, they keep createdAt and updatedAt accurate without you ever setting them manually.

The empty public Order() {} constructor is required by JPA. Hibernate uses it behind the scenes to create Order objects when reading rows from the database.

  • Add the getters and setters below the constructor, still above the closing } of the class:
    // Getters and setters for all fields
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getCustomerName() { return customerName; }
    public void setCustomerName(String customerName) { this.customerName = customerName; }
    public String getMobileNumber() { return mobileNumber; }
    public void setMobileNumber(String mobileNumber) { this.mobileNumber = mobileNumber; }
    public Integer getPages() { return pages; }
    public void setPages(Integer pages) { this.pages = pages; }
    public PrintType getPrintType() { return printType; }
    public void setPrintType(PrintType printType) { this.printType = printType; }
    public ServiceType getServiceType() { return serviceType; }
    public void setServiceType(ServiceType serviceType) { this.serviceType = serviceType; }
    public OrderStatus getStatus() { return status; }
    public void setStatus(OrderStatus status) { this.status = status; }
    public String getNotes() { return notes; }
    public void setNotes(String notes) { this.notes = notes; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
    public LocalDateTime getUpdatedAt() { return updatedAt; }
    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }

Why do you need getters and setters?

Spring uses the Jackson library to convert between JSON and Java objects. When a POST request arrives with JSON, Jackson calls setter methods like setCustomerName() to populate the Order fields. When Spring sends a response, Jackson calls getter methods to build the JSON output.

  • Save Order.java.
  • Check that IntelliJ shows no red underlines in the file. If you see errors, make sure the lifecycle callbacks and getters/setters are inside the class braces.

✔️ Awesome, I've got everything!

Great. Make sure you have saved Order.java and that IntelliJ shows no red error indicators.

ⓧ I'd like to double check the full code

Scroll up and confirm your Order.java contains all three sections in this order:

  • The package, imports, @Entity annotation, and field declarations (from id through updatedAt).
  • The @PrePersist and @PreUpdate lifecycle callbacks plus the public Order() {} constructor.
  • All the getter and setter methods from getId() through setUpdatedAt().
Create the OrderRepository and wire it into the controller

You have the entity, but no way to talk to the database from your Java code. Spring Data JPA solves this. You create an interface, and Spring generates the implementation for you automatically.

  • In the Project tool window, right-click the com.smartqueue package.
  • Select New > Package, type repository, and press Enter.
  • Right-click the repository package, select New > Java Class, choose Interface, and name it OrderRepository.
  • Replace the file contents with this code:
package com.smartqueue.repository;

import com.smartqueue.model.Order;
import org.springframework.data.jpa.repository.JpaRepository;

// Spring generates save(), findAll(), findById(), delete() for you
public interface OrderRepository extends JpaRepository<Order, Long> {
}

What does JpaRepository give you?

By extending JpaRepository<Order, Long>, you inherit a full set of database operations without writing any SQL. save() inserts or updates a row. findAll() returns every row. findById() looks up one row by its primary key. delete() removes a row.

The Long type parameter tells Spring that the Order entity's primary key is a Long. Spring uses this to generate the correct SQL queries at runtime.

  • Save OrderRepository.java.

Now you need to update OrderController.java to use the repository instead of returning hardcoded data.

  • Open OrderController.java in the controller package.
  • Replace the entire file contents with this updated controller:
package com.smartqueue.controller;

import com.smartqueue.model.Order;
import com.smartqueue.repository.OrderRepository;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    // Injected by Spring automatically
    private final OrderRepository orderRepository;

    // Constructor injection
    public OrderController(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @PostMapping
    public Order createOrder(@RequestBody Order order) {
        return orderRepository.save(order);
    }

    @GetMapping
    public List<Order> getAllOrders() {
        return orderRepository.findAll();
    }
}

What changed in the controller?

The controller now receives an OrderRepository through its constructor. Spring sees the constructor parameter and automatically provides the repository instance. This pattern is called dependency injection.

The POST method now calls orderRepository.save(order) to insert the order into MySQL and return the saved object (complete with a generated id). The GET method calls orderRepository.findAll() to return every order from the database.

  • Save OrderController.java.
Test with real data in MySQL
  • Restart your application from IntelliJ by clicking the green Run button on SmartqueueApplication.java.
  • Wait for the console to show Tomcat started on port 8080.

Before you test, predict what will happen. When you POST an order with a customer name and page count, do you think the response will include an id field? Why or why not?

  • Open a new terminal and submit a real print order by running this command:
curl -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d "{\"customerName\": \"Rahul\", \"mobileNumber\": \"9876543210\", \"pages\": 25, \"printType\": \"COLOR\", \"serviceType\": \"PRINT_AND_BINDING\"}"

You should see a JSON response that includes a generated id field, a status of RECEIVED, and createdAt/updatedAt timestamps filled in automatically. The id proves MySQL generated a primary key for the new row.

  • Retrieve all orders from the database by running:
curl http://localhost:8080/api/orders

You should see a JSON array containing the order you just created. This data is coming from MySQL, not from hardcoded values.

  • Verify the data is actually in MySQL by opening a MySQL terminal and running:
mysql -u root -p
  • Enter your MySQL password when prompted, then run:
USE smartqueue;
SELECT * FROM orders;

You should see a table row with Rahul's order, confirming the data lives in MySQL.

Seeing an error when you POST?

  • If you see a 415 Unsupported Media Type error, make sure your curl command includes -H "Content-Type: application/json".
  • If you see a 500 Internal Server Error with a database message, check that your application.properties password matches your MySQL root password.
  • If the JSON fields are missing or all null, make sure Order.java has getters and setters for every field. Jackson needs those methods to populate the object.

Help me fix the error I'm getting when POSTing an order to my Spring Boot API.

The API works. Orders persist in MySQL, and you can retrieve them. But look at OrderController.java. It handles HTTP requests, calls the repository directly, and would be the place you add any business logic. It is doing three jobs in one class.

Why is this a problem?

Right now the controller is tightly coupled to the database layer. If you wanted to add a rule like "orders cannot skip statuses" or "maximum 500 pages per order," that logic would go right next to the HTTP routing code. As the app grows, this single class becomes a tangled mess.

The fix is a service layer that sits between the controller and the repository. The controller handles HTTP. The service handles business logic. The repository handles data. You will build that separation in the next step.

Your orders are real now. They live in MySQL and survive restarts. Next up, you will fix the tight coupling by introducing a service layer and adding the remaining CRUD endpoints.

Introduce the Service Layer with Dependency Injection

You have a working API that creates and retrieves orders from MySQL. But look at OrderController.java. It handles HTTP requests AND talks to the database through OrderRepository directly. If you needed to add a business rule later (like preventing an order from jumping from RECEIVED straight to COLLECTED), that logic would get tangled with HTTP handling code.

The fix is a service layer. You will create an OrderService class that owns all the business logic, sitting between the controller and the repository. Spring Boot's dependency injection wires these layers together automatically through constructor injection.

In this step, get ready to:
  • Create an OrderService class to handle all business logic.
  • Refactor OrderController to delegate to the service layer.
  • Test all five CRUD endpoints.
Create the OrderService class

A well-structured Spring Boot application separates concerns into three layers. The controller handles HTTP concerns (status codes, request and response bodies). The service handles business logic (setting default statuses, looking up orders). The repository handles database access.

What is dependency injection?

Instead of a class creating the objects it needs (like new OrderRepository()), Spring creates those objects and passes them in through the constructor. This is dependency injection.

The benefit is loose coupling. Your controller does not know or care how the service is built. Spring's IoC container manages object creation and wiring, so you can swap implementations without rewriting the classes that depend on them.

Before creating the service, you need a custom exception class. The service will throw this when an order is not found by ID.

  • In IntelliJ's Project panel on the left, right-click the com.smartqueue package.
  • Select New, then Package.
  • Name the package exception and press Enter.
  • Right-click the new exception package, select New, then Java Class.
  • Name it OrderNotFoundException and press Enter.
  • Replace the generated contents with the following code:
package com.smartqueue.exception;

// Custom exception thrown when an order ID does not exist
public class OrderNotFoundException extends RuntimeException {

    public OrderNotFoundException(Long id) {
        // Build a descriptive message with the missing ID
        super("Order not found with id: " + id);
    }
}

What does this code do?

OrderNotFoundException extends RuntimeException, which means it is an unchecked exception. Java does not force callers to catch it. The constructor takes the missing order's ID and builds a readable error message like "Order not found with id: 42".

  • Save the file.

Now create the service class itself. This is where all the business logic will live.

  • Right-click the com.smartqueue package, select New, then Package.
  • Name the package service and press Enter.
  • Right-click the new service package, select New, then Java Class.
  • Name it OrderService and press Enter.
  • Replace the generated contents with the following code:
package com.smartqueue.service;

import com.smartqueue.exception.OrderNotFoundException;
import com.smartqueue.model.Order;
import com.smartqueue.model.OrderStatus;
import com.smartqueue.repository.OrderRepository;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class OrderService {

    private final OrderRepository orderRepository;

    // Spring auto-injects OrderRepository (single-constructor injection)
    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    // Set default status to RECEIVED and save the order
    public Order createOrder(Order order) {
        order.setStatus(OrderStatus.RECEIVED);
        return orderRepository.save(order);
    }

    // Retrieve every order from the database
    public List<Order> getAllOrders() {
        return orderRepository.findAll();
    }
}

What does this code do?

@Service tells Spring this class is a service bean. Spring will create an instance of it automatically and manage its lifecycle.

The constructor takes OrderRepository as a parameter. When a Spring bean has only one constructor, Spring automatically uses it for dependency injection. No @Autowired annotation is needed. The final keyword ensures the field cannot be reassigned after construction.

createOrder explicitly sets the status to RECEIVED before saving. This is business logic that belongs in the service, not the controller.

  • Save OrderService.java.
  • Restart the application by clicking the green Run button in IntelliJ (it automatically stops the previous run).

The console should show Tomcat started on port 8080 with no errors. This confirms Spring detected and instantiated your new @Service bean.

Seeing a compilation error?

  • Check that the OrderNotFoundException class is in the com.smartqueue.exception package. The import in OrderService must match the package path exactly.
  • Verify that OrderService.java is in the com.smartqueue.service package. Spring only scans packages under com.smartqueue.
  • Why won't my service class compile?
  • In OrderService.java, add these three methods below getAllOrders, still inside the class:
    // Look up a single order, throw exception if not found
    public Order getOrderById(Long id) {
        return orderRepository.findById(id)
                .orElseThrow(() -> new OrderNotFoundException(id));
    }

    // Find the order, update its status, and save
    public Order updateOrderStatus(Long id, OrderStatus status) {
        Order order = getOrderById(id);
        order.setStatus(status);
        return orderRepository.save(order);
    }

    // Find the order and remove it from the database
    public void deleteOrder(Long id) {
        Order order = getOrderById(id);
        orderRepository.delete(order);
    }

What do these methods do?

  • getOrderById uses findById which returns an Optional. The .orElseThrow() unwraps the value if present, or throws OrderNotFoundException if the order does not exist.
  • updateOrderStatus and deleteOrder both call getOrderById first. If the order does not exist, the exception is thrown before any update or delete happens.
  • Save OrderService.java.
  • Restart the application to confirm the complete service compiles without errors.

The app should start normally. Your service layer is complete.

✔️ Awesome, I've got everything!

Great. Make sure you have saved OrderService.java before moving on.

ⓧ I'd like to double check the full code

Your OrderService.java file should contain these sections in order. Scroll up and confirm each one is present:

  • Package declaration com.smartqueue.service and six import statements.
  • @Service annotation, class declaration, orderRepository field, and constructor.
  • createOrder method (sets status, saves) and getAllOrders method (returns all orders).
  • getOrderById (with orElseThrow), updateOrderStatus, and deleteOrder methods.
Refactor the controller to use OrderService

The controller currently injects OrderRepository and calls it directly. You will replace it so it injects OrderService instead, and upgrade every endpoint to return proper HTTP status codes using ResponseEntity.

  • Open OrderController.java and replace its entire contents with the following code:
package com.smartqueue.controller;

import com.smartqueue.model.Order;
import com.smartqueue.model.OrderStatus;
import com.smartqueue.service.OrderService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    // Now depends on OrderService instead of OrderRepository
    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
}

What changed here?

The field changed from OrderRepository orderRepository to OrderService orderService. The controller no longer knows the repository exists. It only talks to the service layer.

ResponseEntity is a Spring class that wraps your response body together with an HTTP status code. Instead of returning a plain Order (which defaults to 200 OK), you can now return specific codes like 201 Created or 204 No Content.

  • Add the POST and GET endpoint methods inside the class, below the constructor:
    // Create a new order, delegate to service, return 201 CREATED
    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestBody Order order) {
        Order createdOrder = orderService.createOrder(order);
        return new ResponseEntity<>(createdOrder, HttpStatus.CREATED);
    }

    // Retrieve all orders
    @GetMapping
    public ResponseEntity<List<Order>> getAllOrders() {
        List<Order> orders = orderService.getAllOrders();
        return ResponseEntity.ok(orders);
    }
  • Save OrderController.java and restart the application.
  • Test the POST endpoint by running this command in a new terminal:
curl -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d "{\"customerName\": \"Priya\", \"mobileNumber\": \"9876543210\", \"pages\": 15, \"printType\": \"COLOR\", \"serviceType\": \"PRINT_AND_BINDING\"}"

You should see a JSON response with the order data and a generated id field. Note the ID number in the response. You will use it in the next few tests.

  • Verify the GET endpoint still works by opening http://localhost:8080/api/orders in your browser.

You should see a JSON array containing the order you just created. The controller is now wired through the service layer.

Getting a 500 error or no response?

  • Check the IntelliJ console for a stack trace. If you see a bean-creation error, make sure OrderService.java has the @Service annotation and is in the com.smartqueue.service package.
  • Confirm the controller constructor takes OrderService (not OrderRepository). Both must match: the field type and the constructor parameter type.
  • Why is my controller returning a 500 error after refactoring to use a service layer?

Now add the three remaining endpoints. These are new capabilities that did not exist in the old controller.

  • In OrderController.java, add these three endpoint methods below getAllOrders, still inside the class:
    // Retrieve a single order by its database ID
    @GetMapping("/{id}")
    public ResponseEntity<Order> getOrderById(@PathVariable Long id) {
        Order order = orderService.getOrderById(id);
        return ResponseEntity.ok(order);
    }

    // Update an order's status (e.g., RECEIVED to PRINTING)
    @PutMapping("/{id}/status")
    public ResponseEntity<Order> updateOrderStatus(@PathVariable Long id, @RequestBody Map<String, String> statusUpdate) {
        OrderStatus status = OrderStatus.valueOf(statusUpdate.get("status"));
        Order updatedOrder = orderService.updateOrderStatus(id, status);
        return ResponseEntity.ok(updatedOrder);
    }

    // Delete an order and return 204 NO CONTENT
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
        orderService.deleteOrder(id);
        return ResponseEntity.noContent().build();
    }

What do these endpoints do?

  • @PathVariable extracts the {id} from the URL path. A request to /api/orders/3 passes 3 as the id parameter.
  • The PUT endpoint reads the new status from a JSON body like {"status": "PRINTING"} and converts it to an OrderStatus enum value using valueOf.
  • The DELETE endpoint returns 204 No Content via ResponseEntity.noContent().build(). A 204 means the action succeeded but there is no body to return.
  • Save OrderController.java.

✔️ Awesome, I've got everything!

Great. Confirm you saved OrderController.java before moving on.

ⓧ I'd like to double check the full code

Your OrderController.java file should contain these sections in order. Scroll up and confirm each one is present:

  • Package declaration com.smartqueue.controller and nine import statements (including OrderStatus, OrderService, ResponseEntity, and Map).
  • @RestController and @RequestMapping("/api/orders") annotations, class declaration, orderService field, and constructor.
  • createOrder (POST, returns 201) and getAllOrders (GET, returns 200).
  • getOrderById (GET by ID), updateOrderStatus (PUT), and deleteOrder (DELETE, returns 204).
Test all five CRUD endpoints

You now have five endpoints powered by three clean layers: controller, service, repository. Time to prove they all work.

  • Restart the application.
  • Create a new test order by running this curl command:
curl -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d "{\"customerName\": \"Rahul\", \"mobileNumber\": \"9876543210\", \"pages\": 25, \"printType\": \"COLOR\", \"serviceType\": \"PRINT_AND_BINDING\"}"

Note the id value in the response. You will use it in the next commands. The examples below use ID 1. Replace it with whatever ID your response shows.

  • Retrieve all orders:
curl http://localhost:8080/api/orders

You should see a JSON array containing the order you just created.

  • Retrieve a single order by ID:
curl http://localhost:8080/api/orders/1

You should see the same order, returned as a single JSON object instead of an array.

  • Update the order's status to PRINTING:
curl -X PUT http://localhost:8080/api/orders/1/status -H "Content-Type: application/json" -d "{\"status\": \"PRINTING\"}"

The response should show the same order with "status": "PRINTING".

  • Delete the order:
curl -X DELETE http://localhost:8080/api/orders/1

No response body is returned. The 204 No Content status confirms the delete succeeded.

  • Confirm the order is gone by running curl http://localhost:8080/api/orders again. The deleted order should no longer appear.

All five CRUD endpoints work. The code compiles cleanly with three separate layers: OrderController calls OrderService calls OrderRepository.

The happy path works. But what happens when things go wrong? Before you run the next command, predict: what do you think the API will return if you request an order ID that does not exist?

  • Try requesting a nonexistent order:
curl http://localhost:8080/api/orders/999

You get a 500 Internal Server Error. Check the IntelliJ console. You will see a long stack trace with OrderNotFoundException. The exception is being thrown, but nothing catches it and converts it into a clean 404 response.

Now predict: what happens if you POST an order with a blank customer name and zero pages?

  • Try sending obviously invalid data:
curl -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d "{\"customerName\": \"\", \"pages\": 0}"

It saves without complaint. The API accepted a blank name and zero pages as a valid order. That is a problem.

Why are these problems?

A production API should return a clean 404 Not Found for missing resources, not a 500 error with a stack trace that exposes internal class names. It should also reject invalid input with a clear 400 Bad Request instead of silently saving garbage data.

Both of these gaps are exactly what the next step will fix.

Your architecture is clean. Three separate layers, each with a single responsibility, wired together by dependency injection. But the API still accepts invalid data and crashes with 500 errors when an order is not found. Next up, you will add validation and exception handling to close both gaps.

Add Validation and Exception Handling

Your SmartQueue API now has a clean three-layer architecture with all five CRUD endpoints working. But there are two gaps hiding in plain sight.

Try POSTing an order with a blank customer name or zero pages. The API saves it without complaint. Try requesting an order ID that does not exist. You get a 500 Internal Server Error. A production API needs to reject bad input with clear 400 errors and return proper 404 responses for missing resources. In this step, you will close both gaps.

In this step, get ready to:
  • Add validation annotations to reject invalid input with 400 errors.
  • Create a global exception handler for clean 404 responses.
  • Test both error scenarios to confirm graceful handling.
Add validation annotations to the Order entity

Right now, any JSON body gets accepted by the POST endpoint. Spring's Bean Validation framework lets you declare rules directly on entity fields using annotations like @NotBlank and @Min. When combined with @Valid on the controller method, Spring automatically rejects invalid requests before your code even runs.

  • In IntelliJ, open Order.java from the model package.
  • Find the import section at the top and add these three imports below import jakarta.persistence.*;:
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
  • Now find the five field declarations below private Long id; and replace them with these annotated versions:
// Customer name cannot be blank
@NotBlank(message = "Customer name is required")
private String customerName;

// Mobile number cannot be blank
@NotBlank(message = "Mobile number is required")
private String mobileNumber;

// Must have at least 1 page
@NotNull(message = "Number of pages is required")
@Min(value = 1, message = "Pages must be at least 1")
private Integer pages;

// Print type is mandatory
@NotNull(message = "Print type is required")
@Enumerated(EnumType.STRING)
private PrintType printType;

// Service type is mandatory
@NotNull(message = "Service type is required")
@Enumerated(EnumType.STRING)
private ServiceType serviceType;

What do these annotations do?

@NotBlank rejects null values AND empty strings. It is designed for String fields like customerName.

@NotNull rejects null values but allows empty strings. It is used for non-String fields like enums and numbers.

@Min(value = 1) ensures the number is at least 1. A print order with zero pages makes no sense.

The message attribute on each annotation defines the error text the API returns when validation fails.

  • Save Order.java.

The annotations are in place, but they will not do anything on their own. You need to tell the controller to actually check them.

  • Open OrderController.java from the controller package.
  • Find the createOrder method signature. It currently looks like this:
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
  • Add @Valid before @RequestBody so it looks like this:
public ResponseEntity<Order> createOrder(@Valid @RequestBody Order order) {

What does @Valid do?

@Valid tells Spring to run the validation annotations on the incoming Order object before the method body executes. If any field fails validation, Spring rejects the request with a 400 Bad Request immediately. Your createOrder code never runs.

  • Save OrderController.java.
  • Restart the application by clicking the green Run button in IntelliJ.

Before you test, think about what should happen when you POST an order with a blank customer name and zero pages. Do you think the API will accept it or reject it?

  • Test validation by sending an invalid order with this command:
curl -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d "{\"customerName\": \"\", \"pages\": 0}"

You should see a 400 Bad Request response. Spring rejected the invalid data before it reached your service layer.

Still seeing 200 OK or the order saving?

  • Make sure you added @Valid before @RequestBody in the createOrder method. Without it, Spring skips validation entirely.
  • Check that you restarted the application after saving your changes.
  • Verify the annotations are on the correct fields in Order.java. The @NotBlank annotation belongs on customerName and mobileNumber, not on the other fields.

Why is my @Valid annotation not triggering validation?

Create the custom exception and global handler

Validation handles bad input, but what about requests for orders that do not exist? The OrderService already throws an OrderNotFoundException, but Spring does not know how to handle it gracefully. It wraps the exception in a 500 Internal Server Error.

A @ControllerAdvice class acts as a global interceptor. It catches specific exceptions thrown by any controller and converts them into proper HTTP responses.

  • In the exception package, create a new Java class called OrderNotFoundException.java and paste the following code:
package com.smartqueue.exception;

// Custom exception for when an order ID is not found in the database
public class OrderNotFoundException extends RuntimeException {

    public OrderNotFoundException(Long id) {
        super("Order not found with id: " + id);
    }
}

What does this exception do?

OrderNotFoundException extends RuntimeException, which means it is an unchecked exception. The constructor takes the missing order's id and builds a human-readable message like Order not found with id: 999. Your OrderService already throws this in the getOrderById method.

  • Save OrderNotFoundException.java.

The exception class defines the error, but nothing catches it yet. Next, you will create the global handler that intercepts this exception and returns a structured JSON response.

  • In the same exception package, create a new Java class called GlobalExceptionHandler.java and paste the following code:
package com.smartqueue.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

// Intercepts exceptions across all controllers
@ControllerAdvice
public class GlobalExceptionHandler {

    // Return 404 when an order is not found
    @ExceptionHandler(OrderNotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleOrderNotFound(OrderNotFoundException ex) {
        Map<String, Object> error = new HashMap<>();
        error.put("timestamp", LocalDateTime.now().toString());
        error.put("status", 404);
        error.put("error", "Not Found");
        error.put("message", ex.getMessage());
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
}

What does @ControllerAdvice do?

@ControllerAdvice marks this class as a global exception handler for every controller in your application. You do not need to register it anywhere. Spring discovers it automatically through component scanning.

The @ExceptionHandler(OrderNotFoundException.class) method catches that specific exception wherever it is thrown and builds a JSON response with the timestamp, status code, and error message.

  • Save GlobalExceptionHandler.java.
  • Restart the application.

Before you test, predict what will happen when you request an order with ID 999. Will you still see a 500 error, or something different?

  • Test the 404 handling by requesting a nonexistent order:
curl http://localhost:8080/api/orders/999

You should see a JSON response with status 404 and the message Order not found with id: 999. No more 500 errors for missing orders.

Still seeing a 500 error?

  • Confirm that GlobalExceptionHandler.java is in the com.smartqueue.exception package. Spring's component scan starts from com.smartqueue, so any sub-package is automatically discovered.
  • Make sure the @ControllerAdvice annotation is on the class, not just @ExceptionHandler on the method.
  • Verify you restarted the application after saving both files.

Why is my @ControllerAdvice not catching the exception?

The 404 handler works. Now you will add a second handler method that formats validation errors into a structured response with specific field-level messages.

  • Return to GlobalExceptionHandler.java.
  • Add this import at the top of the file, below the existing imports:
import org.springframework.web.bind.MethodArgumentNotValidException;
  • Add this second handler method inside the class, below the handleOrderNotFound method:
// Return 400 with per-field error messages when validation fails
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationErrors(MethodArgumentNotValidException ex) {
    Map<String, Object> error = new HashMap<>();
    Map<String, String> fieldErrors = new HashMap<>();
    // Extract each field's error message from the binding result
    ex.getBindingResult().getFieldErrors().forEach(fieldError ->
            fieldErrors.put(fieldError.getField(), fieldError.getDefaultMessage())
    );
    error.put("timestamp", LocalDateTime.now().toString());
    error.put("status", 400);
    error.put("error", "Validation Failed");
    error.put("fieldErrors", fieldErrors);
    return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

What does this handler do?

When @Valid rejects a request, Spring throws a MethodArgumentNotValidException. This handler catches it and builds a response that lists every invalid field with its specific error message.

The getBindingResult().getFieldErrors() call extracts each field that failed and the message you defined on the annotation. The result is a fieldErrors map like {"customerName": "Customer name is required"}.

  • Save GlobalExceptionHandler.java.
Test validation and error handling

Both safety nets are in place. Time to run the full verification and confirm that the API handles every error scenario gracefully.

  • Restart the application.

Think about what you expect the response to look like when you POST an order with a blank customer name and zero pages. How many field errors should appear?

  • Test the validation error response by sending an invalid order:
curl -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d "{\"customerName\": \"\", \"pages\": 0}"

You should see a 400 response with a fieldErrors object listing each invalid field and its error message. The blank customerName, missing mobileNumber, zero pages, and missing enum fields all show up with specific messages.

  • Now test the 404 response by requesting a nonexistent order:
curl http://localhost:8080/api/orders/999

You should see a 404 response with the message Order not found with id: 999. No more 500 errors. No more stack traces. The API handles errors gracefully.

Not seeing the fieldErrors in the 400 response?

  • Make sure the handleValidationErrors method is inside the GlobalExceptionHandler class, not outside the closing brace.
  • Verify you added the import org.springframework.web.bind.MethodArgumentNotValidException; line at the top of the file.
  • Confirm the method has the @ExceptionHandler(MethodArgumentNotValidException.class) annotation, not just @ExceptionHandler without the class parameter.

Why is my validation handler not returning field-level error messages?

✔️ Awesome, I've got everything!

Great. Make sure you have saved all four files: Order.java, OrderController.java, OrderNotFoundException.java, and GlobalExceptionHandler.java.

ⓧ I'd like to double check the full code

Here is the full OrderNotFoundException.java:

package com.smartqueue.exception;

public class OrderNotFoundException extends RuntimeException {

    public OrderNotFoundException(Long id) {
        super("Order not found with id: " + id);
    }
}

Here is the full GlobalExceptionHandler.java:

package com.smartqueue.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(OrderNotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleOrderNotFound(OrderNotFoundException ex) {
        Map<String, Object> error = new HashMap<>();
        error.put("timestamp", LocalDateTime.now().toString());
        error.put("status", 404);
        error.put("error", "Not Found");
        error.put("message", ex.getMessage());
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidationErrors(MethodArgumentNotValidException ex) {
        Map<String, Object> error = new HashMap<>();
        Map<String, String> fieldErrors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(fieldError ->
                fieldErrors.put(fieldError.getField(), fieldError.getDefaultMessage())
        );
        error.put("timestamp", LocalDateTime.now().toString());
        error.put("status", 400);
        error.put("error", "Validation Failed");
        error.put("fieldErrors", fieldErrors);
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
}

For Order.java and OrderController.java, scroll up and confirm your changes match the code blocks you added earlier. Order.java should have three new validation imports and five annotated fields. OrderController.java should have @Valid before @RequestBody in the createOrder method.

Your API now handles both invalid input and missing resources gracefully. Every error returns a structured JSON response with the right HTTP status code. That is exactly what a production API should do.

Secret mission

Filter Orders by Status

Your API returns every single order when a shop owner calls GET /api/orders. A busy shop might have hundreds of orders in different stages. Can you add a status filter so the owner can pull up just the orders that are currently printing, or just the ones ready for collection?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The smartqueue MySQL database on localhost:3306.
  • The smartqueue Spring Boot project folder.
  • The running Spring Boot application on port 8080.

Keep everything running

No action needed. Choose this if you are still actively building or want to continue experimenting with your API.

  • Your Spring Boot application continues running in IntelliJ on port 8080.
  • Your smartqueue MySQL database stays on your local machine with all saved orders.
  • Your project folder stays wherever you extracted it.

Pause - I'll come back to this later

Shut down the running application to free up port 8080. Your project files and database stay on your machine for next time.

  • Stop the Spring Boot application by clicking the red square Stop button in IntelliJ's Run panel.
  • If you ran the app from a terminal instead, press Ctrl+C in that terminal to stop the process.
  • Your smartqueue database and project folder remain untouched. To resume, open the project in IntelliJ and click the green Run button again.

Delete - I don't want to use this again

Remove the project files and database completely from your machine.

  • Stop the Spring Boot application by clicking the red square Stop button in IntelliJ's Run panel, or pressing Ctrl+C if running from a terminal.
  • Drop the MySQL database by opening a terminal and running the following commands:
mysql -u root -p

Once you are in the MySQL prompt, run:

DROP DATABASE smartqueue;
EXIT;
  • Delete the smartqueue project folder. In File Explorer (Windows), navigate to where you extracted the ZIP, right-click the smartqueue folder, and select Delete. On macOS, find it in Finder and move it to the Trash.
  • Close the project in IntelliJ by clicking File, then Close Project.

Nice Work!

Nice Work!

Nice work! You just built SmartQueue, a fully functional REST API that manages a Xerox shop's print order queue from start to finish. You can create orders, track them through their lifecycle, and handle errors gracefully.

You've learned how to:

  • Built a Spring Boot REST API with full CRUD operations (create, read, update status, delete) backed by a MySQL database with JPA entity mapping and enum types.
  • Structured your application in three clean layers (Controller > Service > Repository) using dependency injection and constructor injection to wire them together.
  • Added production-quality error handling with bean validation annotations that return structured 400 errors for invalid input, and a @ControllerAdvice global exception handler that returns clean 404 responses for missing orders.
  • Secret Mission: Added order filtering by status using JPA derived query methods and optional query parameters.

Ready to quiz yourself?