Spring Boot by Building · Chapter 2
We generate the TaskFlow project — the one app we grow through the whole course — open every file the generator made, understand why each exists, then write one controller and watch a URL answer in the browser. Nothing is skipped and nothing is magic by the end.
Study the same From an empty folder to a running endpoint explanation with a different purpose: build and verify it in Learn deeply, or retrieve and defend it in Prepare for interviews.
The chapter below remains the canonical explanation for both goals. Keep its model, examples, and trade-offs shared; only the deliberate practice changes with the route.
Use this chapter’s TaskFlow work to make one small, observable change. Run the stated acceptance checks before recording practice.
Without notes, explain the boundary this chapter protects, one concrete implementation decision, and the cost of a tempting shortcut. Then answer one follow-up using the chapter’s own example.
By the last step you will have a Spring Boot app running on your machine, and hitting
http://localhost:8080/ping will return pong. Small on purpose — the point is to make
the full loop (generate → understand → run → hit) feel routine, so later chapters can add layers
without the setup ever being the scary part.
A JDK 17 or newer (check with java -version) and any editor — IntelliJ
IDEA Community is free and the smoothest for Spring. You do not need Maven installed: the
generated project ships with a wrapper (mvnw) that fetches the right version itself. No database to
install — we don't touch one until Chapter 4.
start.spring.io is a web form that hands
you a ready-made project skeleton as a .zip. Every choice below maps to something you now understand
from Chapter 1.
Project: Maven. Language: Java. Spring Boot: pick the current stable release without SNAPSHOT next to it (a snapshot is a preview build). These notes work with modern Boot 4.x; if you follow an older Boot 3.x tutorial, the core annotations here are the same.
Group: com.example. Artifact: taskflow. This becomes your base package
com.example.taskflow — the root of the folder map from Chapter 1. Packaging:
Jar. Java: 17 (or your installed version).
Click Add Dependencies → search Spring Web → add it. That single starter pulls in Spring MVC and an embedded Tomcat server, which is why your app can serve HTTP without you installing a server. We add more starters in later chapters — one per new capability.
Click Generate. A taskflow.zip downloads. Unzip it somewhere sensible and
open the taskflow folder in your IDE. IntelliJ will say "Maven project detected" — let it
import; it downloads dependencies once (give it a minute).
A starter like spring-boot-starter-web is a meal kit: one line in your build file pulls
a curated, version-matched set of libraries that all work together. Without starters you'd hand-pick a dozen
libraries and pray their versions agree. That "dependency hell" is exactly what Boot removed.
Open the project. It looks like this. Nothing here is optional decoration — let's name each piece.
package com.example.taskflow;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TaskflowApplication {
public static void main(String[] args) {
SpringApplication.run(TaskflowApplication.class, args);
}
}
main() is a plain Java entry point — the same one you've written since day one. Running it
starts everything.SpringApplication.run(...) boots the assembly line: it starts the embedded Tomcat server and
scans your packages for classes tagged @RestController, @Service, etc.,
creating a bean for each.@SpringBootApplication is three annotations in one; the important part is
component scanning starts from this class's package — which is why every other class you
write must live under com.example.taskflow to be found.If you put a controller in a package outside com.example.taskflow (say
com.other), Spring's scan never sees it, no bean is made, and your URL returns 404 with no error.
Rule: keep every class under the application's package. This is the "why isn't my endpoint
working" answer 80% of the time.
The pom.xml is the build file. You rarely edit it by hand except to add dependencies — here's the
one line that came from choosing Spring Web:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Run the app now, empty. This proves the setup works and teaches you what a healthy startup looks like, so a broken one is obvious later.
In the IDE: open TaskflowApplication.java and click the green ▶ next to main(). Or from
the project root in a terminal:
Now visit http://localhost:8080 in a browser. You'll get a Whitelabel Error Page
saying 404. That is success. The server is running; you simply haven't told it about any URL yet. Stop
the app (red ■, or Ctrl-C) and let's give it one.
Two lines are your health check: Tomcat started on port 8080 (the server is up) and
Started TaskflowApplication (beans wired, no errors). If you don't see both, scroll up — the first
red ERROR/Caused by: line names the problem, and it's almost always a port clash or a
typo'd annotation, not something mysterious.
Time for a real endpoint. Create a controller package and a class inside it. In IntelliJ:
right-click the com.example.taskflow package → New → Package → name it controller, then
add a class PingController.
package com.example.taskflow.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PingController {
@GetMapping("/ping")
public String ping() {
return "pong";
}
}
package ...controller; — lives under the app package, so component scan finds it. (Remember
pitfall #1.)@RestController — the name tag from Chapter 1. Spring makes one bean of this class and knows
its methods return data straight into the HTTP response body (as text/JSON), not a web-page name.@GetMapping("/ping") — "when an HTTP GET arrives at /ping, run
this method." GET is what a browser does when you type a URL.return "pong"; — the returned String becomes the response body. That's the whole endpoint.Run the app again (▶) and visit http://localhost:8080/ping:
That round trip — browser → Tomcat → your @GetMapping method → back — is the top rung of Fig 1 from
Chapter 1. Everything we add from here just extends the same trip downward.
No new PingController(). No server-startup code. No "register this URL" wiring. You wrote a tagged
class and a tagged method; Spring's component scan found them at boot and connected the URL for you. That is
IoC doing its job — and it's exactly the mechanism that will inject a Service into this controller next
chapter.
Real APIs return JSON objects, not bare strings, and they read input from the URL. Add one more method to prove
both — no new file, just extend PingController for now (we'll split things properly next chapter).
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
@GetMapping("/greet")
public Map<String, String> greet(@RequestParam(defaultValue = "world") String name) {
return Map.of("message", "hello, " + name);
}
@RequestParam String name — pulls ?name=... from the URL into the argument.
defaultValue makes it optional.Map (or any object) → Spring auto-converts it to JSON via a library called
Jackson, which the web starter already included. You return Java; the client receives JSON. You never call a
"toJson" yourself.This is the format the practice chapters use, shown here fully solved so you know exactly what "end to end" means. Read it as a demonstration, then do it on your own machine.
/health endpointGoal. A GET on /health returns JSON
{"status":"UP","service":"taskflow"}.
/health in the browser shows the JSON above.Step by step — exactly what I did:
It only speaks HTTP and returns fixed data → it's a Controller concern. No service or DB needed. So I add a method to a controller.
A @GetMapping("/health") returning a Map so Spring makes it JSON.
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "UP", "service", "taskflow");
}
Self-check: both boxes tick. Notice you reused the exact pattern from
/greet minus the parameter. That reuse — spotting "this is the same shape as one I've done" — is
the skill the whole course is training.
Type Exercise 2.1 into your own project (don't copy-paste — typing builds the muscle) and get the green
output. Then delete /greet and /health if you like; /ping stays. Next
chapter we split responsibilities into real layers and this file gets a proper job.
The full loop on one page
com.example, artifact taskflow → package com.example.taskflow./mvnw spring-boot:run or the green ▶ on main()/ before you add a route = normal.@RestController on the class + @GetMapping("/ping") on the method@RequestParam → read ?name= from the URLcom.example.taskflow or component scan
won't find it (404 with no error).@Controller ≠ @RestController. The plain one returns a
view name; you want the Rest one (returns the body).server.port=8081 in
application.properties or free the port.Chapter 3 — Controller → Service → Repository, wired. We turn this one-file toy into the real four-layer shape from the map, storing tasks in memory, and you'll watch dependency injection connect the layers for you.
Current state: a package plan. Outcome: a running /ping endpoint. Prerequisite: the layer map; it guides you but does not lock this chapter. Java bridge: main() remains the entry point. Estimate: 35 minutes.
Generate the app, add PingController, and use curl to see pong. The artifact is a repeatable local run.
./mvnw spring-boot:run starts without an error.curl localhost:8080/ping returns pong.com.example.taskflow.Likely files: TaskflowApplication.java, controller/PingController.java. Suggested commit: feat: add taskflow ping endpoint.
@RestController, not a view controller.Before opening the solution: start once, curl once, and check the package name.
@GetMapping("/ping") is enough. Keep it transport-only.The next workshop replaces this isolated endpoint with the map’s collaborating layers.