Columbia Course Catalog API
Build and deploy a Java Spring Boot REST API with OOP design patterns.
Introduction
30 Second Summary
Every time you browse a university's course catalog online, there's an API behind the scenes serving up that data. Building one yourself proves you can design clean software architecture, not just follow tutorials.
In this project, you will build a Columbia University Course Catalog REST API using Java and Spring Boot. You'll model different course types with object-oriented design patterns and deploy the finished API to a live public URL.
What You'll Build
A REST API that serves Columbia course data, models course types using OOP inheritance, and exposes endpoints for browsing, filtering, and searching courses.
By the end of this project, you'll have:
- A Spring Boot REST API with endpoints for browsing, filtering, and searching courses.
- An OOP domain model using an abstract base class, three concrete subclasses, and a Searchable interface demonstrating polymorphism.
- A CSV data loader that populates the database automatically on application startup.
- A live deployment on Railway with a public URL that anyone can interact with.
- Secret Mission: Implement the Strategy pattern with swappable recommendation algorithms that a recruiter can invoke via the API.
Are there any prerequisites?
You need familiarity with at least one programming language (Python or TypeScript is perfect). No prior Java experience is required. This project teaches you Java and Spring Boot from scratch.
Set Up Your Java Environment
Before you can build a REST API, you need the tools that make Java development possible. Spring Boot is the framework that powers your API, and it requires JDK 17 (Java Development Kit) to compile and run your code.
In this step, you will install Java, generate a pre-configured Spring Boot project, and confirm everything runs locally.
In this step, get ready to:
- Install JDK 17 and verify the installation.
- Generate a Spring Boot project from Spring Initializr.
- Run the application locally and confirm it starts on localhost:8080.
Install JDK 17
Spring Boot 3.3 requires Java SDK version 17 or higher. The Spring team recommends BellSoft Liberica JDK as the preferred distribution.
- Open a terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check if Java is already installed by running:
java -version
✔️ I see version 17 or higher
Your output shows something like openjdk version "17.x.x". You are good to go. Skip ahead to the next substep.
ⓧ I see an older version
You have Java installed, but it is an older version that will not work with Spring Boot 3.3.13. You need to install JDK 17 alongside or replace the existing version.
macOS
- Install Liberica JDK 17 using Homebrew by running these commands:
brew tap bell-sw/liberica
brew install --cask liberica-jdk17
- Close and reopen your terminal, then run java -version again to confirm it now shows version 17.
Windows
- Install Liberica JDK 17 using winget by running this command:
winget install -e --id BellSoft.LibericaJDK.17
- Close and reopen PowerShell, then run java -version again to confirm it now shows version 17.
ⓧ Command not found
Java is not installed on your machine yet. Follow the instructions below to install it.
macOS
- Install Homebrew first if you do not already have it. Check by running brew --version. If you see a version number, skip to the next bullet.
- Install Liberica JDK 17 by running these commands:
brew tap bell-sw/liberica
brew install --cask liberica-jdk17
- Close and reopen your terminal, then verify the install by running:
java -version
You should see output containing openjdk version "17.
Windows
- Install Liberica JDK 17 using winget by running this command in PowerShell:
winget install -e --id BellSoft.LibericaJDK.17
- Close and reopen PowerShell, then verify the install by running:
java -version
You should see output containing openjdk version "17.
Why Liberica JDK?
BellSoft Liberica is the JDK distribution recommended by the Spring team on the official Spring Quickstart guide. It is free, open-source, and TCK-verified for Java SE compliance.
Still seeing an older version after installing?
On macOS, the system may still resolve to a pre-installed Java. Close all terminal windows and open a fresh one. Run java -version again.
On Windows, ensure you closed and reopened PowerShell after the winget install completed. The PATH updates only apply to new sessions. Help me fix my Java version
Generate a Spring Boot project from Spring Initializr
Spring Initializr is a web tool that generates a ready-to-code project structure with all your dependencies pre-configured. You will use it to scaffold the course catalog API.
- Open your browser and navigate to start.spring.io.
- Set Project to Maven.
- Set Language to Java.
- Set Spring Boot version to 3.3.13.
- Under Project Metadata, set Group to com.columbia.
- Set Artifact to course-catalog.
- Set Name to course-catalog.
- Set Description to Columbia Course Catalog API.
- Set Packaging to Jar.
- Set Java version to 17.
- Click Add Dependencies and search for Spring Web. Click it to add.
- Add Spring Data JPA the same way.
- Add H2 Database.
- Add Validation (it may appear as "Spring Boot Validation" or just "Validation").
- Click the Generate button. A course-catalog.zip file downloads to your computer.
- Unzip course-catalog.zip. Move the resulting course-catalog folder to your Desktop.
What are these dependencies?
Spring Web provides the HTTP server and REST controller annotations. Spring Data JPA handles database operations through simple Java interfaces.
H2 Database is a lightweight in-memory database. It is perfect for a portfolio API because it requires zero setup. Validation adds annotations like @NotBlank and @Min to enforce input rules on your API.
Why Maven over Gradle?
Maven is the most common build tool in enterprise Java, where most recruiter-facing roles live. Its XML configuration is verbose but predictable for a beginner. The project ships with a Maven Wrapper (mvnw), so you do not need to install Maven separately.
Open the project and run it
Now you will open the generated project in VS Code and verify Spring Boot starts correctly.
- Switch back to VS Code.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the course-catalog folder, then click Open.
You should see the project structure in the Explorer sidebar, including pom.xml and the src folder.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same shortcut on macOS and Windows).
- Start the Spring Boot application by running:
macOS
./mvnw spring-boot:run
Windows
mvnw.cmd spring-boot:run
What does this command do?
The mvnw file is the Maven Wrapper. It downloads and uses the correct Maven version automatically, so you never need to install Maven separately.
The spring-boot:run goal compiles your code and starts an embedded Tomcat server on port 8080.
The first run downloads all dependencies, which may take a minute or two. Wait until you see output containing Started CourseCatalogApplication.
- Open your browser and navigate to http://localhost:8080.
You will see a "Whitelabel Error Page" with a 404 status. This is expected because you have not created any endpoints yet. The error page confirms Spring Boot is running and responding to requests.
- Return to your terminal and press Ctrl+C to stop the application.
Application fails to start?
If you see an error about port 8080 already in use, another process is occupying that port. Stop it or close other development servers before retrying.
If you see a permission denied error on macOS when running ./mvnw, make the file executable first by running chmod +x mvnw and then try again.
Help me fix my Spring Boot startup error
✔️ Awesome, I've got everything!
Great. Double check you have saved any files and that the application started successfully before moving on.
ⓧ I'd like to double check the full code
Here is what your pom.xml should look like:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.13</version>
<relativePath/>
</parent>
<groupId>com.columbia</groupId>
<artifactId>course-catalog</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>course-catalog</name>
<description>Columbia Course Catalog API</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
And here is CourseCatalogApplication.java:
package com.columbia.coursecatalog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CourseCatalogApplication {
public static void main(String[] args) {
SpringApplication.run(CourseCatalogApplication.class, args);
}
}
Your Java environment is ready and Spring Boot runs locally. Next up, you will build the OOP domain model that powers your course catalog.
Build the OOP Domain Model
Your development environment is set up and the Spring Boot app starts successfully. Now it's time to design the data model that represents Columbia's course catalog.
Real university catalogs have different course formats: large lectures with hundreds of students, small seminars built around discussion, and hands-on labs with equipment. You'll model this hierarchy using Java's OOP tools: an abstract base class for shared behavior, an interface for search capability, and concrete subclasses for each format.
In this step, get ready to:
- Create a Searchable interface that defines search behavior.
- Build an abstract Course base class with shared fields and validation.
- Implement three concrete subclasses: Lecture, Seminar, and Lab.
Create the Searchable interface
Before building the course classes, you need to define what it means for a course to be "searchable." In Java, an interface is a contract that says "any class implementing me must provide these methods."
- In VS Code's Explorer sidebar, right-click on the coursecatalog folder inside src/main/java/com/columbia/coursecatalog/.
- Select New Folder and name it model.
- Right-click the new model folder, select New File, and name it Searchable.java.
- Paste the following code into Searchable.java:
package com.columbia.coursecatalog.model;
public interface Searchable {
boolean matchesQuery(String query);
}
What does this code do?
This interface declares a single method: matchesQuery(String query). Any class that implements Searchable must provide its own version of this method.
This is polymorphism in action. Later, the API can call matchesQuery() on any course type without knowing whether it's a Lecture, Seminar, or Lab.
- Save Searchable.java.
You won't see a visible change yet. The next chunk uses this interface in the Course base class.
Create the abstract Course base class
An abstract class holds shared fields and behavior that all course types have in common. You cannot create a Course object directly. You can only create specific types like Lecture or Lab that extend it.
- Right-click the model folder, select New File, and name it Course.java.
- Paste the following code into Course.java:
package com.columbia.coursecatalog.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "course_type")
public abstract class Course implements Searchable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Course name is required")
private String name;
@NotBlank(message = "Department is required")
private String department;
@Min(value = 1, message = "Credits must be at least 1")
@Max(value = 6, message = "Credits cannot exceed 6")
private int credits;
private String description;
private String instructor;
private int courseLevel;
public Course() {}
public Course(String name, String department, int credits, String description, String instructor, int courseLevel) {
this.name = name;
this.department = department;
this.credits = credits;
this.description = description;
this.instructor = instructor;
this.courseLevel = courseLevel;
}
public abstract String getFormat();
@Override
public boolean matchesQuery(String query) {
String lower = query.toLowerCase();
return name.toLowerCase().contains(lower)
|| department.toLowerCase().contains(lower)
|| (description != null && description.toLowerCase().contains(lower))
|| (instructor != null && instructor.toLowerCase().contains(lower));
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public int getCredits() { return credits; }
public void setCredits(int credits) { this.credits = credits; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getInstructor() { return instructor; }
public void setInstructor(String instructor) { this.instructor = instructor; }
public int getCourseLevel() { return courseLevel; }
public void setCourseLevel(int courseLevel) { this.courseLevel = courseLevel; }
}
What does this code do?
- @Entity tells JPA this class maps to a database table.
- @Inheritance(strategy = InheritanceType.SINGLE_TABLE) stores all course types (Lecture, Seminar, Lab) in one table. A course_type column distinguishes them.
- @NotBlank and @Min/@Max add validation rules. The API will reject invalid data automatically.
- getFormat() is abstract. Each subclass must provide its own implementation, returning "Lecture", "Seminar", or "Lab".
- matchesQuery() implements the Searchable interface by checking if the query appears in the name, department, description, or instructor fields.
Why single-table inheritance?
JPA offers three inheritance strategies: single table, joined tables, and table-per-class. Single table puts all subclass data in one table with a discriminator column.
For a course catalog where queries often span all course types, this avoids expensive JOIN operations. The trade-off is some nullable columns, which is fine for this use case.
- Save Course.java.
- Verify both files compile by running this in your terminal:
./mvnw compile
You should see BUILD SUCCESS at the end of the output.
Seeing a compilation error?
- Check that Searchable.java is in the same model package as Course.java. Both files need package com.columbia.coursecatalog.model; at the top.
- Make sure the folder path is exactly src/main/java/com/columbia/coursecatalog/model/. A misplaced folder breaks Java's package resolution.
- If you see "cannot find symbol" for jakarta.persistence, confirm your pom.xml still has the spring-boot-starter-data-jpa dependency.
Help me debug my Course.java compilation error
Implement the three concrete subclasses
Each course format has unique attributes beyond what Course provides. A Lecture tracks class size. A Seminar has a reading list. A Lab needs specific equipment. Each subclass extends Course and provides its own getFormat() implementation.
- Create a new file in the model folder called Lecture.java and paste the following code:
package com.columbia.coursecatalog.model;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
@Entity
@DiscriminatorValue("LECTURE")
public class Lecture extends Course {
private int classSize;
private boolean hasExam;
public Lecture() {}
public Lecture(String name, String department, int credits, String description, String instructor, int courseLevel, int classSize, boolean hasExam) {
super(name, department, credits, description, instructor, courseLevel);
this.classSize = classSize;
this.hasExam = hasExam;
}
@Override
public String getFormat() {
return "Lecture";
}
public int getClassSize() { return classSize; }
public void setClassSize(int classSize) { this.classSize = classSize; }
public boolean isHasExam() { return hasExam; }
public void setHasExam(boolean hasExam) { this.hasExam = hasExam; }
}
What does this code do?
- @DiscriminatorValue("LECTURE") tells JPA to write "LECTURE" in the course_type column for this subclass.
- extends Course means Lecture inherits all fields and methods from Course (name, department, credits, etc.).
- The super(...) call in the constructor passes shared fields up to the parent class.
- getFormat() returns "Lecture". This satisfies the abstract method requirement from Course.
- Save Lecture.java.
- Create a new file in the model folder called Seminar.java and paste the following code:
package com.columbia.coursecatalog.model;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
@Entity
@DiscriminatorValue("SEMINAR")
public class Seminar extends Course {
private int maxEnrollment;
private String readingList;
public Seminar() {}
public Seminar(String name, String department, int credits, String description, String instructor, int courseLevel, int maxEnrollment, String readingList) {
super(name, department, credits, description, instructor, courseLevel);
this.maxEnrollment = maxEnrollment;
this.readingList = readingList;
}
@Override
public String getFormat() {
return "Seminar";
}
public int getMaxEnrollment() { return maxEnrollment; }
public void setMaxEnrollment(int maxEnrollment) { this.maxEnrollment = maxEnrollment; }
public String getReadingList() { return readingList; }
public void setReadingList(String readingList) { this.readingList = readingList; }
}
What does this code do?
- Seminar follows the same pattern as Lecture. It extends Course and adds fields specific to seminars.
- maxEnrollment tracks the small cap size typical of seminar-style classes.
- readingList stores the required readings as a string.
- Save Seminar.java.
- Create a new file in the model folder called Lab.java and paste the following code:
package com.columbia.coursecatalog.model;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
@Entity
@DiscriminatorValue("LAB")
public class Lab extends Course {
private String equipmentRequired;
private int labHours;
public Lab() {}
public Lab(String name, String department, int credits, String description, String instructor, int courseLevel, String equipmentRequired, int labHours) {
super(name, department, credits, description, instructor, courseLevel);
this.equipmentRequired = equipmentRequired;
this.labHours = labHours;
}
@Override
public String getFormat() {
return "Lab";
}
public String getEquipmentRequired() { return equipmentRequired; }
public void setEquipmentRequired(String equipmentRequired) { this.equipmentRequired = equipmentRequired; }
public int getLabHours() { return labHours; }
public void setLabHours(int labHours) { this.labHours = labHours; }
}
What does this code do?
- Lab is the third subclass, adding equipmentRequired (what tools the lab uses) and labHours (weekly lab time).
- All three subclasses now form a complete inheritance hierarchy: Course is the abstract parent, and Lecture, Seminar, Lab are the concrete children.
- Save Lab.java.
- Verify the full domain model compiles and the app starts by running:
./mvnw spring-boot:run
You should see the Spring Boot banner followed by Started CourseCatalogApplication in your terminal output.
App won't start?
- If you see "cannot find symbol" errors, confirm all five files (Searchable.java, Course.java, Lecture.java, Seminar.java, Lab.java) are inside src/main/java/com/columbia/coursecatalog/model/.
- If you see a "duplicate class" error, make sure each file has exactly one class definition and the filename matches the class name.
- Press Ctrl+C in the terminal to stop the app when you're done verifying.
Help me debug my Spring Boot startup error
✔️ Awesome, I've got everything!
Great. Make sure you've saved all five files in the model folder, then press Ctrl+C to stop the running app.
ⓧ I'd like to double check the full code
Here is each file in the model package for comparison.
package com.columbia.coursecatalog.model;
public interface Searchable {
boolean matchesQuery(String query);
}
package com.columbia.coursecatalog.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "course_type")
public abstract class Course implements Searchable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Course name is required")
private String name;
@NotBlank(message = "Department is required")
private String department;
@Min(value = 1, message = "Credits must be at least 1")
@Max(value = 6, message = "Credits cannot exceed 6")
private int credits;
private String description;
private String instructor;
private int courseLevel;
public Course() {}
public Course(String name, String department, int credits, String description, String instructor, int courseLevel) {
this.name = name;
this.department = department;
this.credits = credits;
this.description = description;
this.instructor = instructor;
this.courseLevel = courseLevel;
}
public abstract String getFormat();
@Override
public boolean matchesQuery(String query) {
String lower = query.toLowerCase();
return name.toLowerCase().contains(lower)
|| department.toLowerCase().contains(lower)
|| (description != null && description.toLowerCase().contains(lower))
|| (instructor != null && instructor.toLowerCase().contains(lower));
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public int getCredits() { return credits; }
public void setCredits(int credits) { this.credits = credits; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getInstructor() { return instructor; }
public void setInstructor(String instructor) { this.instructor = instructor; }
public int getCourseLevel() { return courseLevel; }
public void setCourseLevel(int courseLevel) { this.courseLevel = courseLevel; }
}
package com.columbia.coursecatalog.model;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
@Entity
@DiscriminatorValue("LECTURE")
public class Lecture extends Course {
private int classSize;
private boolean hasExam;
public Lecture() {}
public Lecture(String name, String department, int credits, String description, String instructor, int courseLevel, int classSize, boolean hasExam) {
super(name, department, credits, description, instructor, courseLevel);
this.classSize = classSize;
this.hasExam = hasExam;
}
@Override
public String getFormat() {
return "Lecture";
}
public int getClassSize() { return classSize; }
public void setClassSize(int classSize) { this.classSize = classSize; }
public boolean isHasExam() { return hasExam; }
public void setHasExam(boolean hasExam) { this.hasExam = hasExam; }
}
package com.columbia.coursecatalog.model;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
@Entity
@DiscriminatorValue("SEMINAR")
public class Seminar extends Course {
private int maxEnrollment;
private String readingList;
public Seminar() {}
public Seminar(String name, String department, int credits, String description, String instructor, int courseLevel, int maxEnrollment, String readingList) {
super(name, department, credits, description, instructor, courseLevel);
this.maxEnrollment = maxEnrollment;
this.readingList = readingList;
}
@Override
public String getFormat() {
return "Seminar";
}
public int getMaxEnrollment() { return maxEnrollment; }
public void setMaxEnrollment(int maxEnrollment) { this.maxEnrollment = maxEnrollment; }
public String getReadingList() { return readingList; }
public void setReadingList(String readingList) { this.readingList = readingList; }
}
package com.columbia.coursecatalog.model;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
@Entity
@DiscriminatorValue("LAB")
public class Lab extends Course {
private String equipmentRequired;
private int labHours;
public Lab() {}
public Lab(String name, String department, int credits, String description, String instructor, int courseLevel, String equipmentRequired, int labHours) {
super(name, department, credits, description, instructor, courseLevel);
this.equipmentRequired = equipmentRequired;
this.labHours = labHours;
}
@Override
public String getFormat() {
return "Lab";
}
public String getEquipmentRequired() { return equipmentRequired; }
public void setEquipmentRequired(String equipmentRequired) { this.equipmentRequired = equipmentRequired; }
public int getLabHours() { return labHours; }
public void setLabHours(int labHours) { this.labHours = labHours; }
}
Your domain model is complete. Next up, you'll wire these classes to a database layer so Spring can persist and query course data automatically.
Create the Data Layer with Spring Data JPA
Your domain model defines the shape of your data. Now you need a way to actually store, retrieve, and query that data.
In this step, you'll connect your OOP model to an in-memory database using Spring Data JPA. You'll also build a data loader that reads real Columbia course data from a CSV file every time the application starts.
In this step, get ready to:
- Configure Spring Data JPA settings for the H2 database.
- Create a repository interface for database queries.
- Build a data loader that populates the database from a CSV file on startup.
Configure application properties for JPA
Your model classes already have JPA annotations like @Entity and @Inheritance. These tell Spring how to map your Java objects to database tables. Now you need to configure the database behaviour itself.
How do the JPA annotations on your model work?
@Entity marks a class as a database table. @Inheritance(strategy = InheritanceType.SINGLE_TABLE) stores all course types (Lecture, Seminar, Lab) in one table instead of three.
@DiscriminatorColumn(name = "course_type") adds a column that tells JPA which subclass each row belongs to. Each subclass's @DiscriminatorValue (like "LECTURE") labels its rows.
- In the VS Code Explorer sidebar, open src/main/resources/application.properties.
- Replace anything already in the file with this configuration:
server.port=${PORT:8080}
server.address=0.0.0.0
spring.application.name=course-catalog
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false
spring.h2-console.enabled=false
What does this configuration do?
- server.port=${PORT:8080} reads the PORT environment variable (for deployment later) and falls back to 8080 locally.
- server.address=0.0.0.0 binds to all network interfaces. This is required for cloud deployment.
- spring.jpa.hibernate.ddl-auto=create-drop recreates database tables from your entity classes every time the app starts. Perfect for development with an in-memory database.
- Save application.properties.
Create the CourseRepository interface
Spring Data JPA can generate database queries for you automatically. You define an interface with method names that follow a naming convention, and Spring implements the SQL behind the scenes.
- In the VS Code Explorer sidebar, right-click the src/main/java/com/columbia/coursecatalog folder.
- Click **New Folder** and name it repository.
- Right-click the new repository folder, click **New File**, and name it CourseRepository.java.
- Paste the following code into CourseRepository.java:
package com.columbia.coursecatalog.repository;
import com.columbia.coursecatalog.model.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CourseRepository extends JpaRepository<Course, Long> {
// Spring Data JPA generates SQL from method names automatically
List<Course> findByDepartment(String department);
List<Course> findByCredits(int credits);
List<Course> findByCourseLevelBetween(int min, int max);
}
What does this code do?
By extending JpaRepository<Course, Long>, Spring gives you full CRUD operations (save, find, delete) for free. No SQL needed.
The custom methods like findByDepartment use Spring's query derivation. It reads the method name, sees "findBy" + "Department", and generates a WHERE department = ? query automatically.
- Save CourseRepository.java.
Seeing red underlines in VS Code?
If VS Code shows import errors, it may still be indexing your project. Wait a few seconds for the Java extension to finish loading. If errors persist, check that your package declaration matches the folder path exactly.
Help me fix import errors in my CourseRepository
Build the DataLoader and course data
A repository is useless without data. You'll create a CSV file with 12 Columbia-style courses, then build a loader that parses it and saves each course to the database when the app starts.
- In the VS Code Explorer, right-click the src/main/resources folder.
- Click **New File** and name it courses.csv.
- Paste the following course data into courses.csv:
type,name,department,credits,description,instructor,courseLevel,extra1,extra2
LECTURE,Introduction to Computer Science,CS,3,Fundamentals of programming and computational thinking,Prof. Adam Smith,1000,200,true
LECTURE,Data Structures and Algorithms,CS,4,Advanced data structures and algorithm design,Prof. Maria Chen,3000,150,true
LECTURE,Machine Learning,CS,3,Introduction to ML models and applications,Prof. James Lee,4000,120,true
SEMINAR,Ethics in AI,CS,3,Exploring ethical implications of artificial intelligence,Prof. Sarah Johnson,4000,15,Weapons of Math Destruction; Algorithms of Oppression
SEMINAR,Advanced Topics in NLP,CS,3,Research seminar on natural language processing,Prof. David Park,6000,12,Speech and Language Processing
LAB,Robotics Laboratory,CS,4,Hands-on robotics programming and design,Prof. Emily Zhang,3000,Soldering stations and Arduino kits,4
LECTURE,Organic Chemistry,CHEM,4,Structure and reactivity of organic molecules,Prof. Robert Kim,2000,180,true
LAB,Organic Chemistry Lab,CHEM,2,Practical techniques in organic synthesis,Prof. Robert Kim,2000,Fume hoods and spectrophotometers,3
SEMINAR,Climate Change Policy,ENVS,3,Interdisciplinary seminar on climate policy solutions,Prof. Lisa Torres,4000,18,Drawdown; The Uninhabitable Earth
LECTURE,Microeconomics,ECON,3,Consumer theory and market structures,Prof. Michael Brown,1000,250,true
LECTURE,Econometrics,ECON,4,Statistical methods for economic data analysis,Prof. Anna White,3000,80,true
LAB,Physics Experimental Methods,PHYS,3,Measurement techniques and error analysis,Prof. Daniel Green,2000,Oscilloscopes and laser equipment,4
What is in this CSV?
Each row is a course with a type column (LECTURE, SEMINAR, or LAB) that maps to your three subclasses. The extra1 and extra2 columns hold subclass-specific data (class size and hasExam for lectures, max enrollment and reading list for seminars, equipment and lab hours for labs).
- Save courses.csv.
Now you need code that reads this CSV and creates the right Java object for each row. Spring Boot's CommandLineRunner interface is designed for exactly this. Any bean that implements it runs automatically after the application context is ready.
- In the VS Code Explorer, right-click the src/main/java/com/columbia/coursecatalog folder.
- Click **New Folder** and name it config.
- Right-click the new config folder, click **New File**, and name it DataLoader.java.
- Paste the following code into DataLoader.java:
package com.columbia.coursecatalog.config;
import com.columbia.coursecatalog.model.Lab;
import com.columbia.coursecatalog.model.Lecture;
import com.columbia.coursecatalog.model.Seminar;
import com.columbia.coursecatalog.repository.CourseRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@Component
public class DataLoader implements CommandLineRunner {
private final CourseRepository courseRepository;
public DataLoader(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
@Override
public void run(String... args) throws Exception {
// Skip loading if data already exists
if (courseRepository.count() > 0) return;
ClassPathResource resource = new ClassPathResource("courses.csv");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
String line = reader.readLine(); // skip header
while ((line = reader.readLine()) != null) {
String[] fields = line.split(",", -1);
if (fields.length < 8) continue;
// Parse shared fields from CSV columns
String type = fields[0].trim();
String name = fields[1].trim();
String department = fields[2].trim();
int credits = Integer.parseInt(fields[3].trim());
String description = fields[4].trim();
String instructor = fields[5].trim();
int courseLevel = Integer.parseInt(fields[6].trim());
// Create the correct subclass based on type column
switch (type.toUpperCase()) {
case "LECTURE" -> {
int classSize = fields.length > 7 ? Integer.parseInt(fields[7].trim()) : 100;
boolean hasExam = fields.length > 8 && Boolean.parseBoolean(fields[8].trim());
courseRepository.save(new Lecture(name, department, credits, description, instructor, courseLevel, classSize, hasExam));
}
case "SEMINAR" -> {
int maxEnrollment = fields.length > 7 ? Integer.parseInt(fields[7].trim()) : 20;
String readingList = fields.length > 8 ? fields[8].trim() : "";
courseRepository.save(new Seminar(name, department, credits, description, instructor, courseLevel, maxEnrollment, readingList));
}
case "LAB" -> {
String equipment = fields.length > 7 ? fields[7].trim() : "";
int labHours = fields.length > 8 ? Integer.parseInt(fields[8].trim()) : 3;
courseRepository.save(new Lab(name, department, credits, description, instructor, courseLevel, equipment, labHours));
}
}
}
}
System.out.println("Loaded " + courseRepository.count() + " courses from CSV.");
}
}
What does this code do?
- @Component registers this class as a Spring bean so it runs automatically on startup.
- The run() method reads courses.csv from the classpath, parses each line, and creates the appropriate subclass (Lecture, Seminar, or Lab) based on the type column.
- The switch statement demonstrates polymorphism in action. The same repository stores all three course types because they share the Course base class.
Why CommandLineRunner instead of a SQL file?
Spring Boot supports loading data from a data.sql file, but a CommandLineRunner gives you programmatic control. You can create type-specific objects, handle parsing logic, and add error handling. This is closer to real-world data ingestion patterns.
- Save DataLoader.java.
- In your terminal, stop the running app with **Ctrl+C**, then restart it by running:
./mvnw spring-boot:run
- Look at your terminal output. You should see Loaded 12 courses from CSV. printed near the end of the startup log.
Don't see the loaded message?
- If you see a compilation error, check that your DataLoader.java package declaration says package com.columbia.coursecatalog.config; and the file is inside the config folder.
- Make sure courses.csv is saved inside src/main/resources/ (not somewhere else). The ClassPathResource looks for files relative to this folder.
- If you see a number other than 12, double-check your CSV has exactly 12 data rows (plus the header row).
Help me debug my DataLoader startup issue
✔️ Awesome, I've got everything!
Great. Double check you've saved all your files before moving on.
ⓧ I'd like to double check the full code
Here are the three new files you created in this step. Compare against your code:
server.port=${PORT:8080}
server.address=0.0.0.0
spring.application.name=course-catalog
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false
spring.h2-console.enabled=false
package com.columbia.coursecatalog.repository;
import com.columbia.coursecatalog.model.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CourseRepository extends JpaRepository<Course, Long> {
List<Course> findByDepartment(String department);
List<Course> findByCredits(int credits);
List<Course> findByCourseLevelBetween(int min, int max);
}
package com.columbia.coursecatalog.config;
import com.columbia.coursecatalog.model.Lab;
import com.columbia.coursecatalog.model.Lecture;
import com.columbia.coursecatalog.model.Seminar;
import com.columbia.coursecatalog.repository.CourseRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@Component
public class DataLoader implements CommandLineRunner {
private final CourseRepository courseRepository;
public DataLoader(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
@Override
public void run(String... args) throws Exception {
if (courseRepository.count() > 0) return;
ClassPathResource resource = new ClassPathResource("courses.csv");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
String line = reader.readLine(); // skip header
while ((line = reader.readLine()) != null) {
String[] fields = line.split(",", -1);
if (fields.length < 8) continue;
String type = fields[0].trim();
String name = fields[1].trim();
String department = fields[2].trim();
int credits = Integer.parseInt(fields[3].trim());
String description = fields[4].trim();
String instructor = fields[5].trim();
int courseLevel = Integer.parseInt(fields[6].trim());
switch (type.toUpperCase()) {
case "LECTURE" -> {
int classSize = fields.length > 7 ? Integer.parseInt(fields[7].trim()) : 100;
boolean hasExam = fields.length > 8 && Boolean.parseBoolean(fields[8].trim());
courseRepository.save(new Lecture(name, department, credits, description, instructor, courseLevel, classSize, hasExam));
}
case "SEMINAR" -> {
int maxEnrollment = fields.length > 7 ? Integer.parseInt(fields[7].trim()) : 20;
String readingList = fields.length > 8 ? fields[8].trim() : "";
courseRepository.save(new Seminar(name, department, credits, description, instructor, courseLevel, maxEnrollment, readingList));
}
case "LAB" -> {
String equipment = fields.length > 7 ? fields[7].trim() : "";
int labHours = fields.length > 8 ? Integer.parseInt(fields[8].trim()) : 3;
courseRepository.save(new Lab(name, department, credits, description, instructor, courseLevel, equipment, labHours));
}
}
}
}
System.out.println("Loaded " + courseRepository.count() + " courses from CSV.");
}
}
type,name,department,credits,description,instructor,courseLevel,extra1,extra2
LECTURE,Introduction to Computer Science,CS,3,Fundamentals of programming and computational thinking,Prof. Adam Smith,1000,200,true
LECTURE,Data Structures and Algorithms,CS,4,Advanced data structures and algorithm design,Prof. Maria Chen,3000,150,true
LECTURE,Machine Learning,CS,3,Introduction to ML models and applications,Prof. James Lee,4000,120,true
SEMINAR,Ethics in AI,CS,3,Exploring ethical implications of artificial intelligence,Prof. Sarah Johnson,4000,15,Weapons of Math Destruction; Algorithms of Oppression
SEMINAR,Advanced Topics in NLP,CS,3,Research seminar on natural language processing,Prof. David Park,6000,12,Speech and Language Processing
LAB,Robotics Laboratory,CS,4,Hands-on robotics programming and design,Prof. Emily Zhang,3000,Soldering stations and Arduino kits,4
LECTURE,Organic Chemistry,CHEM,4,Structure and reactivity of organic molecules,Prof. Robert Kim,2000,180,true
LAB,Organic Chemistry Lab,CHEM,2,Practical techniques in organic synthesis,Prof. Robert Kim,2000,Fume hoods and spectrophotometers,3
SEMINAR,Climate Change Policy,ENVS,3,Interdisciplinary seminar on climate policy solutions,Prof. Lisa Torres,4000,18,Drawdown; The Uninhabitable Earth
LECTURE,Microeconomics,ECON,3,Consumer theory and market structures,Prof. Michael Brown,1000,250,true
LECTURE,Econometrics,ECON,4,Statistical methods for economic data analysis,Prof. Anna White,3000,80,true
LAB,Physics Experimental Methods,PHYS,3,Measurement techniques and error analysis,Prof. Daniel Green,2000,Oscilloscopes and laser equipment,4
Your data layer is complete. The database fills itself with 12 courses every time the app starts. Next up, you'll build the REST API endpoints that expose this data to the outside world.
Build the REST API Endpoints
Your data layer is ready. Courses load from the CSV, get stored in H2, and the repository can query them. But right now, the only way to access that data is from inside the Java application itself.
In this step, you'll expose your course data over HTTP by creating a service layer and a REST controller. By the end, anyone with a browser or curl can browse, filter, and search your course catalog.
In this step, get ready to:
- Create a service layer that encapsulates business logic for querying courses.
- Build a REST controller with GET endpoints for browsing, filtering, and searching.
- Test your live API with curl and verify proper HTTP status codes.
Create the course service
In Spring Boot, controllers should not talk directly to the repository. A service class sits between the controller and the repository, holding business logic like filtering by type or searching by keyword.
- In VS Code's Explorer sidebar, right-click the src/main/java/com/columbia/coursecatalog folder.
- Create a new folder called service.
- Inside the service folder, create a new file called CourseService.java.
- Paste the following code into CourseService.java:
package com.columbia.coursecatalog.service;
import com.columbia.coursecatalog.model.Course;
import com.columbia.coursecatalog.repository.CourseRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class CourseService {
private final CourseRepository courseRepository;
// Spring injects the repository automatically via constructor injection
public CourseService(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
// Return all courses from the database
public List<Course> getAllCourses() {
return courseRepository.findAll();
}
// Find a single course by its ID
public Optional<Course> getCourseById(Long id) {
return courseRepository.findById(id);
}
// Delegate to the repository's custom query method
public List<Course> getCoursesByDepartment(String department) {
return courseRepository.findByDepartment(department);
}
// Filter by course type using the polymorphic getFormat() method
public List<Course> getCoursesByType(String type) {
return courseRepository.findAll().stream()
.filter(c -> c.getFormat().equalsIgnoreCase(type))
.collect(Collectors.toList());
}
// Use the Searchable interface's matchesQuery for full-text search
public List<Course> searchCourses(String query) {
return courseRepository.findAll().stream()
.filter(c -> c.matchesQuery(query))
.collect(Collectors.toList());
}
// Save a new course to the database
public Course saveCourse(Course course) {
return courseRepository.save(course);
}
}
What does this code do?
The @Service annotation tells Spring Boot to manage this class as a bean, making it available for dependency injection into other classes.
- getCoursesByType() uses your polymorphic getFormat() method. Each subclass (Lecture, Seminar, Lab) returns its own format string, so filtering works without knowing the concrete class.
- searchCourses() leverages the Searchable interface you built earlier. Every course knows how to match itself against a query string.
- getCourseById() returns an Optional<Course> so the controller can decide whether to return 200 or 404.
- Save CourseService.java.
You won't see a change yet. The next substep creates the controller that uses this service.
✔️ 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
package com.columbia.coursecatalog.service;
import com.columbia.coursecatalog.model.Course;
import com.columbia.coursecatalog.repository.CourseRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class CourseService {
private final CourseRepository courseRepository;
public CourseService(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
public List<Course> getAllCourses() {
return courseRepository.findAll();
}
public Optional<Course> getCourseById(Long id) {
return courseRepository.findById(id);
}
public List<Course> getCoursesByDepartment(String department) {
return courseRepository.findByDepartment(department);
}
public List<Course> getCoursesByType(String type) {
return courseRepository.findAll().stream()
.filter(c -> c.getFormat().equalsIgnoreCase(type))
.collect(Collectors.toList());
}
public List<Course> searchCourses(String query) {
return courseRepository.findAll().stream()
.filter(c -> c.matchesQuery(query))
.collect(Collectors.toList());
}
public Course saveCourse(Course course) {
return courseRepository.save(course);
}
}
Create the REST controller
The controller is the entry point for HTTP requests. It maps URL paths to Java methods, delegates work to the service, and returns appropriate HTTP status codes.
- In the Explorer sidebar, right-click the src/main/java/com/columbia/coursecatalog folder.
- Create a new folder called controller.
- Inside the controller folder, create a new file called CourseController.java.
- Paste the following code into CourseController.java:
package com.columbia.coursecatalog.controller;
import com.columbia.coursecatalog.model.Course;
import com.columbia.coursecatalog.service.CourseService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/courses")
public class CourseController {
private final CourseService courseService;
// Spring injects the CourseService automatically
public CourseController(CourseService courseService) {
this.courseService = courseService;
}
// GET /api/courses with optional query params for filtering
@GetMapping
public List<Course> getAllCourses(
@RequestParam(required = false) String department,
@RequestParam(required = false) String type,
@RequestParam(required = false) String search) {
if (department != null) return courseService.getCoursesByDepartment(department);
if (type != null) return courseService.getCoursesByType(type);
if (search != null) return courseService.searchCourses(search);
return courseService.getAllCourses();
}
// GET /api/courses/{id} - returns 200 if found, 404 if not
@GetMapping("/{id}")
public ResponseEntity<Course> getCourseById(@PathVariable Long id) {
return courseService.getCourseById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// GET /api/courses/health - simple health check endpoint
@GetMapping("/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("Columbia Course Catalog API is running!");
}
}
What does this code do?
This controller exposes your course data as a REST API at the path /api/courses.
- @RestController tells Spring this class handles HTTP requests and returns JSON automatically.
- @RequestMapping("/api/courses") sets the base URL for all endpoints in this controller.
- @RequestParam(required = false) makes query parameters optional. If none are provided, all courses are returned.
- ResponseEntity lets you control the HTTP status code. The getCourseById method returns 200 when a course exists and 404 when it does not.
Why a service layer between the controller and repository?
The controller's job is to handle HTTP concerns (URL mapping, status codes, request parameters). The service's job is to hold business logic (filtering, searching, validation). Keeping them separate means you can change how filtering works without touching your HTTP layer.
This separation is standard in enterprise Java applications. Recruiters looking at your code will recognize this as a professional pattern.
- Save CourseController.java.
- Return to your terminal and restart the application by running:
./mvnw spring-boot:run
- You should see Loaded 12 courses from CSV. in the terminal output, confirming the app started successfully with your new controller registered.
Seeing a compilation error?
- Check that your service folder is inside com/columbia/coursecatalog, not at the project root. Spring Boot only scans packages under the main application class.
- Make sure the package declaration at the top of CourseService.java says package com.columbia.coursecatalog.service; exactly.
- Verify the imports in CourseController.java match what you pasted. Missing imports cause unresolved symbol errors.
Help me debug my Spring Boot compilation error.
✔️ 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
package com.columbia.coursecatalog.controller;
import com.columbia.coursecatalog.model.Course;
import com.columbia.coursecatalog.service.CourseService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/courses")
public class CourseController {
private final CourseService courseService;
public CourseController(CourseService courseService) {
this.courseService = courseService;
}
@GetMapping
public List<Course> getAllCourses(
@RequestParam(required = false) String department,
@RequestParam(required = false) String type,
@RequestParam(required = false) String search) {
if (department != null) return courseService.getCoursesByDepartment(department);
if (type != null) return courseService.getCoursesByType(type);
if (search != null) return courseService.searchCourses(search);
return courseService.getAllCourses();
}
@GetMapping("/{id}")
public ResponseEntity<Course> getCourseById(@PathVariable Long id) {
return courseService.getCourseById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("Columbia Course Catalog API is running!");
}
}
Test your endpoints with curl
Your API is running. Time to confirm each endpoint responds correctly with the right data and proper HTTP status codes.
- Open a second terminal window (keep your Spring Boot app running in the first one).
- Test the health endpoint by running:
curl http://localhost:8080/api/courses/health
- You should see: Columbia Course Catalog API is running!.
- Fetch all courses by running:
curl http://localhost:8080/api/courses
- You should see a JSON array with 12 course objects, each containing fields like name, department, and credits.
- Filter courses by department by running:
curl http://localhost:8080/api/courses?department=CS
- You should see only the 6 Computer Science courses.
- Filter by course type by running:
curl http://localhost:8080/api/courses?type=Seminar
- You should see only the 3 seminar courses.
- Search across all fields by running:
curl http://localhost:8080/api/courses?search=machine
- You should see the Machine Learning course returned. The matchesQuery method from your Searchable interface is doing the work here.
- Test the 404 response by requesting a course ID that does not exist:
curl -i http://localhost:8080/api/courses/999
- The -i flag shows response headers. You should see HTTP/1.1 404 in the output, confirming your controller returns the correct status for missing resources.
What HTTP status codes should I see?
- 200 for all successful GET requests (all courses, filtered results, single course by ID, health check).
- 404 when requesting a course ID that does not exist in the database.
These responses are handled by ResponseEntity in your controller. The .map(ResponseEntity::ok) call wraps a found course in a 200. The .orElse(ResponseEntity.notFound().build()) returns 404 when the Optional is empty.
Getting connection refused?
- Make sure your Spring Boot application is still running in the first terminal. If you stopped it, run ./mvnw spring-boot:run again.
- If you see an empty response from the /api/courses endpoint, check that courses.csv is in src/main/resources/ and contains data.
- On Windows, use PowerShell's Invoke-RestMethod if curl is not available: Invoke-RestMethod http://localhost:8080/api/courses/health.
Help me troubleshoot my Spring Boot API endpoints.
Your API is live locally and responding to requests. Next up, you'll test all endpoints thoroughly and prepare the project for deployment to a public URL.
Test Locally and Prepare for Deployment
Your API is fully built with endpoints, filtering, and polymorphic data models all working together.
Before deploying to the cloud, you need to confirm every endpoint returns the data you expect. Then you will push the code to GitHub so a cloud platform can pull it down and build it automatically.
In this step, get ready to:
- Test all API endpoints locally with curl.
- Verify the deployment configuration in your application properties.
- Push your project to GitHub.
Test all endpoints using curl
You will hit each endpoint from a second terminal window to confirm the API returns correct JSON responses. Your app should still be running from the previous step.
- In VS Code, click the + icon in the terminal panel to open a second terminal.
- Test the health endpoint by running this command:
curl http://localhost:8080/api/courses/health
You should see the text Columbia Course Catalog API is running! printed in your terminal.
- Test the all-courses endpoint by running:
curl http://localhost:8080/api/courses
You should see a JSON array with 12 course objects, each containing fields like name, department, and credits.
- Test filtering by department by running:
curl "http://localhost:8080/api/courses?department=CS"
You should see only the Computer Science courses (6 courses).
- Test filtering by type by running:
curl "http://localhost:8080/api/courses?type=Lecture"
You should see only Lecture courses (5 courses with classSize and hasExam fields).
- Test the search functionality by running:
curl "http://localhost:8080/api/courses?search=machine"
You should see a single result for "Machine Learning" since it matches the search query.
- Test fetching a single course by ID by running:
curl http://localhost:8080/api/courses/1
You should see the full JSON object for "Introduction to Computer Science".
- Test a course ID that does not exist by running:
curl -i http://localhost:8080/api/courses/999
What does the -i flag do?
The -i flag tells curl to include the HTTP response headers in the output. You should see HTTP/1.1 404 in the response, confirming your API returns the correct status code for missing resources.
Getting connection refused?
Make sure your Spring Boot app is still running in the other terminal window. If it stopped, restart it with ./mvnw spring-boot:run (macOS/Linux) or .\mvnw.cmd spring-boot:run (Windows).
Also check the URL starts with http:// not https://. The local server does not use SSL.
Help me troubleshoot
Verify your deployment configuration
Cloud platforms like Railway assign a dynamic port to your app at runtime. Your app needs to read that port from an environment variable instead of hardcoding 8080.
- In VS Code, open src/main/resources/application.properties.
- Confirm it contains the following two lines:
server.port=${PORT:8080}
server.address=0.0.0.0
Why are these settings needed?
Railway provides a PORT environment variable automatically. The ${PORT:8080} syntax tells Spring Boot to use that variable if it exists, or fall back to 8080 locally.
The server.address=0.0.0.0 line makes your app listen on all network interfaces. Without it, Spring Boot defaults to localhost only, and Railway cannot route traffic to your app.
Initialize Git and push to GitHub
Railway pulls your code directly from a GitHub repository. You need to create a repo and push your project there.
- Stop your running Spring Boot app by pressing Ctrl+C in the terminal where it is running.
- Initialize a Git repository in your project folder by running:
git init
- Stage all files and make your first commit by running these commands:
git add .
git commit -m "Initial commit: Columbia Course Catalog API"
What about .gitignore?
Spring Initializr generated a .gitignore file in your project root when you downloaded the starter. It already excludes build artifacts like the target/ folder and IDE files. No extra setup needed.
- Go to github.com in your browser.
- Click the + icon in the top-right corner, then select New repository.
- Enter columbia-course-catalog as the repository name.
- Leave the repository set to Public.
- Do NOT check any of the initialization boxes (README, .gitignore, license). Your project already has these files locally.
- Click Create repository.
GitHub shows a quick setup page with commands for an existing repository. You will use those commands now.
- Connect your local repo to GitHub and push by running these commands:
git remote add origin [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/columbia-course-catalog.git"]]
git branch -M main
git push -u origin main
You should see output ending with a line like Branch 'main' set up to track remote branch 'main' from 'origin'.
- Refresh your GitHub repository page in the browser.
- You should see all your project files listed, including pom.xml, src/, and .gitignore.
Push failed with authentication error?
GitHub no longer accepts passwords for Git operations. You need either a Personal Access Token or SSH key configured.
To create a token: go to Settings, then Developer settings, then Personal access tokens, then Tokens (classic), then Generate new token. Select the repo scope and use the generated token as your password when prompted.
Help me fix GitHub authentication
Your API is tested and your code is safely stored on GitHub. Next up, you will deploy this API to Railway so anyone on the internet can access it.
Deploy to Railway
Your API runs perfectly on your machine. The problem is that nobody else can access it.
In this step, you will deploy your Spring Boot application to Railway, a cloud platform that builds and hosts your app from a GitHub repository. Once deployed, anyone with the public URL can hit your endpoints.
In this step, get ready to:
- Sign up for Railway and connect your GitHub account.
- Create a Railway project from your GitHub repository.
- Generate a public domain and verify your live API.
Sign up for Railway and connect your GitHub account
Railway offers a free trial with a one-time $5 credit grant. No credit card is required to get started.
- Open your browser and navigate to railway.com.
- Click the sign-up button and choose to sign up with your GitHub account.
- Authorize Railway to access your GitHub repositories when prompted.
What is Railway?
Railway is a cloud hosting platform that connects directly to your GitHub repository. Every time you push code, Railway automatically rebuilds and redeploys your application. It auto-detects Maven projects, builds with mvn clean install, and runs the resulting JAR file.
Why Railway instead of Render?
Railway auto-detects Java and Maven projects without needing a Dockerfile. Render requires a Dockerfile for Java apps. For a first deployment, Railway's simpler workflow means fewer moving parts and faster time to a live URL.
Create a new project from your GitHub repository
With your Railway account linked to GitHub, you can deploy your course catalog API in a few clicks.
- In the Railway dashboard, click New Project.
- Select Deploy from GitHub repo.
- Search for your course catalog repository and click on it.
- Click Deploy Now to start the build immediately.
Railway will create a new project and kick off the initial deployment. You will land on your Project Canvas where you can watch the build progress.
- Click on your service tile in the canvas to open the service panel.
- Select View logs and wait for the build to complete.
What happens during the build?
Railway detects pom.xml in your repository and recognizes it as a Java/Maven project. It runs mvn clean install to compile your code and package it into a JAR. Then it starts your application using that JAR.
The server.port=${PORT:8080} property you set earlier tells Spring Boot to read the PORT environment variable that Railway provides automatically. The server.address=0.0.0.0 setting ensures your app listens on all network interfaces so Railway can route traffic to it.
✔️ I see the build succeeded in the logs
Your app is deployed and running on Railway. Time to make it publicly accessible.
ⓧ I see a build failure or 502 error
The most common failure for Spring Boot on Railway is a port binding issue. Verify these two lines exist in your src/main/resources/application.properties file:
server.port=${PORT:8080}
server.address=0.0.0.0
- If you made changes, commit and push to GitHub. Railway will automatically redeploy.
- Check the deploy logs for specific error messages. A Connection refused or 502 error usually means your app is binding to localhost instead of 0.0.0.0.
ⓧ Railway can't detect my repository
- Make sure your repository is pushed to GitHub and is not empty.
- Check that you authorized Railway to access your repositories during the GitHub linking step.
- If your repository is in an organization, you may need to grant Railway access to that organization in your GitHub settings under Applications.
Generate a public domain and verify your live API
By default, Railway services are not publicly accessible. You need to generate a domain to expose your API to the internet.
- Click on your service in the Project Canvas.
- Go to the Settings tab.
- Scroll down to Networking and find the Public Networking section.
- Click Generate Domain.
Railway generates a URL like yourapp.up.railway.app with automatic SSL included.
- Copy your new Railway domain from the dashboard.
- Test your live API by running this command in your terminal (replace the domain with yours):
curl [[RAILWAY_URL="your-app-name.up.railway.app"]]/api/courses/health
You should see the response: Columbia Course Catalog API is running!.
- Now test the courses endpoint to confirm all 12 courses are loaded:
curl [[RAILWAY_URL="your-app-name.up.railway.app"]]/api/courses
You should see a JSON array containing 12 course objects, including lectures, seminars, and labs from across all departments.
Seeing an empty array or a 502 error?
- An empty array means the CSV data did not load. Check the deploy logs for errors related to courses.csv. Make sure the file is committed to src/main/resources/courses.csv in your GitHub repository.
- A 502 error means Railway cannot reach your application. Verify server.address=0.0.0.0 is in your application.properties file, commit, and push.
- Wait 1-2 minutes after deployment. The first cold start takes longer because Maven downloads dependencies during the build.
Help me troubleshoot my Railway deployment
What about the Hobby plan cost?
Railway's free trial includes a one-time $5 credit. After that, the Hobby plan costs $5/month and includes $5 of resource usage. A lightweight Spring Boot API with an in-memory H2 database uses minimal resources and stays well within the included credit.
Your course catalog API is live on the internet. Anyone with the URL can browse Columbia courses, filter by department, and search by keyword. This is a fully deployed Java Spring Boot application showcasing OOP design patterns and clean REST endpoints.
Secret mission
Implement the Strategy Pattern for Course Recommendations
Your API serves courses, but what if a recruiter could ask for recommendations using different algorithms? Add a Strategy pattern that lets the caller choose between department-based, difficulty-based, and prerequisite-chain recommendations. Same endpoint, different algorithms, swapped at runtime.
Clean Up Your Resources
Clean Up Your Resources
Your Railway service runs on the Hobby plan ($5/month, which includes $5 of usage credit). A lightweight Spring Boot API with an in-memory H2 database will stay well within that included usage. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Cost warning
Your Railway service is billed at $5/month while active. If you leave it running, this is the only cost. If you want to stop charges entirely, follow the Pause or Delete instructions below.
Resources you used:
- Railway service (cloud deployment hosting your API).
- Local project folder (course-catalog on your Desktop).
- GitHub repository (stores your project code).
Keep everything running
No action needed. Choose this if you want to keep your API live as a portfolio piece that recruiters can interact with.
- Your API stays live at its .up.railway.app URL.
- Railway bills $5/month for the Hobby plan. Your lightweight Spring Boot API with H2 will stay within the included $5 usage credit.
- Your local course-catalog folder and GitHub repo remain available for future edits.
Pause - I'll come back to this later
Remove the active deployment to stop billing while keeping the service ready to redeploy later.
- Open your project in the Railway dashboard.
- Click on your service tile to open the deployment view.
- Click the three dots at the end of your active deployment and select Remove.
- This stops your API and halts further usage charges. Your service configuration stays intact, so you can redeploy later by pushing a new commit to GitHub.
Delete - I don't want to use this again
Remove all project resources and start fresh. This cannot be undone.
- Delete your Railway project:
- Open your project in the Railway dashboard.
- Click the Settings button at the top right of the project canvas.
- Navigate to the Danger tab.
- Click Delete Project and confirm. This deletes all services, environments, and deployments.
- Delete your GitHub repository:
- Go to your repository on GitHub.
- Click Settings (the tab at the top of the repo page).
- Scroll to the bottom and click Delete this repository in the Danger Zone section. Confirm the deletion.
- Delete your local project folder by running this command:
macOS
rm -rf ~/Desktop/course-catalog
Windows
Remove-Item -Recurse -Force ~\Desktop\course-catalog
Nice Work!
Nice Work!
You did it. You went from zero Java experience to a live, deployed REST API that any recruiter can interact with immediately.
You've learned how to:
- Build a Java Spring Boot REST API from scratch with filtering, validation, and proper HTTP status codes.
- Design an OOP domain model using abstract classes, interfaces, and polymorphism to represent real-world course types.
- Load real data from CSV using Spring Boot's CommandLineRunner and deploy a live API to a public URL on Railway.
- Implemented the Strategy pattern with swappable recommendation algorithms that a recruiter can invoke via the API.
Ready to quiz yourself?