跪拜 Guibai
← Back to the summary

Spring Boot vs. FastAPI: When a Java Dev Discovers Python Backends Are 70% Less Code

After Learning Spring Boot, Then Looking at FastAPI, I Broke Down

After two years of coding with Spring Boot, I thought my backend skills were decent. Last week, on a whim, I opened the FastAPI official documentation. Fifteen minutes later, I fell silent.

It wasn't that it was too hard; it was too simple. So simple that it made me question what I've been doing all these years.

This isn't a hit piece. Spring Boot was my introductory framework, and it taught me engineering thinking. But some things just need to be said.


1. Hello World Comparison — The First Cut Broke Me

Spring Boot's first lines of code:

@RestController
@RequestMapping("/api")
public class HelloController {
    @GetMapping("/hello/{name}")
    public String sayHello(@PathVariable String name) {
        return "Hello, " + name + "!";
    }
}

What do you have to go through before writing this line? Create a Maven project, configure pom.xml, wait for dependencies to download, write the startup class, configure application.yml... By the time you get it running, half an hour has passed.

And FastAPI?

from fastapi import FastAPI

app = FastAPI()

@app.get("/api/hello/{name}")
def say_hello(name: str):
    return f"Hello, {name}!"

pip install fastapi → write 5 lines of code → uvicorn main:app --reload → open the browser, done.

Breaking point: Before Spring Boot's startup class is even finished, FastAPI is already callable.

And FastAPI comes with a /docs page, Swagger UI generated automatically, no configuration needed. Anyone who has tried to integrate Swagger with Spring Boot knows the pain.


2. Dependency Injection — Spring's Heavy Weapon vs. FastAPI's Light Punch

Spring's dependency injection is its soul, and also its burden.

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public List<User> getUsers() {
        return userRepository.findAll();
    }
}

@Service + @Autowired + @ComponentScan + configuring scan paths... Spring's IoC container is indeed powerful, but people learning Spring spend half their time understanding how the container works.

FastAPI's dependency injection is completely different:

from fastapi import Depends

def get_db():
    db = Database()
    try:
        yield db
    finally:
        db.close()

@app.get("/users")
def get_users(db: Database = Depends(get_db)):
    return db.query_all()

Explicit declaration, clear at a glance, no black magic. Depends() directly throws the dependency chain into the parameters; who depends on whom is visible at a glance.

Breaking point: The DI learned in Spring, when encountered in FastAPI, reveals it can be this simple. It's not that Spring is bad; FastAPI makes you realize that DI itself doesn't need to be that complex.


3. Data Validation — The Pain of JPA vs. The Joy of Pydantic

Anyone who has written Spring Boot knows how painful a DTO class is:

public class UserDVO {
    @NotNull(message = "Name cannot be empty")
    @Size(min = 2, max = 20)
    private String name;

    @Email
    private String email;

    @Pattern(regexp = "^1[3-9]\d{9}$")
    private String phone;

    // Constructor
    public UserDVO() {}

    // getter/setter x 6
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }
}

30 lines minimum, and that's just basic Bean Validation. Add Swagger annotations? 50 lines minimum.

FastAPI's Pydantic:

from pydantic import BaseModel, EmailStr, Field

class UserDTO(BaseModel):
    name: str = Field(min_length=2, max_length=20)
    email: EmailStr
    phone: str = Field(pattern=r"^1[3-9]\d{9}$")

10 lines, done. Validation, type hints, serialization, documentation generation, all automatic.

Most importantly: No need to write getters/setters. No need to write constructors. No need to configure toString. Python's data classes are natural DTOs.

Breaking point: Writing DTOs in Java equals overtime; writing DTOs in Python is an afterthought.


4. Asynchronous Programming — Patches vs. Native

Java's async journey is a history of applying patches:

Writing an async endpoint:

@GetMapping("/users/{id}")
public CompletableFuture<User> getUser(@PathVariable Long id) {
    return CompletableFuture.supplyAsync(() -> userService.findById(id));
}

CompletableFuture, thread pools, ExecutorService, Future chains... Exception propagation alone could fill an article.

FastAPI's async:

@app.get("/users/{id}")
async def get_user(id: int):
    user = await db.fetch_one("SELECT * FROM users WHERE id = ?", (id,))
    return user

async def + await, native async support in the language. No need to learn Reactor, no need to understand Mono/Flux; the code written is almost indistinguishable from synchronous code.

Breaking point: Doing async in Java feels like taking an exam; doing async in FastAPI feels like slacking off.


5. Middleware — Ritual vs. Pragmatism

Spring Boot's interceptor:

@Component
public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler) {
        System.out.println("Request: " + request.getMethod() + " " + request.getRequestURI());
        return true;
    }
}

And you still need to register it:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .addPathPatterns("/api/**");
    }
}

Interface, implementation class, registration configuration — three steps, none can be missed.

FastAPI:

@app.middleware("http")
async def log_requests(request: Request, call_next):
    print(f"Request: {request.method} {request.url}")
    return await call_next(request)

One decorator, done.

Breaking point: What takes one @app.middleware in FastAPI requires three files in Spring.


6. Project Structure — Convention vs. Freedom

Spring Boot's recommended project structure:

src/main/java/com/example/demo/
  ├── controller/
  ├── service/
  ├── repository/
  ├── dto/
  ├── config/
  ├── exception/
  ├── entity/
  └── DemoApplication.java

It's not that this structure is bad, but that it's the only way. Community norms, team conventions, framework requirements — you have no choice.

FastAPI:

myapp/
  ├── main.py
  ├── routers/
  ├── models.py
  └── schemas.py

Split if you want, merge if you want. Small projects can be a single main.py; large projects can be slowly broken into modules. No one dictates to you.

Breaking point: Spring Boot's conventions are good conventions, but sometimes you just want the freedom to "put it anywhere."


7. This Isn't the End, It's a Choice

Writing to the end, I want to say something from the heart.

Spring Boot taught me so many things:

This experience is priceless.

FastAPI, on the other hand, made me re-examine myself:

They are not replacements for each other; they are complementary.

For large enterprise projects, complex business logic, team collaboration, Spring Boot is still king. For quick-and-dirty prototypes, personal projects, AI services, FastAPI lets you fly high.

Finally, a quote for everyone:

"After learning Spring Boot and then looking at FastAPI, what broke me wasn't the technology, but the realization that I had been spoiled by Java — accustomed to complexity, forgetting that simplicity is also a capability."