Cinema Seat Booking App
Build a full-stack cinema booking app with Next.js and Spring Boot.
Introduction
30 Second Summary
Think about the last time you booked cinema tickets online. You picked your movie, chose your seats from a colour-coded grid, and watched the total update as you tapped each one. That entire experience is a full-stack application.
In this project, you will build a cinema seat booking app from scratch. You will create a Spring Boot backend that manages seats and bookings, and a Next.js frontend that shows real movies currently in theatres and lets users pick seats with live pricing.
What You'll Build
A full-stack cinema booking application where the frontend fetches real movie data from the TMDB API and communicates with a Java REST API backend to handle seat selection and booking confirmation.
By the end of this project, you'll have:
- A Spring Boot REST API with endpoints for seat maps and booking submissions.
- A Next.js frontend that displays real movies from TMDB and an interactive cinema-style seat picker.
- Three seat pricing tiers (VIP, Standard, Economy) with dynamic price totals that update as you select seats.
- Secret Mission: Add a showtime selector that lets users pick different screening times, each with its own independent seat availability.
Are there any prerequisites?
This project assumes basic HTML, CSS, JavaScript, and React knowledge. You will also need Node.js (version 20.9 or higher), JDK 21+, and Maven installed on your machine.
Set Up Your Spring Boot Backend
Your cinema booking app needs a backend server to manage seats and process bookings. Spring Boot makes this straightforward by giving you a ready-to-run Java web server with minimal configuration.
In this step, you will generate a Spring Boot project, verify it runs, and configure it to accept requests from your frontend.
In this step, get ready to:
- Generate a Spring Boot project with Spring Initializr.
- Verify the app starts on localhost:8080.
- Configure CORS so your Next.js frontend can talk to the API.
Generate a Spring Boot project
Spring Initializr is a web tool that generates a ready-to-run Spring Boot project for you. You pick your build tool, Java version, and dependencies, and it gives you a zip file with everything wired up.
- Open your browser and navigate to start.spring.io.
- Under **Project**, select Maven.
- Under **Language**, confirm Java is selected.
- Under **Spring Boot**, select 3.5.0.
- Under **Project Metadata**, set Group to com.cinema.
- Set Artifact to seat-booking-api.
- Set Name to seat-booking-api.
- Set Description to Cinema Seat Booking API.
- Leave Packaging as Jar and Java version as 21.
- Click ADD DEPENDENCIES, search for Spring Web, and select it.
- Click GENERATE to download the zip file.
What is Spring Web?
The spring-boot-starter-web dependency bundles everything you need to build a REST API. It includes an embedded Tomcat server, Spring MVC for routing, and JSON serialization out of the box.
- Find the downloaded seat-booking-api.zip in your Downloads folder and extract it to your Desktop.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type IntelliJ and press Enter to open it.
- In IntelliJ IDEA, click Open on the welcome screen.
- Navigate to the seat-booking-api folder on your Desktop and click Open.
- If IntelliJ asks to trust the project, click Trust Project.
What did Spring Initializr generate?
Your project has a pom.xml that defines your dependencies, and a main application class at src/main/java/com/cinema/SeatBookingApiApplication.java.
The @SpringBootApplication annotation on the main class tells Spring Boot to auto-configure everything based on your dependencies. You also get an empty application.properties file for configuration.
Verify the project runs
Before writing any code, confirm that the generated project starts successfully. This proves your Java and Maven setup is working.
- Make the server port explicit by opening src/main/resources/application.properties in IntelliJ and adding this line:
server.port=8080
Why set the port explicitly?
Spring Boot defaults to port 8080, but adding it to application.properties makes the configuration visible and easy to change later.
- Open a terminal in IntelliJ by clicking Terminal in the bottom toolbar (or press Alt+F12).
- Start the application by running this command:
mvn spring-boot:run
- Wait for the startup to complete. You should see a log line that says Started SeatBookingApiApplication followed by a time in seconds.
What should I see?
The terminal displays the Spring Boot banner (a large ASCII art logo), followed by log lines. The last meaningful line contains Started SeatBookingApiApplication and confirms the embedded Tomcat server is listening on port 8080.
App not starting?
If you see a JAVA_HOME error, your JDK is not configured correctly. Run java -version in your terminal to check that JDK 21+ is accessible.
If port 8080 is already in use, another process is occupying it. Stop any other servers on that port and try again.
help me debug my Spring Boot app not starting
- Once you confirm the app started, press Ctrl+C in the terminal to stop the server.
Configure CORS for your frontend
Your Next.js frontend will run on localhost:3000, but your API runs on localhost:8080. Browsers block requests between different origins by default. CORS (Cross-Origin Resource Sharing) configuration tells your backend which origins are allowed to make requests.
- In IntelliJ, right-click the com.cinema package in the src/main/java directory.
- Select New, then Package.
- Type com.cinema.config and press Enter.
- Right-click the new config package, select New, then Java Class.
- Name it CorsConfig and press Enter.
- Replace the entire file contents with this code:
package com.cinema.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
// Allow the Next.js frontend to call any /api/ endpoint
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
};
}
}
What does this code do?
- @Configuration tells Spring Boot this class provides bean definitions.
- addMapping("/api/**") applies CORS rules to every endpoint under /api/.
- allowedOrigins("http://localhost:3000") permits requests from your Next.js dev server.
- allowedMethods specifies which HTTP verbs the frontend can use.
- Save CorsConfig.java.
- In the terminal, run the app again to confirm it still starts cleanly:
mvn spring-boot:run
- You should see Started SeatBookingApiApplication in the logs again with no errors.
- Press Ctrl+C to stop the server.
Seeing a compilation error?
Check that CorsConfig.java is inside the com.cinema.config package. The package com.cinema.config; line at the top of the file must match the folder structure exactly.
Also confirm all five import statements are present at the top of the file. Missing imports are the most common cause of red underlines in IntelliJ.
help me fix my CorsConfig compilation error
✔️ Awesome, I've got everything!
Great. Double check you have saved both CorsConfig.java and application.properties.
ⓧ I'd like to double check the full code
Here is the complete CorsConfig.java file:
package com.cinema.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
};
}
}
And here is application.properties:
server.port=8080
The pom.xml and SeatBookingApiApplication.java were generated by Spring Initializr and should not need changes.
Your backend is ready to go. Next up, you will build the seat and booking API endpoints that your frontend will call to display seats and process reservations.
Build the Seat and Booking API
Your Spring Boot app is running, and it can accept requests from your future frontend. Now it needs something to actually serve.
A cinema booking system needs two core capabilities: showing available seats and processing bookings. In this step, you'll build both as REST API endpoints backed by Java model classes and an in-memory data store.
In this step, get ready to:
- Create Java model classes to represent seats, booking requests, and booking responses.
- Build a seat service that generates a cinema-style seat layout with three pricing tiers.
- Wire up GET and POST endpoints so clients can fetch seats and submit bookings.
Create the model classes
Before any controller or service can do its job, you need plain Java objects that define the shape of your data. These are your model classes.
- In IntelliJ's Project pane on the left, right-click on the com.cinema package.
- Select New > Package.
- Type model and press Enter.
- Right-click on the new com.cinema.model package.
- Select New > Java Class.
- Type Seat and press Enter.
- Replace the generated content of Seat.java with the following code:
package com.cinema.model;
public class Seat {
private String id;
private String row;
private int number;
private String tier;
private double price;
private boolean booked;
// Constructor sets up a new seat with tier and price
public Seat(String id, String row, int number, String tier, double price) {
this.id = id;
this.row = row;
this.number = number;
this.tier = tier;
this.price = price;
this.booked = false;
}
// Getters and setters for each field
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getRow() { return row; }
public void setRow(String row) { this.row = row; }
public int getNumber() { return number; }
public void setNumber(int number) { this.number = number; }
public String getTier() { return tier; }
public void setTier(String tier) { this.tier = tier; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public boolean isBooked() { return booked; }
public void setBooked(boolean booked) { this.booked = booked; }
}
What does this code do?
The Seat class represents one seat in the cinema. Each seat knows its position (row and number), its pricing tier (VIP, Standard, or Economy), and whether someone has already booked it.
When Spring Boot converts these objects to JSON for the API response, it uses the getters to read each field. That is why every field needs a getter method.
✔️ Awesome, I've got everything!
Great. Make sure IntelliJ shows no red underlines in Seat.java.
ⓧ I'd like to double check the full code
package com.cinema.model;
public class Seat {
private String id;
private String row;
private int number;
private String tier;
private double price;
private boolean booked;
public Seat(String id, String row, int number, String tier, double price) {
this.id = id;
this.row = row;
this.number = number;
this.tier = tier;
this.price = price;
this.booked = false;
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getRow() { return row; }
public void setRow(String row) { this.row = row; }
public int getNumber() { return number; }
public void setNumber(int number) { this.number = number; }
public String getTier() { return tier; }
public void setTier(String tier) { this.tier = tier; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public boolean isBooked() { return booked; }
public void setBooked(boolean booked) { this.booked = booked; }
}
- Create another Java class in the com.cinema.model package by right-clicking the package and selecting New > Java Class.
- Name it BookingRequest and press Enter.
- Replace its contents with:
package com.cinema.model;
import java.util.List;
// Represents the data a client sends when making a booking
public class BookingRequest {
private String screeningId;
private List<String> seatIds;
public String getScreeningId() { return screeningId; }
public void setScreeningId(String screeningId) { this.screeningId = screeningId; }
public List<String> getSeatIds() { return seatIds; }
public void setSeatIds(List<String> seatIds) { this.seatIds = seatIds; }
}
What does this code do?
The BookingRequest class defines what the frontend sends when a user confirms a booking. It carries the screening ID (which movie showing) and the list of seat IDs the user selected.
Spring Boot automatically converts incoming JSON into this object using the setter methods. This pattern is called deserialization.
✔️ Awesome, I've got everything!
Great. Confirm there are no red underlines in BookingRequest.java.
ⓧ I'd like to double check the full code
package com.cinema.model;
import java.util.List;
public class BookingRequest {
private String screeningId;
private List<String> seatIds;
public String getScreeningId() { return screeningId; }
public void setScreeningId(String screeningId) { this.screeningId = screeningId; }
public List<String> getSeatIds() { return seatIds; }
public void setSeatIds(List<String> seatIds) { this.seatIds = seatIds; }
}
- Create one more Java class in com.cinema.model named BookingResponse.
- Replace its contents with:
package com.cinema.model;
import java.util.List;
// Represents the confirmation data sent back after a successful booking
public class BookingResponse {
private String bookingId;
private String screeningId;
private List<String> seatIds;
private double totalPrice;
public BookingResponse(String bookingId, String screeningId, List<String> seatIds, double totalPrice) {
this.bookingId = bookingId;
this.screeningId = screeningId;
this.seatIds = seatIds;
this.totalPrice = totalPrice;
}
// Getters only - responses are read-only
public String getBookingId() { return bookingId; }
public String getScreeningId() { return screeningId; }
public List<String> getSeatIds() { return seatIds; }
public double getTotalPrice() { return totalPrice; }
}
What does this code do?
The BookingResponse is what the API sends back after a successful booking. It includes a generated booking ID, the seats that were booked, and the calculated total price.
Notice it only has getters (no setters). Responses are read-only objects that the server creates and sends outward.
- In IntelliJ's Project pane, expand com.cinema.model. You should see Seat, BookingRequest, and BookingResponse listed with no red error indicators.
Seeing red underlines?
- Check that the package line at the top of each file says package com.cinema.model; exactly. A mismatch between the declared package and the file's folder location causes errors.
- Make sure BookingRequest.java and BookingResponse.java both have import java.util.List; at the top.
Help me fix red underlines in my model classes
✔️ Awesome, I've got everything!
Great. Double check you have no compilation errors in any model file.
ⓧ I'd like to double check the full code
package com.cinema.model;
import java.util.List;
public class BookingResponse {
private String bookingId;
private String screeningId;
private List<String> seatIds;
private double totalPrice;
public BookingResponse(String bookingId, String screeningId, List<String> seatIds, double totalPrice) {
this.bookingId = bookingId;
this.screeningId = screeningId;
this.seatIds = seatIds;
this.totalPrice = totalPrice;
}
public String getBookingId() { return bookingId; }
public String getScreeningId() { return screeningId; }
public List<String> getSeatIds() { return seatIds; }
public double getTotalPrice() { return totalPrice; }
}
Create the seat service and controller
The models define the shape of your data, but something needs to generate seat layouts and manage bookings. That is the job of a service class.
- Right-click on the com.cinema package in the Project pane.
- Select New > Package and type service.
- Right-click on com.cinema.service and create a new Java class named SeatService.
- Replace its contents with the following code:
package com.cinema.service;
import com.cinema.model.Seat;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class SeatService {
// In-memory store: maps each screening to its seat layout
private final Map<String, List<Seat>> screeningSeats = new HashMap<>();
// Returns seats for a screening, generating a fresh layout if none exists
public List<Seat> getSeatsForScreening(String screeningId) {
return screeningSeats.computeIfAbsent(screeningId, id -> generateSeats());
}
// Marks the requested seats as booked, returns false if any are already taken
public boolean bookSeats(String screeningId, List<String> seatIds) {
List<Seat> seats = getSeatsForScreening(screeningId);
for (Seat seat : seats) {
if (seatIds.contains(seat.getId())) {
if (seat.isBooked()) {
return false;
}
}
}
for (Seat seat : seats) {
if (seatIds.contains(seat.getId())) {
seat.setBooked(true);
}
}
return true;
}
// Sums up prices of the selected seats
public double calculateTotal(String screeningId, List<String> seatIds) {
List<Seat> seats = getSeatsForScreening(screeningId);
return seats.stream()
.filter(seat -> seatIds.contains(seat.getId()))
.mapToDouble(Seat::getPrice)
.sum();
}
// Generates a cinema layout: VIP (rows A-B), Standard (C-E), Economy (F-H)
private List<Seat> generateSeats() {
List<Seat> seats = new ArrayList<>();
String[] vipRows = {"A", "B"};
String[] standardRows = {"C", "D", "E"};
String[] economyRows = {"F", "G", "H"};
for (String row : vipRows) {
for (int i = 1; i <= 8; i++) {
seats.add(new Seat(row + i, row, i, "VIP", 25.00));
}
}
for (String row : standardRows) {
for (int i = 1; i <= 10; i++) {
seats.add(new Seat(row + i, row, i, "STANDARD", 15.00));
}
}
for (String row : economyRows) {
for (int i = 1; i <= 12; i++) {
seats.add(new Seat(row + i, row, i, "ECONOMY", 10.00));
}
}
// Pre-book roughly 20% of seats to simulate a partially-filled cinema
Random random = new Random(42);
for (Seat seat : seats) {
if (random.nextDouble() < 0.2) {
seat.setBooked(true);
}
}
return seats;
}
}
What does this service do?
- The @Service annotation tells Spring Boot to create a single shared instance of this class that controllers can use.
- The screeningSeats HashMap stores seat layouts in memory. Each unique screening ID gets its own independent set of seats.
- The generateSeats() method creates a realistic cinema layout: 2 VIP rows (8 seats each at $25), 3 Standard rows (10 seats each at $15), and 3 Economy rows (12 seats each at $10).
- About 20% of seats are randomly pre-booked using a fixed seed, so the layout looks the same every time the server restarts.
- Confirm SeatService.java has no red underlines in the editor.
Seeing import errors?
- Make sure the import com.cinema.model.Seat; line is present. If IntelliJ highlights it red, your Seat.java file might have a package declaration mismatch.
- Check that import java.util.*; is included. This covers HashMap, List, ArrayList, and Random.
Help me fix import errors in SeatService.java
✔️ Awesome, I've got everything!
Great. The service is ready for your controller to use.
ⓧ I'd like to double check the full code
package com.cinema.service;
import com.cinema.model.Seat;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class SeatService {
private final Map<String, List<Seat>> screeningSeats = new HashMap<>();
public List<Seat> getSeatsForScreening(String screeningId) {
return screeningSeats.computeIfAbsent(screeningId, id -> generateSeats());
}
public boolean bookSeats(String screeningId, List<String> seatIds) {
List<Seat> seats = getSeatsForScreening(screeningId);
for (Seat seat : seats) {
if (seatIds.contains(seat.getId())) {
if (seat.isBooked()) {
return false;
}
}
}
for (Seat seat : seats) {
if (seatIds.contains(seat.getId())) {
seat.setBooked(true);
}
}
return true;
}
public double calculateTotal(String screeningId, List<String> seatIds) {
List<Seat> seats = getSeatsForScreening(screeningId);
return seats.stream()
.filter(seat -> seatIds.contains(seat.getId()))
.mapToDouble(Seat::getPrice)
.sum();
}
private List<Seat> generateSeats() {
List<Seat> seats = new ArrayList<>();
String[] vipRows = {"A", "B"};
String[] standardRows = {"C", "D", "E"};
String[] economyRows = {"F", "G", "H"};
for (String row : vipRows) {
for (int i = 1; i <= 8; i++) {
seats.add(new Seat(row + i, row, i, "VIP", 25.00));
}
}
for (String row : standardRows) {
for (int i = 1; i <= 10; i++) {
seats.add(new Seat(row + i, row, i, "STANDARD", 15.00));
}
}
for (String row : economyRows) {
for (int i = 1; i <= 12; i++) {
seats.add(new Seat(row + i, row, i, "ECONOMY", 10.00));
}
}
Random random = new Random(42);
for (Seat seat : seats) {
if (random.nextDouble() < 0.2) {
seat.setBooked(true);
}
}
return seats;
}
}
Now the service needs a controller to expose it over HTTP. The controller maps incoming web requests to service methods.
- Right-click on com.cinema and create a new package named controller.
- Right-click on com.cinema.controller and create a new Java class named SeatController.
- Replace its contents with:
package com.cinema.controller;
import com.cinema.model.Seat;
import com.cinema.service.SeatService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/seats")
public class SeatController {
private final SeatService seatService;
// Spring injects the SeatService automatically via constructor injection
public SeatController(SeatService seatService) {
this.seatService = seatService;
}
// GET /api/seats/{screeningId} - returns all seats for a screening
@GetMapping("/{screeningId}")
public List<Seat> getSeats(@PathVariable String screeningId) {
return seatService.getSeatsForScreening(screeningId);
}
}
What does this controller do?
- The @RestController annotation marks this class as a web controller that returns data (JSON) directly instead of rendering a web page.
- The @RequestMapping("/api/seats") sets the base URL path for all endpoints in this controller.
- The @GetMapping("/{screeningId}") responds to GET requests at /api/seats/any-id-here, pulling the ID from the URL.
- Spring Boot automatically converts the returned List<Seat> into a JSON array in the HTTP response.
- Stop your running application by pressing the red square Stop button in IntelliJ's toolbar (or press Ctrl+F2).
- Restart it by running mvn spring-boot:run again in IntelliJ's terminal, or press the green Run button.
- Open your browser and navigate to http://localhost:8080/api/seats/test-screening.
- You should see a large JSON array of seat objects, each with fields like id, row, tier, price, and booked.
Not seeing JSON in your browser?
- Check IntelliJ's console for startup errors. If the application failed to start, look for a message about a missing bean or unresolved dependency.
- Make sure SeatService.java has the @Service annotation. Without it, Spring cannot inject it into the controller.
- Confirm the URL is exactly http://localhost:8080/api/seats/test-screening with no trailing slash.
Help me debug why my seats endpoint returns an error
✔️ Awesome, I've got everything!
Great. Your seats endpoint is live and returning data.
ⓧ I'd like to double check the full code
package com.cinema.controller;
import com.cinema.model.Seat;
import com.cinema.service.SeatService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/seats")
public class SeatController {
private final SeatService seatService;
public SeatController(SeatService seatService) {
this.seatService = seatService;
}
@GetMapping("/{screeningId}")
public List<Seat> getSeats(@PathVariable String screeningId) {
return seatService.getSeatsForScreening(screeningId);
}
}
Create the booking controller
The seats endpoint lets clients see what is available. Now you need an endpoint that accepts a booking and marks those seats as taken.
- Right-click on com.cinema.controller and create a new Java class named BookingController.
- Replace its contents with:
package com.cinema.controller;
import com.cinema.model.BookingRequest;
import com.cinema.model.BookingResponse;
import com.cinema.service.SeatService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@RequestMapping("/api/bookings")
public class BookingController {
private final SeatService seatService;
public BookingController(SeatService seatService) {
this.seatService = seatService;
}
// POST /api/bookings - accepts seat selections and returns a confirmation
@PostMapping
public ResponseEntity<BookingResponse> createBooking(@RequestBody BookingRequest request) {
// Attempt to book the requested seats
boolean success = seatService.bookSeats(request.getScreeningId(), request.getSeatIds());
if (!success) {
return ResponseEntity.badRequest().build();
}
// Calculate the total and generate a short booking ID
double total = seatService.calculateTotal(request.getScreeningId(), request.getSeatIds());
String bookingId = UUID.randomUUID().toString().substring(0, 8).toUpperCase();
BookingResponse response = new BookingResponse(bookingId, request.getScreeningId(), request.getSeatIds(), total);
return ResponseEntity.ok(response);
}
}
What does this controller do?
- The @PostMapping handles POST requests to /api/bookings. POST is the standard HTTP method for creating new resources.
- The @RequestBody annotation tells Spring to parse the incoming JSON body into a BookingRequest object.
- If any requested seat is already booked, the API returns a 400 Bad Request status. Otherwise, it marks the seats as taken, calculates the total price, and returns a confirmation with a unique 8-character booking ID.
- The ResponseEntity wrapper gives you control over the HTTP status code returned to the client.
- Stop and restart your application again.
- Open a terminal in IntelliJ (click Terminal at the bottom of the window).
- Test the booking endpoint by running this command:
curl -X POST http://localhost:8080/api/bookings -H "Content-Type: application/json" -d '{"screeningId":"test-screening","seatIds":["A1","A2"]}'
- You should see a JSON response containing a bookingId, the screeningId, your selected seatIds, and a totalPrice of 50.0 (two VIP seats at $25 each).
Getting an empty response or error?
- If you see a 400 error, it means the seats are already booked. Stop and restart the server to reset the in-memory data, then try again.
- On Windows, single quotes in curl commands can cause issues. Use double quotes for the outer string and escape the inner quotes with a backslash.
- Make sure the application fully started before running the curl command. Look for the "Started SeatBookingApiApplication" line in the console output.
Help me debug my booking endpoint
✔️ Awesome, I've got everything!
Great. Your booking API is complete and accepting requests.
ⓧ I'd like to double check the full code
package com.cinema.controller;
import com.cinema.model.BookingRequest;
import com.cinema.model.BookingResponse;
import com.cinema.service.SeatService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@RequestMapping("/api/bookings")
public class BookingController {
private final SeatService seatService;
public BookingController(SeatService seatService) {
this.seatService = seatService;
}
@PostMapping
public ResponseEntity<BookingResponse> createBooking(@RequestBody BookingRequest request) {
boolean success = seatService.bookSeats(request.getScreeningId(), request.getSeatIds());
if (!success) {
return ResponseEntity.badRequest().build();
}
double total = seatService.calculateTotal(request.getScreeningId(), request.getSeatIds());
String bookingId = UUID.randomUUID().toString().substring(0, 8).toUpperCase();
BookingResponse response = new BookingResponse(bookingId, request.getScreeningId(), request.getSeatIds(), total);
return ResponseEntity.ok(response);
}
}
Your backend is fully functional. You have endpoints that serve seat data and process bookings. Next, you will build the Next.js frontend that gives users a visual way to browse movies and pick their seats.
Set Up Your Next.js Frontend
Your backend API is running and ready to serve seat data and bookings. Now it's time to build the frontend that users will actually interact with.
In this step, you'll scaffold a Next.js project, connect it to the TMDB movie database for real movie posters and ratings, and create the page structure for movie browsing and seat selection.
In this step, get ready to:
- Scaffold a Next.js project with App Router and Tailwind CSS.
- Obtain a free TMDB API key and store it in your project.
- Create the page structure for movie listing and seat selection.
Scaffold a new Next.js project
Next.js is a React framework that handles routing, server-side rendering, and build tooling out of the box. You'll use its App Router for file-based routing and Tailwind CSS for styling.
- Switch back to VS Code.
- Open a new terminal by pressing Ctrl+` (macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold your Next.js project by running this command:
npx create-next-app@latest cinema-frontend
What will this command do?
This command downloads the latest Next.js project scaffolding tool and creates a new folder called cinema-frontend on your Desktop. It will ask you a series of questions about how to configure the project.
The wizard will prompt you with several configuration questions. Answer each one individually:
- Enter No for the TypeScript prompt.
- Enter Yes for the ESLint prompt.
- Enter Yes for the Tailwind CSS prompt.
- Enter No for the src/ directory prompt.
- Enter Yes for the App Router prompt.
- Accept the default for any other prompts by pressing Enter.
Wait for the installation to complete. This may take a minute or two depending on your internet speed.
- Once the installation finishes, open the new project folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the cinema-frontend folder, then click Open.
- Open a new terminal in VS Code by pressing Ctrl+` (macOS and Windows).
- Start the development server by running:
npm run dev
- Open your browser and navigate to http://localhost:3000.
- You should see the default Next.js welcome page with the Next.js logo and links to documentation.
Don't see the welcome page?
- Make sure the terminal shows the dev server running on port 3000. If another process is using that port, stop it first.
- Check that you ran npm run dev from inside the cinema-frontend folder, not from your Desktop.
- If you see a Node.js version error, confirm you have Node.js 20.9 or higher installed by running node --version.
Why isn't my Next.js dev server starting?
Get your TMDB API key
Your app will display real movies currently playing in theatres. To fetch this data, you need an API key from The Movie Database (TMDB).
What is TMDB?
TMDB (The Movie Database) is a community-built database of movie and TV information. Its API is free for non-commercial use and provides movie titles, posters, ratings, and more.
- Open your browser and go to themoviedb.org.
- Click Join TMDB (or sign in if you already have an account).
- Fill in a username, password, and email address, then click Sign up.
- Confirm your email address by clicking the verification link TMDB sends to your inbox.
- Once logged in, click your profile avatar in the top-right corner.
- Click Settings.
- Click API in the left sidebar.
- Click to request an API key, selecting the Developer option.
- Accept the terms of use and fill out the application form. For the URL, you can enter http://localhost:3000. For the description, mention it is a personal learning project.
Which token do I need?
After your API key is approved, you will see two values on the API settings page: an API Key and an API Read Access Token. Copy the API Read Access Token (the longer value). This is what your app uses as a Bearer token to authenticate requests.
- Copy the API Read Access Token value from the API settings page.
Now you need to store this token in your project as an environment variable.
- Stop the dev server in your VS Code terminal by pressing Ctrl+C.
- Create a new file called .env.local in the root of your cinema-frontend folder by running:
touch .env.local
- Open .env.local in VS Code by clicking on it in the Explorer sidebar.
- Add the following line, replacing your-api-key-here with the API Read Access Token you copied:
NEXT_PUBLIC_TMDB_API_KEY=your-api-key-here
Why NEXT_PUBLIC_ prefix?
In Next.js, environment variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Without this prefix, the variable would only be available on the server side and your client-side code could not read it.
- Save .env.local.
Can't find the API Read Access Token?
- Make sure you are on the API settings page (Settings > API in the left sidebar), not the general settings page.
- The API Read Access Token is listed below the shorter API Key. It is a long string of characters.
- If you only see an option to request a key, your application may still be pending. Try refreshing the page after a few moments.
Help me find my TMDB API Read Access Token.
Create the app page structure
Next.js uses file-based routing with the App Router. Each folder inside the app/ directory becomes a URL route, and a page.js file inside it defines what renders at that route.
Your app needs two pages: a home page listing movies, and a dynamic seat selection page for each movie.
- Create the dynamic route folder for seat selection by running these commands in your VS Code terminal:
mkdir -p app/movie/\[id\]
What are the square brackets for?
In Next.js App Router, a folder name wrapped in square brackets like [id] creates a dynamic route. This means the URL /movie/123 or /movie/456 will both render the same page, with the number available as a parameter.
- Create a placeholder page file for the seat selection route by running:
touch app/movie/\[id\]/page.js
- Open app/movie/[id]/page.js in the Explorer sidebar and add this placeholder content:
export default function SeatSelection() {
return <div>Seat Selection Page</div>;
}
What does this placeholder do?
This creates a minimal React component that proves the route works. You will replace this with the full seat selection UI in a later step.
- Save app/movie/[id]/page.js.
- Restart the dev server by running:
npm run dev
- In your browser, navigate to http://localhost:3000/movie/123.
- You should see the text "Seat Selection Page" displayed. This confirms your dynamic route is working.
Seeing a 404 page?
- Check that the folder structure is exactly app/movie/[id]/page.js. The square brackets must be part of the folder name.
- Make sure you saved the file and that the dev server is running.
- Verify in the Explorer sidebar that the folder shows as [id] with brackets, not just id.
Why is my dynamic route returning a 404?
Your project now has two routes: the home page at / (served by app/page.js, which create-next-app generated) and the seat selection page at /movie/[id] (your new dynamic route).
✔️ Awesome, I've got everything!
Great. Make sure you have saved .env.local and app/movie/[id]/page.js.
ⓧ I'd like to double check the full code
Here is the content of .env.local (with your own token replacing the placeholder):
NEXT_PUBLIC_TMDB_API_KEY=your-api-key-here
Here is app/movie/[id]/page.js:
export default function SeatSelection() {
return <div>Seat Selection Page</div>;
}
Your frontend project is scaffolded and connected to TMDB. Next up, you'll build the movie listing page that fetches real movies and displays them in a responsive grid.
Build the Movie Listing Page
Your backend API is running and ready to handle requests. Now it's time to build the page your users will actually see.
This page pulls real movies currently in theatres from the TMDB API and displays them as a grid of poster cards. Each card shows the movie poster, title, and star rating.
In this step, get ready to:
- Fetch "Now Playing" movies from the TMDB API and display them in a responsive grid.
- Make each movie card clickable, navigating to the seat selection page.
Fetch and display Now Playing movies
The TMDB API provides a now_playing endpoint that returns movies currently in theatres. Your page will call this endpoint when it loads, store the results in React state, and render them as poster cards in a responsive CSS grid.
- In VS Code, open app/page.js and replace all of its content with the following code:
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
export default function Home() {
// State to hold the list of movies from TMDB
const [movies, setMovies] = useState([]);
// Loading state to show a placeholder while fetching
const [loading, setLoading] = useState(true);
// Fetch movies from TMDB when the component mounts
useEffect(() => {
async function fetchMovies() {
const res = await fetch(
`https://api.themoviedb.org/3/movie/now_playing?language=en-US&page=1`,
{
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_TMDB_API_KEY}`,
},
}
);
const data = await res.json();
setMovies(data.results || []);
setLoading(false);
}
fetchMovies();
}, []);
// Show loading state while movies are being fetched
if (loading) {
return <div className="flex justify-center items-center min-h-screen">Loading movies...</div>;
}
return (
<main className="min-h-screen p-8 bg-gray-900 text-white">
<h1 className="text-3xl font-bold mb-8 text-center">Now Playing</h1>
{/* Responsive grid: 2 columns on mobile, 3 on medium, 4 on large */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 max-w-6xl mx-auto">
{movies.map((movie) => (
<Link key={movie.id} href={`/movie/${movie.id}?title=${encodeURIComponent(movie.title)}`}>
<div className="bg-gray-800 rounded-lg overflow-hidden hover:scale-105 transition-transform cursor-pointer">
<img
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
alt={movie.title}
className="w-full"
/>
<div className="p-3">
<h2 className="font-semibold text-sm truncate">{movie.title}</h2>
<p className="text-yellow-400 text-xs">⭐ {movie.vote_average.toFixed(1)}</p>
</div>
</div>
</Link>
))}
</div>
</main>
);
}
What does this code do?
This component uses two React hooks to manage data fetching and display.
- useState creates two state variables: movies holds the array of movie objects from TMDB, and loading tracks whether the fetch is still in progress.
- useEffect runs fetchMovies() once when the component first renders. The empty dependency array [] ensures it only fires on mount.
- The fetch call uses a Bearer token from your NEXT_PUBLIC_TMDB_API_KEY environment variable. TMDB requires this for authentication.
- The responsive grid uses Tailwind CSS utility classes to show 2 columns on mobile, 3 on medium screens, and 4 on large screens.
- Each movie card is wrapped in a Link component that navigates to /movie/[id]?title=... when clicked. This passes both the movie ID and title to the seat selection page.
- Save app/page.js.
- Start your Next.js development server by running this command in the VS Code terminal:
npm run dev
- Open http://localhost:3000 in your browser.
- You should see a dark page with the heading "Now Playing" and a grid of movie poster cards, each showing the poster image, title, and a star rating.
Seeing a blank page or error?
- Check your browser's developer console for errors. If you see a 401 status, your TMDB API key in .env.local is missing or incorrect. Double check it matches the key from your TMDB account.
- Make sure .env.local uses the exact variable name NEXT_PUBLIC_TMDB_API_KEY. The NEXT_PUBLIC_ prefix is required for browser-accessible environment variables in Next.js.
- If you changed .env.local after starting the server, stop it with Ctrl+C and restart with npm run dev. Next.js only reads environment variables at startup.
Help me debug why my TMDB API call is failing in my Next.js app.
Test the clickable movie cards
Each movie card is wrapped in a Next.js Link component. This provides client-side navigation without a full page reload.
- Click any movie card in the grid.
- Check your browser's URL bar. It should now show something like http://localhost:3000/movie/123456?title=Movie%20Name where the movie's ID and title are embedded in the URL.
Why pass the title in the URL?
The seat selection page needs to display the movie title, but it only receives the movie ID from the route parameter. By encoding the title as a query parameter with encodeURIComponent, you avoid a second API call to TMDB just to get the movie name. This keeps the seat selection page fast and reduces unnecessary network requests.
The seat selection page is still a placeholder, so you won't see anything meaningful yet. You'll build that in the next step.
✔️ Awesome, I've got everything!
Great. Double check you've saved app/page.js and that your dev server is still running.
ⓧ I'd like to double check the full code
Here is the complete app/page.js file:
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
export default function Home() {
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchMovies() {
const res = await fetch(
`https://api.themoviedb.org/3/movie/now_playing?language=en-US&page=1`,
{
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_TMDB_API_KEY}`,
},
}
);
const data = await res.json();
setMovies(data.results || []);
setLoading(false);
}
fetchMovies();
}, []);
if (loading) {
return <div className="flex justify-center items-center min-h-screen">Loading movies...</div>;
}
return (
<main className="min-h-screen p-8 bg-gray-900 text-white">
<h1 className="text-3xl font-bold mb-8 text-center">Now Playing</h1>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 max-w-6xl mx-auto">
{movies.map((movie) => (
<Link key={movie.id} href={`/movie/${movie.id}?title=${encodeURIComponent(movie.title)}`}>
<div className="bg-gray-800 rounded-lg overflow-hidden hover:scale-105 transition-transform cursor-pointer">
<img
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
alt={movie.title}
className="w-full"
/>
<div className="p-3">
<h2 className="font-semibold text-sm truncate">{movie.title}</h2>
<p className="text-yellow-400 text-xs">⭐ {movie.vote_average.toFixed(1)}</p>
</div>
</div>
</Link>
))}
</div>
</main>
);
}
Your movie listing page is live and pulling real data from TMDB. Next up, you'll build the seat selection page that lets users pick their cinema seats and book them through your Spring Boot API.
Build the Seat Selection Page
Your movie listing page is live and each card links to a seat selection route. Now you need to build the page that actually appears when a user clicks a movie.
This is where your frontend and backend truly connect. The seat selection page fetches real seat data from your Spring Boot API, lets users pick seats interactively, and submits a booking back to the server.
In this step, get ready to:
- Create the seat map component that fetches data from your Spring Boot API and renders a cinema-style grid.
- Add click-to-select functionality with live price tracking.
- Wire up the Confirm Booking button to POST to your backend and display a receipt.
Create the seat map component
The seat selection page needs to read the movie ID from the URL, fetch seat data from your backend, and display a color-coded grid of seats grouped by row.
- Switch back to VS Code.
- Open app/movie/[id]/page.js and replace its entire contents with the following code:
"use client";
import { useState, useEffect } from "react";
import { useParams, useSearchParams } from "next/navigation";
export default function SeatSelection() {
const params = useParams();
const searchParams = useSearchParams();
const movieId = params.id;
const movieTitle = searchParams.get("title") || "Movie";
const [seats, setSeats] = useState([]);
const [selectedSeats, setSelectedSeats] = useState([]);
const [booking, setBooking] = useState(null);
const [loading, setLoading] = useState(true);
// Construct a unique screening ID from the movie's ID
const screeningId = `movie-${movieId}`;
// Fetch seat data from the Spring Boot API on mount
useEffect(() => {
async function fetchSeats() {
const res = await fetch(`http://localhost:8080/api/seats/${screeningId}`);
const data = await res.json();
setSeats(data);
setLoading(false);
}
fetchSeats();
}, [screeningId]);
What does this code do?
useParams() grabs the movie ID from the URL path (e.g. /movie/550). useSearchParams() reads the title from the query string that you passed from the movie card link.
The screeningId is built by combining "movie-" with the movie's ID. Your Spring Boot backend uses this to look up (or generate) the seat layout for that screening.
Add click-to-select functionality with live price tracking
Users need to click seats to select them, see a running total of the price, and have visual feedback on which tier each seat belongs to. Three handler functions power this interaction.
- Directly below the useEffect block you just pasted, add the following code:
// Toggle a seat's selection state
function handleSeatClick(seat) {
if (seat.booked) return;
setSelectedSeats((prev) =>
prev.find((s) => s.id === seat.id)
? prev.filter((s) => s.id !== seat.id)
: [...prev, seat]
);
}
// Submit selected seats to the booking API
async function handleBooking() {
const res = await fetch("http://localhost:8080/api/bookings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
screeningId,
seatIds: selectedSeats.map((s) => s.id),
}),
});
if (res.ok) {
const data = await res.json();
setBooking(data);
}
}
// Calculate the total price from all selected seats
const totalPrice = selectedSeats.reduce((sum, seat) => sum + seat.price, 0);
// Group seats by row for rendering
const rows = seats.reduce((acc, seat) => {
if (!acc[seat.row]) acc[seat.row] = [];
acc[seat.row].push(seat);
return acc;
}, {});
// Return the correct CSS classes based on seat state and tier
function getSeatColor(seat) {
if (seat.booked) return "bg-gray-600 cursor-not-allowed";
if (selectedSeats.find((s) => s.id === seat.id)) return "bg-white text-black";
if (seat.tier === "VIP") return "bg-yellow-500 hover:bg-yellow-400";
if (seat.tier === "STANDARD") return "bg-blue-500 hover:bg-blue-400";
return "bg-green-500 hover:bg-green-400";
}
What does this code do?
- handleSeatClick toggles a seat in and out of the selectedSeats array. If it's already selected, clicking removes it. If not, clicking adds it.
- handleBooking sends a POST request with the screening ID and an array of selected seat IDs. On success, it saves the booking response to state.
- getSeatColor returns Tailwind classes based on priority: booked (gray), selected (white), then tier-based colors (gold for VIP, blue for Standard, green for Economy).
Build the seat grid and booking confirmation UI
The final piece is the JSX that renders the cinema-style seat grid, the color legend, the selected seats summary, and a booking confirmation screen.
- Below the getSeatColor function, add the component's return statements to complete the file:
if (loading) {
return <div className="flex justify-center items-center min-h-screen text-white bg-gray-900">Loading seats...</div>;
}
if (booking) {
return (
<main className="min-h-screen p-8 bg-gray-900 text-white flex flex-col items-center justify-center">
<div className="bg-gray-800 p-8 rounded-lg max-w-md w-full text-center">
<h1 className="text-2xl font-bold mb-4">Booking Confirmed!</h1>
<p className="text-gray-400 mb-2">Booking ID: <span className="text-white font-mono">{booking.bookingId}</span></p>
<p className="text-gray-400 mb-2">Movie: <span className="text-white">{movieTitle}</span></p>
<p className="text-gray-400 mb-2">Seats: <span className="text-white">{booking.seatIds.join(", ")}</span></p>
<p className="text-2xl font-bold text-green-400 mt-4">Total: ${booking.totalPrice.toFixed(2)}</p>
</div>
</main>
);
}
return (
<main className="min-h-screen p-8 bg-gray-900 text-white">
<h1 className="text-2xl font-bold text-center mb-2">{movieTitle}</h1>
<p className="text-center text-gray-400 mb-8">Select your seats</p>
<div className="max-w-2xl mx-auto mb-6">
<div className="bg-gray-700 text-center py-2 rounded mb-8 text-sm text-gray-300">SCREEN</div>
<div className="space-y-2">
{Object.entries(rows).map(([row, rowSeats]) => (
<div key={row} className="flex items-center justify-center gap-1">
<span className="w-6 text-xs text-gray-400">{row}</span>
{rowSeats.map((seat) => (
<button
key={seat.id}
onClick={() => handleSeatClick(seat)}
disabled={seat.booked}
className={`w-7 h-7 rounded text-xs font-bold transition-colors ${getSeatColor(seat)}`}
title={`${seat.tier} - $${seat.price}`}
>
{seat.number}
</button>
))}
</div>
))}
</div>
</div>
<div className="max-w-2xl mx-auto">
<div className="flex justify-center gap-6 mb-6 text-sm">
<div className="flex items-center gap-2"><div className="w-4 h-4 bg-yellow-500 rounded"></div> VIP ($25)</div>
<div className="flex items-center gap-2"><div className="w-4 h-4 bg-blue-500 rounded"></div> Standard ($15)</div>
<div className="flex items-center gap-2"><div className="w-4 h-4 bg-green-500 rounded"></div> Economy ($10)</div>
<div className="flex items-center gap-2"><div className="w-4 h-4 bg-gray-600 rounded"></div> Taken</div>
</div>
{selectedSeats.length > 0 && (
<div className="bg-gray-800 p-4 rounded-lg text-center">
<p className="mb-2">Selected: {selectedSeats.map((s) => s.id).join(", ")}</p>
<p className="text-xl font-bold mb-4">Total: ${totalPrice.toFixed(2)}</p>
<button
onClick={handleBooking}
className="bg-red-600 hover:bg-red-500 px-6 py-2 rounded font-bold"
>
Confirm Booking
</button>
</div>
)}
</div>
</main>
);
}
What does this rendering do?
- The if (loading) check shows a loading spinner while seats are being fetched from the API.
- The if (booking) check renders a confirmation receipt after a successful booking, showing the booking ID, movie title, seats, and total price.
- The main return renders the seat grid row by row. Each seat is a button colored by its tier. A SCREEN label at the top orients the user.
- The selection summary panel only appears when at least one seat is selected. It shows selected seat IDs, the running total, and the Confirm Booking button.
- Save app/movie/[id]/page.js.
- Return to your browser at localhost:3000 and click any movie card.
- You should see a cinema-style seat grid with gold seats (rows A-B), blue seats (rows C-E), and green seats (rows F-H). Some seats appear gray because they are pre-booked.
- Click a few available seats. They should turn white and a summary panel should appear at the bottom showing the selected seat IDs and a running total.
- Click Confirm Booking. You should see a confirmation screen with a booking ID, the movie title, the seats you selected, and the total cost.
Seeing a blank page or errors?
- Make sure your Spring Boot backend is still running on localhost:8080 in IntelliJ. The seat selection page fetches data from it.
- Check the browser console for CORS errors. If you see one, confirm that your CorsConfig.java allows http://localhost:3000 and that the backend was restarted after you added it.
- If the Confirm Booking button does nothing, open the browser console and look for network errors. A 400 response means the seats are already booked. Restart your Spring Boot app to reset the in-memory data.
Help me debug my seat selection page
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete app/movie/[id]/page.js file. Make sure yours matches exactly:
"use client";
import { useState, useEffect } from "react";
import { useParams, useSearchParams } from "next/navigation";
export default function SeatSelection() {
const params = useParams();
const searchParams = useSearchParams();
const movieId = params.id;
const movieTitle = searchParams.get("title") || "Movie";
const [seats, setSeats] = useState([]);
const [selectedSeats, setSelectedSeats] = useState([]);
const [booking, setBooking] = useState(null);
const [loading, setLoading] = useState(true);
const screeningId = `movie-${movieId}`;
useEffect(() => {
async function fetchSeats() {
const res = await fetch(`http://localhost:8080/api/seats/${screeningId}`);
const data = await res.json();
setSeats(data);
setLoading(false);
}
fetchSeats();
}, [screeningId]);
function handleSeatClick(seat) {
if (seat.booked) return;
setSelectedSeats((prev) =>
prev.find((s) => s.id === seat.id)
? prev.filter((s) => s.id !== seat.id)
: [...prev, seat]
);
}
async function handleBooking() {
const res = await fetch("http://localhost:8080/api/bookings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
screeningId,
seatIds: selectedSeats.map((s) => s.id),
}),
});
if (res.ok) {
const data = await res.json();
setBooking(data);
}
}
const totalPrice = selectedSeats.reduce((sum, seat) => sum + seat.price, 0);
const rows = seats.reduce((acc, seat) => {
if (!acc[seat.row]) acc[seat.row] = [];
acc[seat.row].push(seat);
return acc;
}, {});
function getSeatColor(seat) {
if (seat.booked) return "bg-gray-600 cursor-not-allowed";
if (selectedSeats.find((s) => s.id === seat.id)) return "bg-white text-black";
if (seat.tier === "VIP") return "bg-yellow-500 hover:bg-yellow-400";
if (seat.tier === "STANDARD") return "bg-blue-500 hover:bg-blue-400";
return "bg-green-500 hover:bg-green-400";
}
if (loading) {
return <div className="flex justify-center items-center min-h-screen text-white bg-gray-900">Loading seats...</div>;
}
if (booking) {
return (
<main className="min-h-screen p-8 bg-gray-900 text-white flex flex-col items-center justify-center">
<div className="bg-gray-800 p-8 rounded-lg max-w-md w-full text-center">
<h1 className="text-2xl font-bold mb-4">Booking Confirmed!</h1>
<p className="text-gray-400 mb-2">Booking ID: <span className="text-white font-mono">{booking.bookingId}</span></p>
<p className="text-gray-400 mb-2">Movie: <span className="text-white">{movieTitle}</span></p>
<p className="text-gray-400 mb-2">Seats: <span className="text-white">{booking.seatIds.join(", ")}</span></p>
<p className="text-2xl font-bold text-green-400 mt-4">Total: ${booking.totalPrice.toFixed(2)}</p>
</div>
</main>
);
}
return (
<main className="min-h-screen p-8 bg-gray-900 text-white">
<h1 className="text-2xl font-bold text-center mb-2">{movieTitle}</h1>
<p className="text-center text-gray-400 mb-8">Select your seats</p>
<div className="max-w-2xl mx-auto mb-6">
<div className="bg-gray-700 text-center py-2 rounded mb-8 text-sm text-gray-300">SCREEN</div>
<div className="space-y-2">
{Object.entries(rows).map(([row, rowSeats]) => (
<div key={row} className="flex items-center justify-center gap-1">
<span className="w-6 text-xs text-gray-400">{row}</span>
{rowSeats.map((seat) => (
<button
key={seat.id}
onClick={() => handleSeatClick(seat)}
disabled={seat.booked}
className={`w-7 h-7 rounded text-xs font-bold transition-colors ${getSeatColor(seat)}`}
title={`${seat.tier} - $${seat.price}`}
>
{seat.number}
</button>
))}
</div>
))}
</div>
</div>
<div className="max-w-2xl mx-auto">
<div className="flex justify-center gap-6 mb-6 text-sm">
<div className="flex items-center gap-2"><div className="w-4 h-4 bg-yellow-500 rounded"></div> VIP ($25)</div>
<div className="flex items-center gap-2"><div className="w-4 h-4 bg-blue-500 rounded"></div> Standard ($15)</div>
<div className="flex items-center gap-2"><div className="w-4 h-4 bg-green-500 rounded"></div> Economy ($10)</div>
<div className="flex items-center gap-2"><div className="w-4 h-4 bg-gray-600 rounded"></div> Taken</div>
</div>
{selectedSeats.length > 0 && (
<div className="bg-gray-800 p-4 rounded-lg text-center">
<p className="mb-2">Selected: {selectedSeats.map((s) => s.id).join(", ")}</p>
<p className="text-xl font-bold mb-4">Total: ${totalPrice.toFixed(2)}</p>
<button
onClick={handleBooking}
className="bg-red-600 hover:bg-red-500 px-6 py-2 rounded font-bold"
>
Confirm Booking
</button>
</div>
)}
</div>
</main>
);
}
Why in-memory seat data?
Your Spring Boot backend stores seats in a HashMap, not a database. This means seat availability resets every time you restart the server.
For a learning project, this keeps things simple. In production, you would swap the HashMap for a database like PostgreSQL to persist bookings across restarts.
Your full-stack cinema booking app is complete. You have a Spring Boot API serving seat data and processing bookings, and a Next.js frontend that fetches real movies, displays an interactive seat map, and submits bookings. The entire flow works end to end.
Secret mission
Add Showtime Selection
Real cinemas don't just show one screening per movie. Can you add a showtime selector that lets users pick between 2:00 PM, 5:30 PM, and 9:00 PM screenings, each with independent seat availability?
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 with a free TMDB API, so there are no ongoing costs.
Resources you used:
- Spring Boot backend project (seat-booking-api folder).
- Next.js frontend project folder.
- Running dev servers (localhost:8080 and localhost:3000).
- TMDB API key stored in .env.local.
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment further with the seat booking app.
- Your Spring Boot server continues running on localhost:8080 in IntelliJ IDEA.
- Your Next.js dev server continues running on localhost:3000 in VS Code.
- Your TMDB API key remains active and free for non-commercial use.
Pause - I'll come back to this later
Shut down the running servers to free up system resources. Your project files stay on disk so you can restart later.
- In the IntelliJ IDEA terminal where Spring Boot is running, press Ctrl+C to stop the backend server.
- In the VS Code terminal where Next.js is running, press Ctrl+C to stop the frontend dev server.
- Close IntelliJ IDEA and VS Code.
- To restart later, reopen each project in its IDE and run mvn spring-boot:run for the backend and npm run dev for the frontend.
Delete - I don't want to use this again
Remove all project files and stop all running processes. This cannot be undone.
- In the IntelliJ IDEA terminal, press Ctrl+C to stop the Spring Boot server.
- In the VS Code terminal, press Ctrl+C to stop the Next.js dev server.
- Close both IntelliJ IDEA and VS Code.
- Delete the Spring Boot project folder by running this command:
macOS
cd ~/Desktop
rm -rf seat-booking-api
Delete your Next.js project folder (replace the folder name with whatever you named it during setup):
rm -rf cinema-seat-booking
Windows
cd ~\Desktop
rmdir /s /q seat-booking-api
Delete your Next.js project folder (replace the folder name with whatever you named it during setup):
rmdir /s /q cinema-seat-booking
Your TMDB API key is free and does not incur charges. You can leave your TMDB account active or revoke the API key from your account settings at themoviedb.org.
That's a Wrap!
That's a Wrap!
Nice work. You just built a full-stack cinema booking system from scratch, connecting a Java backend to a React frontend with real movie data.
You've learned how to:
- Built a Spring Boot REST API with seat management and booking endpoints that track availability in memory.
- Created a Next.js frontend that fetches real movie data from the TMDB API and displays it in a responsive grid.
- Designed an interactive seat picker with three pricing tiers, dynamic totals, and a full booking confirmation flow.
- Added showtime selection with independent seat availability per screening, proving your backend can handle multiple unique states.
Ready to quiz yourself?