Learning Together


Spring Boot: Your Fairy Godmother for Java Development

written by

Imagine having a fairy godmother who can grant your wishes and make any task easier. Well, when it comes to software development, Spring Boot can be just like that fairy godmother—making your job simpler and more magical.

Life without fairy godmother

Life with fairy godmother

In the kingdom of Java development, building an application used to feel like a never-ending list of chores — configuring XML files, setting up dependencies, wiring beans manually, and managing deployment environments. It was all necessary, but rarely magical. Then came Spring Boot, the enchanted helper every developer had been waiting for.

Spring Boot is a powerful framework built on top of the Spring ecosystem that makes it easier and faster to create stand-alone, production-grade Java applications. Think of it as your fairy godmother: it handles all the behind-the-scenes work — from auto-configuring beans to setting up embedded servers — so you can focus on building features that truly matter.

Whether you’re building a REST API, a web app, or a microservice, Spring Boot gives you the magic tools to get there faster — with less effort and fewer errors. In this post, we’ll see just how Spring Boot waves its wand to make coding easier.

In this post, we’re going to build a simple Java application where users can create posts and add comments. While the app itself is straightforward, it’s the perfect example to show just how powerful and easy to use Spring Boot really is. My goal is to keep evolving this project in future posts by gradually adding more features and exploring Spring Boot’s full potential.

Here are the main features our app should support:

  • Display a list of all posts, showing the title, a short excerpt, and the author’s name.
  • Allow users to create new posts and add new authors.
  • Show the full details of a post, including its content and existing comments, and provide a way to add new comments.

To build this app, we’ll follow these main steps:

  1. Create the necessary classes to store Post, Author, and Comment objects in the database.
  2. Set up REST endpoints to perform basic CRUD operations for posts, comments, and authors.

3. Build a simple user interface that allows users to create new posts, view a list of posts, and add comments to them.

Step 0: Preparing the Spellbook: Setting Up Our Spring Boot Project”

Before we start building anything, let’s create the basic structure of our Spring Boot project. Since we’re using Maven, we’ll follow the standard folder layout:

[project-root]/

├── pom.xml

└── src/

    └── main/

        ├── java/

        └── resources/

🔍 Note: We’re skipping tests for now to keep things simple. They’re super important, but we’ll cover them in a future post.

You can check out the full project here, but for now, let’s focus on a key section in the pom.xml file — the <parent> tag:

<parent>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-parent</artifactId>

    <version>3.5.3</version>

</parent>

What Does the spring-boot-starter-parent Do?

1. Dependency Management

No more version headaches! It predefines compatible versions for Spring Boot dependencies. For example:

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-web</artifactId>

</dependency>

You don’t need to specify the version — Maven will pick the right one for Spring Boot 3.5.3.


2. Default Plugin Configuration

Spring Boot automatically configures several Maven plugins for us:

  • spring-boot-maven-plugin
    • Packages your app as a JAR/WAR
    • Enables running with java -jar yourapp.jar
    • Embeds Tomcat, Jetty, or Undertow
  • maven-compiler-plugin
    • Compiles Java code
    • Sets the Java version (usually Java 17+)
  • maven-surefire-plugin
    • Runs unit tests during the build
    • Automatically detects test classes (like *Test.java)
    • Supports JUnit 5 by default
  • maven-resources-plugin
    • Copies resource files (application.yml, static/, templates/, etc.)
  • maven-jar-plugin
    • Builds the final .jar file
    • Adds metadata like Main-Class to the MANIFEST.MF

3. Java Version & Build Settings

It also ensures your project uses a consistent Java version and sensible defaults, so your build just works out of the box.


In short: this parent config saves you from tons of boilerplate and setup. It’s Spring Boot’s way of letting you focus on building your app, not fighting with configuration.

Step 1: The First Incantation: Awakening the Spring Boot Magic

Now it’s time to create the first and maybe most important class in our whole project. Here’s what it looks like:

@SpringBootApplication

public class PostApplication {

