ProjectCore· 180 min read

Project: Build a Task API with a Database

Combine everything into one real back-end: a Task REST API with full CRUD, a JPA database, validation and clean errors.

What you will learn

  • Build a complete CRUD REST API end to end
  • Persist data with a JPA entity and repository
  • Add validation and proper error handling

What you will build

You will build a Task API — the back-end for a to-do app. By the end it will store tasks in a database and let any front-end create, list, update, complete and delete them through clean REST endpoints. This is exactly the kind of API a junior Java developer builds on the job.

You have learned every piece already. Now you assemble them.

The whole build, in order

Here is the complete plan as numbered steps. We build it in exactly this order below — get each step working before moving to the next:

  1. Create the project on start.spring.io with four dependencies (Spring Web, Spring Data JPA, H2, Validation) and confirm it starts.
  2. Write the Task entity so JPA can store each task as a row, with a @NotBlank rule on the title.
  3. Write the repository interface to get save, findAll, findById, deleteById (plus a custom finder) for free.
  4. Write the CRUD controller that wires the five endpoints to the repository, with @Valid for validation and orElseThrow for a 404.
  5. Add central error handling (NotFoundException + @RestControllerAdvice) so a missing task returns a tidy 404 JSON.
  6. Test every endpoint with Postman or curl: create, read all, read one, update, validate, 404, and delete.

Step 1 — Create the project

On start.spring.io, make a Maven + Java project with these dependencies: Spring Web, Spring Data JPA, the H2 Database, and Validation. Run it once to confirm Tomcat starts on port 8080.

Step 2 — The Task entity

Model a task as a JPA entity, with a validation rule on the title:

Step 2: the Task entity with a validation rule
@Entity
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Title is required")
    private String title;

    private boolean done = false;

    protected Task() { }              // JPA needs this
    public Task(String title) { this.title = title; }
    // getters and setters for id, title, done ...
}

Note: Output: On startup JPA creates the table: task(id BIGINT PK, title VARCHAR, done BOOLEAN) Each Task object will become a row. @NotBlank will block tasks with an empty title once we add @Valid.

Step 3 — The repository

One empty interface gives you all the database methods, plus a bonus custom finder:

Step 3: the repository, with a custom finder for free
public interface TaskRepository
        extends JpaRepository<Task, Long> {
    List<Task> findByDone(boolean done);   // bonus custom finder
}

Note: Output: (No output — Spring now provides save, findAll, findById, deleteById, plus your findByDone, with no implementation written.)

Step 4 — The CRUD controller

Wire up all the endpoints, with validation and a clean 404:

Step 4: the full CRUD controller with validation and 404
@RestController
@RequestMapping("/tasks")
public class TaskController {
    private final TaskRepository repo;

    public TaskController(TaskRepository repo) {
        this.repo = repo;
    }

    @GetMapping
    public List<Task> all() { return repo.findAll(); }

    @GetMapping("/{id}")
    public Task one(@PathVariable Long id) {
        return repo.findById(id)
            .orElseThrow(() -> new NotFoundException("No task " + id));
    }

    @PostMapping
    public Task create(@Valid @RequestBody Task task) {
        return repo.save(task);
    }

    @PutMapping("/{id}")
    public Task update(@PathVariable Long id, @Valid @RequestBody Task task) {
        task.setId(id);
        return repo.save(task);
    }

    @DeleteMapping("/{id}")
    public String delete(@PathVariable Long id) {
        repo.deleteById(id);
        return "Deleted task " + id;
    }
}

Note: Output: POST /tasks {"title":"Learn Spring Boot"} -> {"id":1,"title":"Learn Spring Boot","done":false} GET /tasks -> [{"id":1,"title":"Learn Spring Boot","done":false}] PUT /tasks/1 {"title":"Learn Spring Boot","done":true} -> {"id":1,"title":"Learn Spring Boot","done":true} GET /tasks/999 -> 404 Not Found: No task 999 POST /tasks {"title":""} -> 400 Bad Request: Title is required DELETE /tasks/1 -> Deleted task 1 Every feature works: create, list, fetch one, update, validate, 404, and delete — a complete back-end.

Step 5 — Central error handling

Add the NotFoundException and a @RestControllerAdvice from the error-handling lesson so missing tasks return a tidy 404 JSON. Your API now behaves professionally for every case.

Your tasks

  • Get the project running with the four dependencies.
  • Build the Task entity, repository and CRUD controller.
  • Add validation (@Valid) so empty titles are rejected with a 400.
  • Add error handling so a missing task returns a clean 404.
  • Test every endpoint with Postman or curl.

Stretch goals

  • Add an endpoint GET /tasks/done that uses findByDone(true).
  • Add Spring Security so changes require a login (reading stays open).
  • Package it as a jar and run it with java -jar.

Tip: Build in small steps and test after each one: get GET working before POST, POST before PUT. A small finished API beats a big broken one — exactly the habit real developers follow.

Note: When this works you have built a complete, database-backed REST API with validation, error handling and (optionally) security — the core job of a back-end Java developer. Push it to GitHub and add it to your portfolio!

Q. In your finished Task API, which annotation makes the empty-title rule actually reject a POST /tasks {"title":""} request with a 400?

Answer: The @NotBlank rule on the Task title is inert until @Valid is placed before the @RequestBody parameter. @Valid tells Spring to enforce the rules and return 400 Bad Request for an empty title. @Entity, @RequestMapping and @GeneratedValue do other jobs (table mapping, base path, id generation).

✍️ Practice

  1. Build the full Task API meeting all five required tasks and test every endpoint.
  2. Implement at least one stretch goal (the /tasks/done endpoint or the jar build).

🏠 Homework

  1. Rebuild the same API for a different idea — a Note API (title, body) or a Product API (name, price, inStock) — with full CRUD, validation and 404 handling, and write a short README explaining how to run it.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →