Study Notes
Spring Boot by Buildingfoundation

From an empty folder to a running endpoint

Generate TaskFlow, inspect the project skeleton, run the app, add a first REST endpoint, and verify the full generate-run-hit loop.

35 min read·verified Spring Boot 4.x·reviewed 2026-08-28

After this you should be able to

  • Run a Spring Boot app
  • Build and call a REST endpoint
Initializr@RestControllercurl

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.

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
  • Run a Spring Boot app
  • Build and call a REST endpoint

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.

Learn next: Use run a spring boot app as the base, then trace an http request in REST API Backend, Beneath the Hood. Interview next: Use run a spring boot app as the base, then separate api and persistence shapes in API Boundaries: DTOs, Validation & Errors.

Continue to REST API Backend, Beneath the Hood

// 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#

  • start.spring.io → Maven · Java · current stable Boot · Jar · Java 17+
  • Group com.example, artifact taskflow → package com.example.taskflow
  • One dependency: Spring Web → MVC + embedded Tomcat

Run#

  • ./mvnw spring-boot:run or the green ▶ on main()
  • Health lines: Tomcat started on port 8080 + Started TaskflowApplication
  • 404 on / before you add a route = normal.

First endpoint#

  • @RestController on the class + @GetMapping("/ping") on the method
  • Return a String → body; return an object/Map → JSON (via Jackson, already included)
  • @RequestParam → read ?name= from the URL

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.