    public static void main(String[] args) {

        SpringApplication.run(PostApplication.class, args);

    }

}

At first glance, it may look simple. But this class is what turns your app into a Spring Boot application.

If you’re curious and want to go beyond the surface, let’s dive deeper and uncover the magic behind this code.

Understanding @SpringBootApplication

The annotation @SpringBootApplication is actually a meta-annotation, which means it is made up of several other annotations. Here’s how it’s defined under the hood:

@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Inherited

@SpringBootConfiguration

@EnableAutoConfiguration

@ComponentScan

public @interface SpringBootApplication {

    // additional attributes…

}

Let’s break down the important parts:

Annotations like @Target, @Retention, @Documented, and @Inherited are Java meta-annotations. They control how annotations behave, but their details are outside the scope of this post.

Let’s Look at the Key Annotations in Detail:

  • @SpringBootConfiguration: Marks this class as a Spring configuration class. It’s like saying, “Hey Spring, this is where the setup starts!”, You can define beans here using @Bean—though in this post, we won’t be doing that.
  • @EnableAutoConfiguration: This is the heart of Spring Boot’s magic. It tells Spring Boot to automatically configure your app based on the dependencies you’ve included. let see some examples:
    • If you include spring-boot-starter-web, Spring Boot will:
      • Set up an embedded Tomcat server
      • Configure the DispatcherServlet, and more.
    • If you include the H2 database dependency, Spring Boot will:
      • Automatically configure an in-memory database for you.
  • @ComponentScan: This tells Spring to scan the current package and all sub-packages for classes annotated with @Component, @Service, @Controller, @Repository, etc. For example: If your main class is in com.example.blog.PostApplication, Spring will scan: com.example.blog.*

 

The main() Method: Launching the App

Inside the class, we have this line:

SpringApplication.run(PostApplication.class, args);

This is the entry point of your Spring Boot application. Behind the scenes, this one-liner does a lot:

  • Loads the application.properties file
  • Registers all beans (@Component, @Service, @Controller, and @Bean)
  • Starts the embedded server (Tomcat, Jetty, or Undertow, depending on your setup)
  • And much more…

In short, this simple-looking class is your gateway into the Spring Boot world—a lot happens with very little code, which is exactly why Spring Boot is so powerful.

Step 2: Modeling the Magic: Setting Up Our Domain Classes

To start building our app, we need a database. To keep things simple, we’ll use H2, an in-memory relational database written in Java. It’s lightweight, fast, and perfect for development, testing, and prototyping—no installation or configuration required.

Add the Required Dependencies

Open your pom.xml file and add the following two dependencies:

  •  H2 Database:

<dependency>

    <groupId>com.h2database</groupId>

    <artifactId>h2</artifactId>

    <scope>runtime</scope>

</dependency>

  • Spring Data JPA:

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

This second dependency enables Spring Data JPA, which simplifies database operations. It includes:

  • spring-data-jpa: abstraction layer over JPA
  • Hibernate: the default JPA implementation
  • spring-boot-starter-aop: for handling aspects like transactions
  • spring-boot-starter-jdbc: for lower-level database access
  • jakarta.persistence or javax.persistence: standard JPA API

Creating the Core Entities

Now that we have the dependencies set, let’s define the data models. Our main entity is Post, but we’ll also need Author and Comment.

@Entity

public class Post {

    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;

    private String title;

    private String content;

    private String excerpt;

    @OneToMany(mappedBy = “post”, cascade = CascadeType.ALL, orphanRemoval = true)

    private List<Comment> comments = new ArrayList<>();

    @ManyToOne(cascade = CascadeType.ALL)

    private Author author;

    //Setter and getter methods    

}

What Do These Annotations Mean?

  • @Entity: Marks the class as a JPA entity, which will be mapped to a table in the database.
  • @Id: Defines the primary key field.
  • @GeneratedValue: Automatically generates the ID (using an auto-increment strategy).
  • @OneToMany: A Post can have many Comments.
  • @ManyToOne: A Post is written by one Author, but one Author can write many Posts.

