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

  1. Spring Initializr: what it is and how to use it
  2. Generating the project from the web
  3. Generating the project from the command line
  4. Importing the project into the IDE
  5. The generated startup class
  6. The first endpoint: GET /api/v1/stations
  7. Running the application
  8. Testing the endpoint with curl
  9. Packaging and running the JAR
  10. Spring Boot DevTools and automatic restart
  11. Common Mistakes and Tips
  12. Exercises

  1. 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.xml already 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 .gitignore and an empty application.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.

  1. 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.

  1. 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 ciclourbana

A breakdown of the curl options:

  • -d key=value sends a form parameter. Using -d makes curl issue a POST request automatically.
  • dependencies=web,devtools is the comma-separated list of dependency identifiers. web corresponds to "Spring Web" and devtools to "Spring Boot DevTools".
  • -o ciclourbana.zip saves 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 -40

If you omit bootVersion, Initializr uses the latest stable release, which is usually what you want.

  1. 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.

  1. 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:

  • @SpringBootApplication is 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.
  • main is an ordinary Java main method. A Spring Boot application is, literally, a Java program that starts from its main. 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 args argument 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.

  1. The first endpoint: GET /api/v1/stations

We 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, hashCode and toString.
  • 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:

  • @RestController marks 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.
  • @GetMapping with no arguments maps the method to GET on 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 the Content-Type: application/json header. 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

  1. 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:

./mvnw spring-boot:run

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 seconds

The 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.

  1. Testing the endpoint with curl

With the application running, open another terminal:

curl http://localhost:8080/api/v1/stations

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 | jq

The 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 GMT

Notice 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.

  1. Packaging and running the JAR

Building the artefact

./mvnw clean package

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.original

Two 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.MF

The manifest reveals the trick:

Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.ciclourbana.CicloUrbanaApplication

When 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

java -jar target/ciclourbana-0.0.1-SNAPSHOT.jar

It starts up exactly as it did with spring-boot:run. Check that it still responds:

curl -s http://localhost:8080/api/v1/stations | jq '.[0].name'
# "Main Square"

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.jar

Note the difference: -Xmx256m goes before -jar because it is a JVM option, whereas --server.port=9090 goes after because it is an application argument.

  1. 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+S is enough.
  • VS Code: also compiles on save.

Try it. With the application running via ./mvnw spring-boot:run, add a fifth station:

new Station(5L, "Old Market", "Market Street 8", 20, 40.4211, -3.7101)

Recompile and watch the log:

2026-08-31T10:31:08.442+02:00  INFO --- [  restartedMain] c.c.CicloUrbanaApplication : Started CicloUrbanaApplication in 0.612 seconds

Now 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:

# src/main/resources/application.properties
spring.devtools.restart.enabled=false

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 with lsof -i :8080 (Linux/macOS) or netstat -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-web is missing. Without a web dependency there is no server to keep the process alive, so main finishes and the JVM shuts down.
  • DevTools restarts nothing. You are saving the .java but it is not being recompiled. Enable automatic building in the IDE or compile by hand.
  • Running the .jar.original by mistake. It gives no 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. With git 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 mvnw

After creating Station and StationController as described in the lesson:

./mvnw spring-boot:run

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, producing GET /api/v1/stations/summary.
  • Station::capacity is a method reference. In a record, the accessor has the same name as the component (capacity(), not getCapacity()).
  • mapToInt(...).sum() turns the stream into an IntStream and 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=9090

Verification in another terminal:

curl -s http://localhost:9090/api/v1/stations | jq 'length'
# 4

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

Module 2: Spring Boot Core Concepts

Module 3: Building RESTful Web Services

Module 4: Data Access with Spring Boot

Module 5: Security in Spring Boot

Module 6: Testing in Spring Boot

Module 7: Advanced Spring Boot Features

Module 8: Deploying Spring Boot Applications

Module 9: Performance and Monitoring

Module 10: Best Practices and Tips

© Copyright 2026. All rights reserved