It is time to write code. In this lesson you will generate the ciclourbana project with Spring Initializr —both from the web and from the command line—, open it in your IDE and create the platform's first real endpoint: GET /api/v1/stations, which will return the list of stations in the Ribalta network. You will then run it in three different ways, exercise it with curl and package it into an executable JAR that runs with java -jar without installing any server. By the end you will have an API working end to end.
Contents
- Spring Initializr: what it is and how to use it
- Generating the project from the web
- Generating the project from the command line
- Importing the project into the IDE
- The generated startup class
- The first endpoint:
GET /api/v1/stations - Running the application
- Testing the endpoint with curl
- Packaging and running the JAR
- Spring Boot DevTools and automatic restart
- Common Mistakes and Tips
- Exercises
- Spring Initializr: what it is and how to use it
Spring Initializr (start.spring.io) is the official Spring Boot project generator. You give it four details and a list of dependencies, and it hands you back a ZIP containing:
- A
pom.xmlalready configured with the Spring Boot parent and the dependencies you chose. - The main application class, annotated and ready to start.
- A basic test class.
- The Maven wrapper (
mvnw,mvnw.cmd,.mvn/). - A suitable
.gitignoreand an emptyapplication.properties.
It is the right way to start any Spring Boot project. Creating the structure by hand is possible, but it is unnecessary work and error-prone.
- Generating the project from the web
Open https://start.spring.io and fill the form in with these exact values, which we will use for the whole course:
| Field | Value | Why |
|---|---|---|
| Project | Maven | Decided in lesson 01-02 |
| Language | Java | — |
| Spring Boot | The latest stable 3.x on offer | Avoid versions marked SNAPSHOT or M1 (pre-releases) |
| Group | com.ciclourbana |
Identifies the organisation |
| Artifact | ciclourbana |
Name of the project and of the JAR |
| Name | ciclourbana |
Generates the CiclourbanaApplication class |
| Description | Management of the Ribalta electric bike network |
pom.xml metadata |
| Package name | com.ciclourbana |
Important: the project's root package |
| Packaging | Jar | Executable JAR with an embedded server |
| Java | 21 | The course's LTS version |
In the Dependencies panel, click "ADD DEPENDENCIES" and add exactly two:
- Spring Web — Spring MVC, Jackson for JSON and an embedded Tomcat. This is what turns the project into a web application.
- Spring Boot DevTools — automatic restart on code changes and cache deactivation during development.
Click GENERATE. ciclourbana.zip downloads. Unzip it wherever you keep your projects.
One tip before downloading: the EXPLORE button shows the project's contents without downloading it, and SHARE produces a URL that reproduces exactly that configuration. It is very handy for sharing a starting point with a colleague.
- Generating the project from the command line
Spring Initializr is also an HTTP API. That lets you generate the project without opening a browser and —more importantly— keep the command in the team's documentation so anyone can reproduce the same starting point.
curl https://start.spring.io/starter.zip \
-d type=maven-project \
-d language=java \
-d bootVersion=3.3.5 \
-d groupId=com.ciclourbana \
-d artifactId=ciclourbana \
-d name=ciclourbana \
-d description="Management of the Ribalta electric bike network" \
-d packageName=com.ciclourbana \
-d packaging=jar \
-d javaVersion=21 \
-d dependencies=web,devtools \
-o ciclourbana.zip
# Unzip into a folder named after the project
unzip ciclourbana.zip -d ciclourbana
cd ciclourbanaA breakdown of the curl options:
-d key=valuesends a form parameter. Using-dmakes curl issue a POST request automatically.dependencies=web,devtoolsis the comma-separated list of dependency identifiers.webcorresponds to "Spring Web" anddevtoolsto "Spring Boot DevTools".-o ciclourbana.zipsaves the response to a file instead of dumping it to the terminal.
Two very useful exploratory commands:
# See every available option: versions, dependencies and identifiers
curl https://start.spring.io
# Query the metadata as JSON (available Boot versions, and so on)
curl -H "Accept: application/json" https://start.spring.io | head -40If you omit bootVersion, Initializr uses the latest stable release, which is usually what you want.
- Importing the project into the IDE
The project is a standard Maven project, so any IDE understands it.
IntelliJ IDEA: File → Open and select the ciclourbana folder (not the bare pom.xml, although that works too). IntelliJ detects the pom.xml, indexes the project and downloads the dependencies. You will see a progress bar at the bottom right; wait for it to finish.
Eclipse / STS: File → Import → Maven → Existing Maven Projects, select the root folder and click Finish.
VS Code: File → Open Folder on ciclourbana. The Java extension starts on its own and a Spring Boot Dashboard view appears in the sidebar.
In all three cases, the first import takes a while: Maven downloads the Spring dependencies into ~/.m2/repository. That is a few hundred megabytes the first time and practically instantaneous from then on.
- The generated startup class
Open src/main/java/com/ciclourbana/CiclourbanaApplication.java. It contains this:
package com.ciclourbana;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CiclourbanaApplication {
public static void main(String[] args) {
SpringApplication.run(CiclourbanaApplication.class, args);
}
}It is ten lines, but every one of them matters:
@SpringBootApplicationis the annotation that switches everything on. It bundles three annotations we will study in module 2: it enables autoconfiguration, marks the class as a source of configuration and activates component scanning starting from this package.mainis an ordinary Javamainmethod. A Spring Boot application is, literally, a Java program that starts from itsmain. There is no external container invoking it.SpringApplication.run(...)creates the Spring context, registers the beans and —on detecting Spring Web— starts Tomcat. It returns the running context. What happens inside, step by step, is the subject of lesson 01-05.- The
argsargument is propagated: it lets you pass parameters from the command line, such as--server.port=9090.
For consistency with the rest of the course, rename the class to CicloUrbanaApplication (with a capital U). Use the IDE's rename refactoring (Shift+F6 in IntelliJ) so that it updates the test file as well.
- The first endpoint:
GET /api/v1/stations
GET /api/v1/stationsWe are going to expose Ribalta's stations. We need two pieces: a type representing a station and a controller that returns it as JSON.
The Station record
Java 21 offers records, ideal for immutable data. Create the file src/main/java/com/ciclourbana/stations/Station.java:
package com.ciclourbana.stations;
/**
* Represents a docking station in the Ribalta network.
* For now it is a plain in-memory record; in module 4 it
* will become a JPA entity persisted in the database.
*/
public record Station(
Long id,
String name,
String address,
int capacity,
double latitude,
double longitude
) {
}Why a record and not a regular class:
- It automatically generates the constructor, the accessor methods (
name(),capacity()...),equals,hashCodeandtoString. - It is immutable: once created it does not change, which avoids errors in concurrent environments.
- Jackson, the library Spring Boot uses to turn objects into JSON, has understood records for several versions now. Each component becomes a JSON property with the same name.
The controller
Create src/main/java/com/ciclourbana/stations/StationController.java:
package com.ciclourbana.stations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/v1/stations")
public class StationController {
// Fixed in-memory data. In module 4 it will come from PostgreSQL.
private static final List<Station> STATIONS = List.of(
new Station(1L, "Main Square", "Main Square 1", 24, 40.4168, -3.7038),
new Station(2L, "North Station", "Station Avenue 3", 30, 40.4290, -3.7020),
new Station(3L, "River Park", "Riverside Walk 12", 18, 40.4105, -3.6950),
new Station(4L, "University", "South Campus, Gate B", 36, 40.4402, -3.7255)
);
@GetMapping
public List<Station> listStations() {
return STATIONS;
}
}What each annotation does, at just the right depth for this lesson:
@RestControllermarks the class as a Spring component that handles HTTP requests and whose methods return the response body directly (not the name of an HTML view). Spring finds the class while scanning the package and registers it.@RequestMapping("/api/v1/stations")sets the base path for every method in the class. That way the prefix is written only once.@GetMappingwith no arguments maps the method toGETon the exact base path, that is,GET /api/v1/stations.- The method returns a
List<Station>. Thanks to Jackson, Spring Boot converts it automatically into a JSON array and sets theContent-Type: application/jsonheader. Nothing has to be serialised by hand.
The fine detail of these annotations, content negotiation, status codes and parameter handling is covered in depth in module 3. Here it is enough that it works.
How the pieces fit together
sequenceDiagram
participant C as curl
participant T as Embedded Tomcat
participant D as DispatcherServlet
participant E as StationController
participant J as Jackson
C->>T: GET /api/v1/stations
T->>D: HTTP request
D->>D: Find the method that handles the path
D->>E: listStations()
E-->>D: List of Station
D->>J: Serialise to JSON
J-->>D: JSON array of stations
D-->>T: 200 OK + JSON body
T-->>C: HTTP response
- Running the application
There are three ways, and it is worth knowing all three.
With the Maven plugin
This is the canonical way from the terminal:
spring-boot:run compiles the project and starts the application in the same JVM as Maven, with no need to package it. It is the most convenient option during development.
The output ends with something along these lines:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
2026-08-31T10:22:41.118+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] c.c.CicloUrbanaApplication : Starting CicloUrbanaApplication using Java 21.0.5
2026-08-31T10:22:42.905+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-08-31T10:22:42.918+02:00 INFO 18422 --- [ciclourbana] [ restartedMain] c.c.CicloUrbanaApplication : Started CicloUrbanaApplication in 2.174 secondsThe three key lines: the Java version in use, the port 8080 Tomcat is listening on, and the startup time. We will read this log line by line in lesson 01-05.
To stop the application, Ctrl+C.
From the IDE
Open CicloUrbanaApplication and run the main method like any Java program: the green triangle in IntelliJ, Run As → Java Application in Eclipse, or Run on the main in VS Code.
The advantage of the IDE is the debugger: you can set a breakpoint in listStations() and watch execution stop when the request arrives.
STS and VS Code also offer a Spring Boot Dashboard to start, stop and restart without hunting for the class.
From the packaged JAR
This is the production route, and we cover it in section 9.
- Testing the endpoint with curl
With the application running, open another terminal:
Response:
[{"id":1,"name":"Main Square","address":"Main Square 1","capacity":24,"latitude":40.4168,"longitude":-3.7038},{"id":2,"name":"North Station",...}]To see it readably and with the headers:
# With headers and status code
curl -i http://localhost:8080/api/v1/stations
# Pretty-printed with jq (if you have it installed)
curl -s http://localhost:8080/api/v1/stations | jqThe response with -i starts like this:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 31 Aug 2026 08:24:11 GMTNotice that you have not written a single line about JSON or about the 200 status code. Spring Boot inferred the Content-Type from the return type and applied 200 by default. That is autoconfiguration at work.
Try a non-existent path too, to see the default error behaviour:
curl -i http://localhost:8080/api/v1/bikes
# HTTP/1.1 404
# {"timestamp":"...","status":404,"error":"Not Found","path":"/api/v1/bikes"}Spring Boot returns a structured error body without you configuring anything. Customising it is the goal of lesson 03-06.
- Packaging and running the JAR
Building the artefact
This command cleans the target folder, compiles, runs the tests and produces the JAR. When it finishes:
ls -lh target/*.jar
# -rw-r--r-- 1 user user 22M ciclourbana-0.0.1-SNAPSHOT.jar
# -rw-r--r-- 1 user user 12K ciclourbana-0.0.1-SNAPSHOT.jar.originalTwo files appear, and the difference explains everything:
.jar.original(12 KB) is the JAR Maven produces in the standard way: only your compiled classes..jar(22 MB) is the fat jar, or uber jar: your classes plus every dependency —Spring, Jackson, Tomcat— plus a special loader.
The spring-boot-maven-plugin is what performs that transformation during the package phase.
What an executable JAR and a fat jar are
An ordinary JAR is a ZIP of .class files. To run it you need all its dependencies on the classpath, which forces you to ship dozens of files or to install an application server.
Spring Boot solves this with an ingenious format:
ciclourbana-0.0.1-SNAPSHOT.jar
├── META-INF/
│ └── MANIFEST.MF ← declares Main-Class and Start-Class
├── org/springframework/boot/loader/ ← the Spring Boot loader
├── BOOT-INF/
│ ├── classes/ ← YOUR compiled classes
│ │ └── com/ciclourbana/...
│ └── lib/ ← ALL the dependencies, as nested .jar files
│ ├── spring-web-6.x.jar
│ ├── tomcat-embed-core-10.x.jar
│ └── jackson-databind-2.x.jar
└── ...You can check it yourself:
unzip -l target/ciclourbana-0.0.1-SNAPSHOT.jar | head -20
unzip -p target/ciclourbana-0.0.1-SNAPSHOT.jar META-INF/MANIFEST.MFThe manifest reveals the trick:
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.ciclourbana.CicloUrbanaApplicationWhen you run the JAR, Java invokes Spring Boot's JarLauncher, not your class. That launcher installs a class loader capable of reading nested JARs inside BOOT-INF/lib —something standard Java cannot do— and only then calls your Start-Class.
The practical consequence is enormous: a single file contains the complete application, server included. That is what gets copied onto a server, put into a Docker image or uploaded to a cloud platform.
Running the JAR
It starts up exactly as it did with spring-boot:run. Check that it still responds:
You can pass properties on the command line:
# Change the port without recompiling
java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar --server.port=9090
# Cap the JVM's memory (useful in containers)
java -Xmx256m -jar target/ciclourbana-0.0.1-SNAPSHOT.jarNote the difference: -Xmx256m goes before -jar because it is a JVM option, whereas --server.port=9090 goes after because it is an application argument.
- Spring Boot DevTools and automatic restart
When we generated the project we added spring-boot-devtools:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>The <optional>true</optional> and <scope>runtime</scope> attributes mean DevTools is not propagated to anyone depending on your project and that it switches itself off automatically when the application runs from a fat jar. In other words: it helps during development and vanishes in production without you doing a thing.
What it brings:
| Feature | Effect |
|---|---|
| Automatic restart | On detecting recompiled classes, it restarts the Spring context in under a second |
| LiveReload | Refreshes the browser automatically when static resources change |
| Caches disabled | Turns off template caches so changes are visible instantly |
| Development properties | Applies sensible defaults for development |
How the fast restart works
DevTools uses two class loaders: a base one with the dependencies (Spring, Tomcat, Jackson), which never change, and a restart one with your classes, which do. On restart it only discards and reloads the second one. That is why it takes tenths of a second instead of the two seconds of a full startup.
That is also why the main thread appears in the log as restartedMain instead of main.
Triggering a restart
The restart is triggered when the .class files in target/classes change, not when you save the .java. So a recompile is required:
- IntelliJ:
Ctrl+F9(Build Project), or enable Settings → Build → Compiler → Build project automatically. - Eclipse/STS: compiles on save by default, so
Ctrl+Sis enough. - VS Code: also compiles on save.
Try it. With the application running via ./mvnw spring-boot:run, add a fifth station:
Recompile and watch the log:
2026-08-31T10:31:08.442+02:00 INFO --- [ restartedMain] c.c.CicloUrbanaApplication : Started CicloUrbanaApplication in 0.612 secondsNow curl returns five stations without you having stopped anything.
If the restart gets in your way, you can switch it off without removing the dependency:
Common Mistakes and Tips
Web server failed to start. Port 8080 was already in use.You have another instance running, which is very typical if you forgot to stop the one in the IDE before launching./mvnw spring-boot:run. Find the process withlsof -i :8080(Linux/macOS) ornetstat -ano | findstr :8080(Windows), or start on a different port with--server.port=9090.- The endpoint returns 404 even though the code looks right. It is almost always because the controller sits outside the root package
com.ciclourbana. Component scanning only looks at that package and its subpackages; we will see this in lesson 01-04. - The application starts and exits immediately.
spring-boot-starter-webis missing. Without a web dependency there is no server to keep the process alive, somainfinishes and the JVM shuts down. - DevTools restarts nothing. You are saving the
.javabut it is not being recompiled. Enable automatic building in the IDE or compile by hand. - Running the
.jar.originalby mistake. It givesno main manifest attribute. The right file is the large.jar, with no suffix. - Tip — the JAR is self-contained, not standalone. It carries every Java dependency, but it still needs a JVM installed wherever it runs. That is why Spring Boot Docker images start from an image with a JDK or JRE (module 7).
- Tip — initialise Git now. The project comes with a correct
.gitignore. Withgit init && git add . && git commit -m "Initial CicloUrbana project"you will have a safe point to return to from day one.
Exercises
Exercise 1
Generate the ciclourbana project from the command line with curl, run it and use curl to check that GET /api/v1/stations returns Ribalta's four stations. Note down the startup time shown in the log.
Exercise 2
Add a second endpoint GET /api/v1/stations/summary to StationController that returns a JSON object with the total number of stations and the total capacity of the network. Use a record NetworkSummary.
Exercise 3
Package the application, inspect the contents of the fat jar to locate your compiled classes and the embedded Tomcat JAR, and run it on port 9090, checking that it responds.
Solutions
Solution 1
curl https://start.spring.io/starter.zip \
-d type=maven-project -d language=java \
-d groupId=com.ciclourbana -d artifactId=ciclourbana \
-d name=ciclourbana -d packageName=com.ciclourbana \
-d packaging=jar -d javaVersion=21 \
-d dependencies=web,devtools \
-o ciclourbana.zip
unzip ciclourbana.zip -d ciclourbana
cd ciclourbana
chmod +x mvnwAfter creating Station and StationController as described in the lesson:
In another terminal:
curl -s http://localhost:8080/api/v1/stations | jq 'length'
# 4
curl -s http://localhost:8080/api/v1/stations | jq '.[].name'
# "Main Square"
# "North Station"
# "River Park"
# "University"The startup time appears on the Started CicloUrbanaApplication in X seconds line. On a normal machine it is between 1.5 and 3 seconds.
Solution 2
First the summary record, in the same package:
package com.ciclourbana.stations;
/**
* Aggregated summary of the Ribalta station network.
*/
public record NetworkSummary(
int totalStations,
int totalCapacity
) {
}And the new method in the controller:
@GetMapping("/summary")
public NetworkSummary getSummary() {
int totalCapacity = STATIONS.stream()
.mapToInt(Station::capacity) // reference to the record's accessor
.sum();
return new NetworkSummary(STATIONS.size(), totalCapacity);
}A detailed explanation:
@GetMapping("/summary")is concatenated with the class-level@RequestMapping, producingGET /api/v1/stations/summary.Station::capacityis a method reference. In a record, the accessor has the same name as the component (capacity(), notgetCapacity()).mapToInt(...).sum()turns the stream into anIntStreamand adds up the values, avoiding autoboxing.- Returning an object (not a list) makes Jackson produce a JSON object rather than an array.
Checking it:
curl -s http://localhost:8080/api/v1/stations/summary | jq
# {
# "totalStations": 4,
# "totalCapacity": 108
# }Solution 3
./mvnw clean package
# Locate your own classes inside the fat jar
unzip -l target/ciclourbana-0.0.1-SNAPSHOT.jar | grep "com/ciclourbana"
# BOOT-INF/classes/com/ciclourbana/CicloUrbanaApplication.class
# BOOT-INF/classes/com/ciclourbana/stations/Station.class
# BOOT-INF/classes/com/ciclourbana/stations/StationController.class
# Locate the embedded Tomcat
unzip -l target/ciclourbana-0.0.1-SNAPSHOT.jar | grep tomcat
# BOOT-INF/lib/tomcat-embed-core-10.1.x.jar
# BOOT-INF/lib/tomcat-embed-el-10.1.x.jar
# BOOT-INF/lib/tomcat-embed-websocket-10.1.x.jar
# Run it on port 9090
java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar --server.port=9090Verification in another terminal:
The takeaway from this exercise: your classes live in BOOT-INF/classes and the dependencies in BOOT-INF/lib as nested JARs. That separation is exactly what allows Spring Boot to build efficient layered Docker images, something we will take advantage of in module 7.
Conclusion
CicloUrbana is up and running. You have generated the project with Spring Initializr from the web and from the terminal, understood the ten lines of the startup class, created the Station record and the StationController that exposes GET /api/v1/stations with Ribalta's four stations, and run it with Maven, with the IDE and as a self-contained fat jar via java -jar. You also know why that JAR weighs 22 MB and how the JarLauncher loads the nested JARs in BOOT-INF/lib.
In the next lesson, Understanding the Project Structure, we will open the box: we will walk through the generated directory tree, read the pom.xml line by line, understand exactly what a starter is and why the root package determines which components Spring finds, and design the package layout CicloUrbana will keep for the whole course.
Spring Boot Course
Module 1: Introduction to Spring Boot
- What Is Spring Boot?
- Setting Up Your Development Environment
- Building Your First Spring Boot Application
- Understanding the Project Structure
- Application Startup and Lifecycle
Module 2: Spring Boot Core Concepts
- Spring Boot Annotations
- Dependency Injection in Spring Boot
- Bean Scope and Lifecycle
- Spring Boot Configuration
- Spring Boot Properties
- Auto-Configuration and Starters from the Inside
Module 3: Building RESTful Web Services
- Introduction to RESTful Web Services
- Creating REST Controllers
- Handling HTTP Methods
- Validating Input Data
- DTOs and Mapping Between Layers
- Exception Handling in REST
- Documenting the API with OpenAPI
Module 4: Data Access with Spring Boot
- Introduction to Spring Data JPA
- Configuring Data Sources
- Creating JPA Entities
- Relationships Between Entities
- Using Spring Data Repositories
- Query Methods in Spring Data JPA
- Transactions and Persistence Management
- Schema Migrations with Flyway
Module 5: Security in Spring Boot
- Introduction to Spring Security
- Configuring Spring Security
- User Authentication and Authorization
- Implementing JWT Authentication
- Method-Level Security and API Hardening
Module 6: Testing in Spring Boot
- Introduction to Testing
- Unit Testing with JUnit
- Mocking with Mockito
- Integration Testing
- Testing with Testcontainers
Module 7: Advanced Spring Boot Features
- Spring Boot Actuator
- Spring Boot Profiles
- Scheduled Tasks and Asynchronous Execution
- Spring Boot with Docker
- Spring Boot and Microservices
- Service Communication and Fault Tolerance
Module 8: Deploying Spring Boot Applications
- Introduction to Deployment
- Deploying to Heroku
- Deploying to AWS
- Deploying to Kubernetes
- Continuous Integration and Delivery
Module 9: Performance and Monitoring
- Performance Tuning
- Caching with Spring Cache
- Monitoring with Spring Boot Actuator
- Using Prometheus and Grafana
- Logging and Log Management
- Distributed Tracing