JPA will create a post table and map each field to a column. You can customize table or column names using @Table or @Column.

 Defining Supporting Entities

Author Entity

@Entity

public class Author {

    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;

    private String firstName;

    private String lastName;

    private String email;

    //Setter and getter methods    

 }

Comment Entity

@Entity

public class Comment {

    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;

    private String text;

    //Setter and getter methods    

}

Creating Repositories

To let Spring Data JPA handle our database operations, we need to define repositories for each entity.

AuthorRepository

public interface AuthorRepository extends JpaRepository<Author, Long> {

}

That’s it! Just by extending JpaRepository, Spring Boot will generate the implementation for you at runtime. This gives you access to a bunch of ready-to-use methods:

  • Optional<Author> findById(Long id): Find an author by ID
  • Author save(Author author): Save or update an author
  • void deleteById(Long id): Delete an author by ID
  • List<Author> findAll(): List all authors

The <Author, Long> part tells Spring:

  • Author: the entity type
  • Long: the type of the primary key (id field)

Comment Repository

it looks almost the same 

public interface CommentRepository extends JpaRepository<Comment, Long> {

}

But in this case the auto generated methods signature is slightly different:

  • Optional<Comment> findById(Long id): Find an comment by ID
  • Comment save(Comment author): Save or update an comment
  • void deleteById(Long id): Delete an comment
  • List<Comment> findAll(): List all comments

The PostRepository also extends JpaRepository, but we may customize it later to optimize some queries for performance.

Step 3: Tuning the Spell: Customizing the Repository for Better Performance

By default, for performance reasons, JPA does not automatically load related entities like Author or Comment when you retrieve a Post. This means that if you run a method like:

Optional<Post> byId = postRepository.findById(postId);

or

List<Post> all = postRepository.findAll();

…the related data (such as the author or comments) is not fetched immediately. This is intentional — we don’t want to load all related data (like every comment on every post) when it’s not needed, especially when listing multiple posts.

However, these relationships are loaded lazily. That means if you later access them like this:

Optional<Post> byId = postRepository.findById(postId);

byId.getAuthor();

…JPA will trigger a new SQL query just to fetch the author.

This becomes a problem when you do something like:

List<Post> all = postRepository.findAll();

for (Post post : all) {

    post.getAuthor();

}

This will execute:

  • One query to get all posts, and then
  • One additional query for each post to get its author.

So if you have 100 posts, that’s 101 queries! This is known as the N+1 query problem, and it severely affects performance.

Our Use Case

We need to:

  • Display a list of all posts, showing the title, a short excerpt, and the author’s name.
  • Show the full details of a post, including its content and existing comments, and provide a way to add new comments.

But:

  • When listing posts, we don’t need comments. We do need the author’s name but we don’t want to trigger one query per post to get its author.
  • When one Post detail, we need comments but we don’t want to trigger one extra query to load the comments.

Solution: Use @EntityGraph to Customize Fetch Behavior

Spring Data JPA’s @EntityGraph annotation allows you to eagerly load specific relationships using a single optimized SQL query. You can customize the behavior of methods like findById and findAll to pre-fetch the data you need.

Here’s how to use it in your PostRepository:

public interface PostRepository extends JpaRepository<Post, Long> {

    @EntityGraph(attributePaths = “comments”)

    Optional<Post> findById(Long id);

    @EntityGraph(attributePaths = “author”)

    List<Post> findAll();

}

What This Means

  • @EntityGraph(attributePaths = “comments”)
    → When fetching a single post by ID, load all its comments in the same query.
  • @EntityGraph(attributePaths = “author”)
    → When listing all posts, load each post’s author in the same query — avoiding the N+1 problem.

This approach gives you better control over performance and ensures you only load what you need, when you need it.

Step 4: Opening the Portal: Connecting to the Magical Data Realm

After setting up our Entities and Repository, we still need to configure the DataSource. This tells JPA where the database is located so it knows where to store and retrieve data.

To do this, let’s create a file named application.properties inside the src/main/resources folder and add the following configuration:

spring.datasource.url=jdbc:h2:file:./data/mydb

spring.datasource.username = sa

spring.datasource.password=password

Here’s what each property means:

  • spring.datasource.url: Specifies the location and type of the database.
    • jdbc:h2:file:./data/mydb tells Spring to use an H2 database and store the data in a file on your local file system (in the ./data/ directory).
    • If you wanted an in-memory database (data disappears when the app stops), you could use something like jdbc:h2:mem:testdb instead.
  • spring.datasource.username and spring.datasource.password: Define the credentials used to access the database.
    • For H2, the default username is sa and the password is usually left blank or set to something like password.

We now have:

  • A lightweight database (H2) ready to use
  • Three entities: Post, Author, and Comment
  • Repositories that give us basic CRUD operations with almost no code

Next, we’ll focus on how to interact with this data—creating REST endpoints, UI pages, and customizing behavior where needed.

Step 5: Waving the Wand: Opening Pathways with Spring Boot Controllers

Now Let’s Create Our REST Endpoint — But First, Some Important Concepts

Before jumping into building our REST endpoint, let’s take a moment to explore two key concepts that will help us design a clean and well-structured application:

  • Layered Architecture – This will help us organize our code into clear, manageable layers, each with a specific responsibility.
  • DTO (Data Transfer Object) Pattern – This pattern will allow us to safely and efficiently transfer data between layers, especially when working with APIs.

Understanding these concepts will make it easier to scale, maintain, and test our application as it grows.

Before the Spell: Setting the Foundation with Layered Architecture

Layered architecture (also known as n-tier architecture) is a design pattern that organizes code into layers, each responsible for a specific concern. It promotes separation of concerns, testability, reusability, and maintainability.

In Spring Boot applications, the most common layers are:

  1. Controller Layer (Web Layer): This is the entry point of the app. It handles HTTP requests, processes input (like query parameters or JSON bodies), and returns HTTP responses.
  2. Service Layer (Business Logic): This layer contains the business logic. It coordinates the app’s behavior and often connects multiple repositories or applies business rules.
  3. Repository Layer (Data Access): This layer handles interaction with the database, typically using Spring Data JPA. It abstracts SQL and provides CRUD operations.
  4. Model Layer (Domain Entities): This is your domain model, defined with JPA entities. These represent the structure of your data.

In the previous sections we already work with the Model layer and Repository Layer, now we need to move forward and work with the Service and Controller Layer.

Casting Safe Spells: Using DTOs to Control the Magic

DTO (Data Transfer Object) is a design pattern where simple objects are used to transfer data between layers of an application:

  • Controller uses DTOs to receive input and send responses
  • Service uses DTOs for communication with the controller, but works with Entities internally
  • Mappers convert between DTOs and Entities

Creating a New Post Endpoint

Now it’s time to build the endpoint that allows users to create a new post.

Endpoint Details:

  • HTTP Method: POST
  • URL: http://localhost:8080/api/posts

Request Payload:

{

    “title” : “…”,

    “content”: “…”,

    “authorId”: “1”,

    “excerpt”: “…”

}

Expected Response:

{

    “id”: “3”,

    “title”: “…”,

    “content”: “…”,

    “excerpt”: “…”,

    “author”: {

        “id”: 1,

        “fullName”: “…”,

        “email”: “…”

    }

}

Using the DTO Pattern

As discussed earlier, we’ll use the DTO (Data Transfer Object) pattern to separate the internal entity logic from what we expose externally, so let create ours DTO objects:

Request DTO:

public class CreatePostRequest {

    private String title;

    private String content;

    private String excerpt;

    private Long authorId;

    //Getters and setters methods

}

Response DTOs:

public class PostResponse {

    private String id;

    private String title;

    private String content;

    private String excerpt;

    private AuthorResponse authorResponse;

   //Getters and setters methods

}

public class AuthorResponse {

    private Long id;

    private String fullName ;

    private String email;

