Spring Boot by Building · Chapter 2

From an empty folder to a running endpoint

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.

Chapter 2 · We do — fully guided · Hands-on ~30 min · You'll run real code


Choose your route

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.

Learning goal

Shared core

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.

Learn deeply: build evidence

Use this chapter’s TaskFlow work to make one small, observable change. Run the stated acceptance checks before recording practice.

Prepare for interviews: explain the trade-off

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.

// goal of this chapter

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.

// what you need first (one-time)

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.

Section 1Generate the project with Spring Initializr

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.

  1. Open start.spring.io and set the top-left options

    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.

  2. Fill in Project Metadata

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

  3. Add exactly one dependency: Spring Web

    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.

  4. Generate, then unzip

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

// what a "starter" is

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.


Section 2Meet every file the generator made

Open the project. It looks like this. Nothing here is optional decoration — let's name each piece.

taskflow ├─ pom.xml // the build file: dependencies + how to run ├─ mvnw, mvnw.cmd // the Maven wrapper (so you don't install Maven) ├─ src/main/java/com/example/taskflow │ └─ TaskflowApplication.java // the ignition — has main() ├─ src/main/resources │ ├─ application.properties // settings sheet (empty for now) │ ├─ static/ // static files (css/js/images) — unused for now │ └─ templates/ // server-rendered HTML — unused (we return JSON) └─ src/test/java/com/example/taskflow └─ TaskflowApplicationTests.java // a starter test: "does the app boot?"

The two files that matter today

src/main/java/com/example/taskflow/TaskflowApplication.java
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);
    }
}
this is the whole ignition
  • 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.
// pitfall #1 — the most common beginner bug

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:

pom.xml — the dependency that makes HTTP possible
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Section 3Run it once — before writing anything

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:

$./mvnw spring-boot:run .... ____ _ /\\ / ___'_ __ _ _(_)_ __ __ _ ( ( )\___ | '_ | '_| | '_ \/ _` | \\/ ___)| |_)| | | | | || (_| | ' |____| .__|_| |_|_| |_\__, | :: Spring Boot :: Tomcat started on port 8080 (http) Started TaskflowApplication in 1.8 seconds ✔ the app is up and listening on http://localhost:8080

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.

// read the startup log like a pro

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.


Section 4Write your first controller

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.

NEW · src/main/java/com/example/taskflow/controller/PingController.java
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";
    }
}
every line, decoded
  • 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:

$ curl http://localhost:8080/ping pong

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.

// notice what you did NOT write

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.


Section 5Return JSON, and take a parameter

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

add to PingController.java
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);
}
two new ideas
  • @RequestParam String name — pulls ?name=... from the URL into the argument. defaultValue makes it optional.
  • Returning a 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.
$ curl "http://localhost:8080/greet?name=Abhishek" {"message":"hello, Abhishek"} $ curl http://localhost:8080/greet {"message":"hello, world"}

Section 6Worked micro-exercise — done with you, end to end

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.

Exercise 2.1 we do — full solution shown

Add an /health endpoint

Goal. A GET on /health returns JSON {"status":"UP","service":"taskflow"}.

  • Visiting /health in the browser shows the JSON above.
  • It's a GET; no parameters.

Step by step — exactly what I did:

  1. Decide which layer this is.

    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.

  2. Write the method.

    A @GetMapping("/health") returning a Map so Spring makes it JSON.

  3. Run and verify against the checklist.
add to PingController.java
@GetMapping("/health")
public Map<String, String> health() {
    return Map.of("status", "UP", "service", "taskflow");
}
$ curl http://localhost:8080/health {"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.

// now really do it

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.


Notebook Summary

The full loop on one page

Generate

Run

First endpoint

Box these

  • ⭐Every class must live under com.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).
  • 💡Port 8080 in use → app won't start. Set server.port=8081 in application.properties or free the port.
// next

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.

TaskFlow checkpoint

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.

Mission: prove TaskFlow can receive a request

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.
  • The controller sits below com.example.taskflow.

Likely files: TaskflowApplication.java, controller/PingController.java. Suggested commit: feat: add taskflow ping endpoint.

Hint 1
Choose Spring Web in Initializr.
Hint 2
Use @RestController, not a view controller.
Hint 3
Read the first startup error, not the last log line.

Before opening the solution: start once, curl once, and check the package name.

Reference patch — first endpoint
One controller with one @GetMapping("/ping") is enough. Keep it transport-only.

Recall before continuing

  1. What starts the embedded server?
  2. Why must the controller share the base package?
  3. How do you prove the endpoint works without a browser?

The next workshop replaces this isolated endpoint with the map’s collaborating layers.