    //Getters and setters methods

}

You can see how this class match with the Request Payload and the Response Payload showed in previuosly.

Mapping Between Entities and DTOs

To convert between CreatePostRequest and Post, or between Post and PostResponse, we need mapper methods like:

Manual Mapping Example:

public Post toEntity(CreatePostRequest request, Author author) {

    if ( request == null && author == null ) {

        return null;

    }

    Post post = new Post();

    if ( request != null ) {

        post.setTitle( request.getTitle() );

        post.setContent( request.getContent() );

        post.setExcerpt( request.getExcerpt() );

    }

    post.setAuthor( author );

    return post;

}

or

public PostResponse toDto(Post post) {

    if ( post == null ) {

        return null;

    }

    PostResponse postResponse = new PostResponse();

    if ( post.getId() != null ) {

        postResponse.setId( String.valueOf( post.getId() ) );

    }

    postResponse.setTitle( post.getTitle() );

    postResponse.setContent( post.getContent() );

    postResponse.setExcerpt( post.getExcerpt() );

    postResponse.setAuthor( authorMapper.toDto( post.getAuthor() ) );

    return postResponse;

}

Simplifying Mapping with MapStruct

To avoid writing boilerplate code, we can use MapStruct, a code generator that will create these mapping methods automatically. We just need to implement the Interface and MapStruct

is going to implement the code for us.

PostMapper Interface:

@Mapper(componentModel = “spring”, uses = {AuthorMapper.class})

public interface  PostMapper {

    @Mapping(target = “author”, source = “author”)

    @Mapping(target = “id”, ignore = true)

    Post toEntity(CreatePostRequest request, Author author);

    PostResponse toDto(Post post);

}

Explanation:

  • @Mapper: Marks the interface as a MapStruct mapper. With componentModel = “spring”, the generated implementation is automatically registered as a Spring Bean.
  • @Mapping: Tells MapStruct how to map specific fields. In this case:
    • Use the author parameter to set the Post.author field.
    • Ignore the id field since it’s a new entity and will be auto-generated by JPA.

Steps to Create a Post

Here’s a breakdown of the full process for creating a new Post:

  1. Receive a CreatePostRequest from the client.
  2. Look up the Author using the provided authorId:
    • If the author does not exist, throw an exception.
    • Respond with HTTP 404 (Not Found).
  3. If the author exists:
    • Convert the request into a Post entity.
    • Save it using PostRepository.
    • Convert the saved entity into a PostResponse.
    • Return it with HTTP 200 (OK).

Where Should This Logic Live?

This raises an important question: should the logic go in the Controller or the Service? Let’s review the previous steps to determine which ones belong in the Service and which in the Controller.

Controller:

Responsible for HTTP-specific logic (requests and responses), so it should be in charge of:

  • Step 1: Receive request
  • Step 2b: Send error response
  • Step 3c: Return successful response

Service:

Responsible for business logic, , so it should be in charge of:

  • Step 2a: Find author or throw exception
  • Step 3a: Convert request to entity and save
  • Step 3b: Convert entity to response

Sample PostService Implementation

Let see how the PostService implementation look like:

@Service

public class PostService {

    @Autowired

    private PostRepository postRepository;

    @Autowired

    @Qualifier(“postMapperImpl”)

    private PostMapper postMapper;

    public PostResponse save(final CreatePostRequest createPostRequest) {

Author author = getAuthorById(request.getAuthorId());

Post post = postMapper.toEntity(request, author);

return postMapper.toDto(postRepository.save(post));

    }

    protected Author getAuthorById(Long id) {

        return authorService.get(id);

    }

    //Others method to find all the Post and to find one Post by Id

}

Summary of Key Annotations

  • @Service: Marks the class as a Spring service component, it means that it is going to be exposed as a springboot Bean to be used later.
  • @Autowired: Injects Spring-managed beans like PostRepository (it was expose because we extend JPARepository) and PostMapper.

But we need to throw a Exception if the Author does not exists right?, so let check the AuthorService class:

@Service

public class AuthorService {

    @Autowired

    private AuthorRepository authorRepository;

    @Autowired

    private AuthorMapper authorMapper;

    public Author get( long id) {

        return authorRepository.findById(id).orElseThrow(() -> new AuthorNotFoundException(id));

    }

    //Other methods

}

How you can see the AuthorService#get method throws an AuthorNotFoundException if the Author does not exist.

If you want to see the whole PostService implementation, can check here, also you can see the whole code of the AuthorService and the CommentService.

 Implementing the PostController

To create our REST API for managing blog posts, we need to start by adding a required dependency to our pom.xml:

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-web</artifactId>

</dependency>

This dependency gives us everything we need to create web controllers, including support for @RestController.

finally the Controller Implementation

@RestController

@RequestMapping(“/api/posts”)

public class PostController {

    @Autowired

    private PostService postService;

    @PostMapping

    public PostResponse createPost(@RequestBody CreatePostRequest request) {

        return postService.save(request);

    }

    //other methods to find all the Posts and to find a Post by Id

}

Explanation:

  • @RestController: Tells Spring that this class will handle HTTP requests and return data as JSON (not HTML views).
  • @RequestMapping(“/api/posts”): Sets the base URL for all methods in this controller. Every endpoint here will be under /api/posts.
  • @PostMapping: This marks the method as the handler for HTTP POST requests to /api/posts.

Handling Errors Globally

Right now, if an invalid authorId is sent in the request and the author doesn’t exist, our service might throw an AuthorNotFoundException, but the response won’t automatically return a 404 Not Found.

Instead of handling this exception manually in every controller, we can define a global exception handler using @ControllerAdvice to set the behavior for all the Controllers at once.

@ControllerAdvice

public class GlobalExceptionHandler {

    @ExceptionHandler(NotFoundEntityException.class)

    public ResponseEntity<String> handleNotFoundException(final NotFoundEntityException ex) {

        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);

    }

}

Explanation:

  • @ControllerAdvice: This tells Spring that this class will handle exceptions thrown by any controller in the application.
  • @ExceptionHandler(NotFoundEntityException.class): This method will catch any exception of type NotFoundEntityException (or its subclasses) and return a 404 Not Found HTTP response along with the exception’s message.

Why NotFoundEntityException Instead of AuthorNotFoundException?

Here’s what the AuthorNotFoundException looks like:

public class AuthorNotFoundException extends NotFoundEntityException {

    public AuthorNotFoundException(Long id) {

        super(“Author not found with id: ” + id);

    }

}

This class extends NotFoundEntityException, so it inherits its behavior.

This design allows you to define a single global rule (return 404 for any “not found” case) and apply it to multiple exceptions like:

  • AuthorNotFoundException
  • PostNotFoundException
  • etc.

This keeps your code clean and consistent.

If you want to check the PostController code deeper, you can do it here, also you can check the CommentController and AuthorCotroller code.

Step 6: The Mirror on the Wall: Crafting a User Interface to View the Magic

Now that we have all the backend endpoints working, we need a user interface (UI) to interact with them easily. We’re going to build a Single Page Application (SPA) that will allow users to:

  • Create a new Post
  • Create a new Author
  • List all Posts
  • View the details and comments of a specific Post

I’ll show you how the UI looks in the following screens:

  • Post Listing
  • Create Post
  • Create Author
  • Post Details Page

To build this UI, we’ll use Thymeleaf, a modern Java template engine. Thymeleaf is commonly used with Spring Boot to generate dynamic HTML pages on the server side. While we’ll mostly use vanilla JavaScript and HTML in our UI, Thymeleaf will help us with two key things:

  1. Combining multiple HTML fragments into a single page
    We’ll break our UI into smaller HTML files (like one for listing posts, one for creating posts, etc.) to keep things organized. But in the end, we’ll combine them into a single page on the server using Thymeleaf’s fragment system.
  2. Serving the initial HTML file when visiting http://localhost:8080/
    Thymeleaf will help us render this file when the app’s root URL is accessed.

To use Thymeleaf, we first need to add the following dependency to our pom.xml:

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-thymeleaf</artifactId>

</dependency>

Creating the Main HTML Page

Now let’s create our main HTML page, app.html, inside the /src/main/resources/templates folder:

<!DOCTYPE html>

<html xmlns:th=”http://www.thymeleaf.org”>

  <head>

      <title>Post App</title>

      <meta charset=”UTF-8″>

      <link rel=”stylesheet” th:href=”@{/css/create-author.css}”>

      <link rel=”stylesheet” th:href=”@{/css/create-post.css}”>

      <link rel=”stylesheet” th:href=”@{/css/form.css}”>

      <link rel=”stylesheet” th:href=”@{/css/view-comment.css}”>

  </head>

  <body>

      <h1>Post App</h1>

      <div th:replace=”fragments/post/view-post :: view-posts”></div>

      <div th:replace=”fragments/post/view-post :: view-post-detail”></div>

      <div th:replace=”fragments/post/create-post :: view-create-post”></div>

      <div th:replace=”fragments/author/create-author :: view-create-author”></div>

      <script th:src=”@{/js/app.js}”></script>

  </body>

</html>

Let’s Break It Down

  • <html xmlns:th=”http://www.thymeleaf.org”>
    This declares the th: namespace, allowing us to use Thymeleaf attributes in our HTML (like th:href, th:replace, etc.) while keeping the HTML valid.
  • <link rel=”stylesheet” th:href=”@{/css/create-author.css}”>
    This loads a CSS file from the /src/main/resources/static/css folder. The th:href attribute dynamically resolves the path to the correct static resource.
  • <div th:replace=”fragments/post/view-post :: view-posts”></div>
    This is where Thymeleaf includes a section of HTML from another file. In this case, it loads the view-posts section from the fragments/post/view-post.html file.
    The replacement happens on the server side, and the browser sees just one complete HTML page. Thymeleaf also uses caching, so it doesn’t read and process the fragment files every time the page is requested—making it very efficient. here you can see the view-post.html file to understand better how all this works.
  • <script th:src=”@{/js/app.js}”></script>
    This includes our JavaScript file from /src/main/resources/static/js/app.js. Similar to how we load CSS, th:src ensures the path is resolved correctly at runtime.

Here You can see the whole project and check it out all the code: Post project

Step 7: Casting the Final Spell: Building and Running the App 

Now that we’ve finished developing our application, it’s time to build and run it.

We’ll use Maven to handle the build process. If you haven’t installed Maven yet, you can find instructions at https://maven.apache.org/install.html.

To build the app, open a terminal, navigate to the root folder of your project, and run:

mvn install

After the build completes, Maven will generate a target/ folder. Inside it, you’ll find a file named: post-0.0.1-SNAPSHOT.jar

To run the application, execute the following command:

java -jar post-0.0.1-SNAPSHOT.jar

Once you see a message like:

“Started PostApplication in 2.172 seconds (process running for 2.605)”

it means the application is up and running! You can now open your browser and go to http://localhost:8080 to access the home page of your app.

What next

In this post, we’ve taken a complete journey through building a simple but powerful blogging platform using Spring Boot and Thymeleaf. We started by designing and implementing our backend layer, where we used layered architecture and the DTO pattern to cleanly separate concerns between controllers, services, and data access.

Then, we moved on to building a Single Page Application (SPA) as our frontend. Even though we kept things simple with plain HTML and JavaScript, we used Thymeleaf to a couple of thing.

But I know maybe what you are thinking:

  • How can I implement an advanced post search using JpaRepository?
  • What if I want to run background tasks or scheduled jobs in Spring Boot?
  • How should I handle user authentication?
  • can I handle more complex thing with Thymeleaf?
  • Can I cache data to avoid hitting the database every time?
  • How can I implemented test with Springboot?

Those are great questions, but they’re also big topics that deserve their own deep dive. So stay tuned—more on this coming soon!