Build your first REST API with Java and Spring Boot

A free, practical and verifiable route from environment setup to GET and POST endpoints with validation.

By the end, you will have a working local API that you have tested and understood. Code does not run on Hostinger, and no employment or official certification is promised.

  • 7guided lessons
  • 5exercises
  • 12diagnostic questions
  • 4 hestimated duration
Programmer testing a REST API with Java on her computer
The code runs locally with the JDK and Maven; Hostinger only serves the PHP/MySQL campus.

A small, complete and verifiable result.

By the end, you will have a local task API with retrieval, creation, validation, errors and tests.

Versions in this edition

Java
25 LTS
Spring Boot
4.1.0
Maven
3.9.16 through Maven Wrapper only-script

For this edition, Java 25 is the current LTS release. Spring Boot 4.1.0 is a stable release compatible with Java 25 and requires at least Java 17. Maven 3.9.16 is the stable version recommended when this edition was created. Spring Boot manages transitive dependency versions; individual versions are not pinned unnecessarily.

What you will be able to do

  1. Distinguish client, server, resource, request and response.
  2. Set up JDK 25 and verify the project from the terminal, independently of the IDE.
  3. Explain why GET and POST have different semantics.
  4. Build a Spring Boot API that lists and creates tasks in memory.
  5. Validate input JSON and return understandable HTTP errors.
  6. Run automated tests and test manually with curl or PowerShell.
  7. Identify what is still needed to turn the exercise into a production backend.

Seven lessons, from the environment to testing

Work in order. Run each check on your computer and record the first useful error when something fails.

01Environment setup without depending on the IDE35 min

The application will run exclusively on your computer. You need a JDK, not just a runtime: the JDK includes the compiler and tools Maven uses. This project pins Java 25 and Spring Boot 4.1.0; the PHP campus never compiles or executes the code you write.

Minimum checks

Install an OpenJDK 25 distribution from a trusted provider. Open a new terminal and run java -version and javac -version. Both outputs must begin with 25. If they do not match, check JAVA_HOME and the order of PATH before changing the project.

Reproducible Wrapper

The repository includes mvnw, mvnw.cmd and .mvn/wrapper/maven-wrapper.properties. This is the official mode only-script: on its first run it downloads Maven 3.9.16, then reuses that distribution. On macOS or Linux, use ./mvnw -version; in Windows PowerShell, use .\mvnw.cmd -version.

Reading the structure

src/main/java contains production code, src/main/resources configuration, src/test/java tests and pom.xml the build model. The IDE imports the POM, but the terminal is the reproducible reference.

Practise now

  1. Extract starter.zip into a path without unusual characters.
  2. Run the three version checks.
  3. Run the initial test and save the output.
  4. Record the operating system, JDK provider and any corrections you make.

Verification: The Wrapper command shows Maven 3.9.16 and Java 25; the context test finishes with BUILD SUCCESS.

Common errors
  • Installing only a JRE
  • Opening the IDE before correcting PATH
  • Running global mvn instead of the Wrapper
  • Keeping the project inside a ZIP without extracting it
02HTTP and REST: the contract before the code35 min

HTTP is the exchange protocol. REST is a style for organising resources and using HTTP semantics; it does not mean that any JSON at a URL is REST.

Anatomy of an interaction

A request contains a method, URI, headers and sometimes a body. A response contains a state, headers and a representation. In GET /api/v1/tareas, the client requests a collection. In POST /api/v1/tareas, it requests creation of an item from the supplied JSON.

Semantics that matter

GET is safe: it is not intended to modify state. It is also idempotent: repeating it has the same effect, although data may change between reads. POST is not idempotent by definition; two submissions can create two resources. Successful creation returns 201 and a header named Location. An invalid JSON belongs to the 400 group; a non-existent route must not be disguised as 200.

The exercise's Contract

GET /api/v1/tareas          -> 200 + JSON list
GET /api/v1/tareas/{id}     -> 200 or 404
POST /api/v1/tareas         -> 201 + task + Location
POST with an empty title   -> 400 + problem+json

The version v1 forms part of the path to make the contract visible. It does not solve an evolution strategy by itself, but avoids silently changing things for consumers.

Practise now

  1. Draw the client, API and in-memory store.
  2. Write the method, path, state and body of the four interactions.
  3. Explain in one sentence why repeating POST can duplicate a task.

Verification: You can predict the expected state without looking at the controller.

Common errors
  • Naming routes with verbs
  • Always returning 200
  • Confusing JSON with REST
  • Claiming that POST is idempotent
03Guided creation of the Spring Boot project35 min

The project already includes a foundation so that it does not depend on a web generator. The parent spring-boot-starter-parent:4.1.0 manages compatible versions. spring-boot-starter-webmvc includes Spring MVC and the server; spring-boot-starter-validation includes Jakarta Validation.

Main class and packages

PrimeraApiApplication is in the root package com.kintavor.primeraapi. @SpringBootApplication enables configuration, auto-configuration and scanning of descendant packages. If you move the class outside the root package, Spring may stop discovering controllers and services.

First start-up

Run ./mvnw spring-boot:run or .\mvnw.cmd spring-boot:run. Look for port 8080 and a start-up without exceptions. Stop with Ctrl+C; avoid forcibly closing the terminal when an orderly shutdown is possible.

What auto-configuration does

Boot examines available classes and configuration and creates suitable infrastructure. It does not generate your business rules. If the port is occupied, temporarily use --spring-boot.run.arguments=--server.port=8081; do not kill unfamiliar processes.

Run ./mvnw test before and after each step. A successful build does not prove that the HTTP contract is correct, but eliminates one category of failures.

Practise now

  1. Open pom.xml and locate the three centrally managed versions.
  2. Start the application and record the port.
  3. Stop the application and run the context test.
  4. Make a local commit named 'Prepare the API skeleton'.

Verification: The context starts, and the learner can explain where each dependency comes from.

Common errors
  • Pinning versions of transitive Spring dependencies
  • Placing packages outside the scan
  • Changing several dependencies at once
  • Confusing starting with testing
04First GET endpoint and JSON representation35 min

The controller translates HTTP; it must not become a store or a business rule. TareaService temporarily stores tasks in memory, and TareaResponse defines what is exposed.

Route and response

@GetMapping
List<TareaResponse> listar() {
  return service.listar().stream().map(TareaResponse::from).toList();
}

The class annotation supplies /api/v1/tareas. Because the operation completes normally, Spring serialises the list and responds with 200 and JSON. A record is suitable for a small DTO because it declares its components without setters.

Separate models

Tarea represents the internal data. TareaResponse represents the external contract. Although their fields are currently similar, separating them prevents an internal change from accidentally exposing new information.

Manual test

With the application running, execute curl -i http://localhost:8080/api/v1/tareas. Check the state, Content-Type and that the response is an array. On Windows, you can use Invoke-RestMethod. Save the exact request in your notebook: a screenshot without the command is not enough to reproduce the result.

Practise now

  1. Implement or compare the collection GET.
  2. Add a second example task only during a test.
  3. Predict the order before running it and explain why it is sorted by id.
  4. Enable the GET case in the acceptance test.

Verification: GET returns 200, JSON and an ordered list without exposing the internal map.

Common errors
  • Returning the Map directly
  • Creating the service with new inside the controller
  • Modifying data through GET
  • Asserting only the size, rather than the content
05POST endpoint: create a resource and report it35 min

POST receives an intention to create. @RequestBody converts the JSON into CrearTareaRequest; the service assigns an identity, and the controller returns the created representation.

Implementation

@PostMapping
ResponseEntity<TareaResponse> crear(
    @Valid @RequestBody CrearTareaRequest request,
    UriComponentsBuilder uris) {
  Tarea creada = service.crear(request.titulo(), request.descripcion());
  URI location = uris.path("/api/v1/tareas/{id}")
      .buildAndExpand(creada.id()).toUri();
  return ResponseEntity.created(location).body(TareaResponse.from(creada));
}

The identifier is assigned on the server; accepting an arbitrary client id would introduce collisions and ambiguous rules. The Location header identifies the created resource, and the body avoids forcing the client to make another request to see it.

Manual test

curl -i -X POST http://localhost:8080/api/v1/tareas \
  -H 'Content-Type: application/json' \
  -d '{"titulo":"Leer sobre HTTP","descripcion":"Anotar estados"}'

Repeat the submission deliberately: two resources are created because we have not designed idempotence. This is a property that a real business case must decide, rather than a framework error.

Practise now

  1. Implement POST.
  2. Check 201 and Location.
  3. Use GET by id with the identifier you received.
  4. Send twice and record the difference.

Verification: The response has 201, a consistent body and a server-generated identifier.

Common errors
  • Returning 200
  • Accepting the client's id
  • Building Location through unsafe concatenation
  • Storing the DTO as the internal model
06Validation and HTTP errors that explain without leaking data35 min

Validation prevents impossible data from entering the application. The DTO declares shape and size; the service normalises; a global handler converts known exceptions into consistent responses.

Input Constraints

public record CrearTareaRequest(
  @NotBlank @Size(max = 80) String titulo,
  @Size(max = 300) String descripcion
) {}

@Valid activates these constraints when receiving the body. A title containing only spaces fails. The size limit protects contract and resources, although a real API would also limit the total body size on the server.

Problem Details

@RestControllerAdvice transforms MethodArgumentNotValidException into state 400 and adds errors for each field. The external response does not contain a stack trace. TareaNoEncontradaException is translated to 404: the request was valid, but the resource does not exist.

Two levels

Jakarta Validation checks shape at the boundary. The invariants that must hold even without HTTP belong in the domain or service. Copying the same rule into every controller creates inconsistencies.

Test an empty title, an 81-character title and a non-existent id. All three failures must be predictable and must not stop the process.

Practise now

  1. Add the request's constraints.
  2. Implement or compare the advice.
  3. Enable the validation test.
  4. Manually check a 404 and a 400.

Verification: Invalid inputs do not reach the service, and each failure returns the correct state and format.

Common errors
  • Forgetting @Valid
  • Returning a stack trace
  • Using 500 for invalid data
  • Relying only on frontend validation
07Testing the API and taking the next professional step30 min

A manual check helps exploration; an automated test preserves the contract. The solution uses MockMvc to exercise routing, JSON, validation and controller without opening a port, and a unit test for the in-memory service.

The verification cycle

  1. Predict the result and run ./mvnw test.
  2. Read the first failure, rather than the last line.
  3. Reduce the change and rerun one specific test.
  4. Run the complete suite before finishing.

A useful assertion checks the state and important data. Checking only that a response exists would allow silent errors. The validation test also demonstrates that the service does not accept a body without a title.

What this demo does not attempt to solve

Data disappears on restart. There is no PostgreSQL, authentication, roles, transactions, pagination, OpenAPI or deployment. This limitation is deliberate: first understand the complete HTTP flow, then replace the store without distorting the contract.

Conclusion

Complete the five exercises, take the diagnostics and compare your version with solution.zip only after recording your decisions. The solution is a reference, rather than the only valid implementation.

Practise now

  1. Run all tests.
  2. Cause a failure by temporarily changing an expected state.
  3. Restore the code and explain the first useful line of the diagnostics.
  4. Write down three risks for a version with real users.

Verification: The suite passes, and the learner explains what it covers and what it does not.

Common errors
  • Changing the test to accept the bug
  • Depending on execution order
  • Sharing mutable data
  • Confusing a passing suite with safe production operation

Two separate ZIP files for purposeful work.

Start with the starter project. Consult the solution only after running tests, recording your decisions and using the hints.

Starter project

Buildable Maven foundation, instructions, acceptance cases and a request collection.

Download starter.zip
Reference result

GET, POST, validation, Problem Details and complete tests to compare decisions.

Download solution.zip
Security: KINTAVOR does not receive or run your project. Do not upload keys, tokens or real data to the repository.

Five exercises with progressive hints

Read the instructions, produce evidence and open the hints one at a time. The solution is at the end of each exercise.

  1. 01

    Diagnose the environment

    Run java -version, javac -version and the Maven Wrapper. Build a table with the observed version, required version and action if they differ. Describe a scenario in which java is 25 and javac is 21.

    Evidence: A table and output without sensitive personal paths.

    Open hints
    1. Java and javac may come from different installations.
    2. Check JAVA_HOME and the order of PATH.
    3. Verify the correction in a new terminal, rather than only in the IDE.
    Compare with the reasoned solution

    All three tools must identify Java 25, and Maven must be 3.9.16. If java and javac differ, locate both paths, point JAVA_HOME to JDK 25 and put its bin folder first in PATH. Close and reopen the terminal, repeat the check and do not change the POM to hide the problem.

  2. 02

    Add a completed-tasks filter

    Extend GET /api/v1/tareas with an optional completada parameter. Without the parameter, return all tasks; with true or false, filter them. Do not duplicate routes or modify state.

    Evidence: A diff, three tests and curl examples.

    Open hints
    1. Use @RequestParam(required=false).
    2. The service can accept Boolean to distinguish an absent value.
    3. Add three tests: absent, true and false.
    Compare with the reasoned solution

    The controller receives Boolean completada and delegates. The service returns the ordered list and applies filter only when completada is not null. GET remains safe; tests create controlled data or check presence without depending on global ids.

  3. 03

    Correct a misleading POST

    Review a version that responds with 200, accepts id in the JSON and omits Location. List the violations, correct the contract and create a test that would have detected each one.

    Evidence: A list of violations and a before/after test.

    Open hints
    1. Creation has a specific state.
    2. Identity belongs to the server in this design.
    3. Location can be checked using a regular expression or prefix.
    Compare with the reasoned solution

    The request removes id; the service generates it. The controller uses ResponseEntity.created(location), so it responds with 201 and Location. Tests reject or ignore unsupported fields according to an explicit policy, assert isCreated and verify /api/v1/tareas/{id}.

  4. 04

    Design observable validation

    Create a partition table for title and description. Implement tests for empty input, spaces, the exact limit and one character over it. Check the problematic field without depending on the complete message text.

    Evidence: A partition table and a passing suite.

    Open hints
    1. The limits are 80 and 300.
    2. @NotBlank distinguishes spaces from content.
    3. Asserting the key in the errors map is more stable than asserting localised text.
    Compare with the reasoned solution

    Minimum partitions include null/empty/spaces, and lengths 1, 80 and 81 for the title; absent, 300 and 301 for the description. Valid cases expect 201 and invalid cases 400 with errors.titulo or errors.descripcion.

  5. 05

    Plan the move to PostgreSQL

    Without adding new dependencies, design how to replace the map with PostgreSQL. Include a table, constraints, repository interface, transaction boundary, migration and tests. Retain the HTTP contract unless there is a documented reason to change it.

    Evidence: A diagram, DDL, interface and test strategy.

    Open hints
    1. Start with the data model, rather than annotations.
    2. The service should not know SQL.
    3. A integration must test against PostgreSQL semantics or state the approximation used.
    Compare with the reasoned solution

    Propose the table tarea(id identity PK, titulo varchar(80) not null, descripcion varchar(300), completada boolean not null). Define a Tareas port with listar, buscar and guardar; a JPA or JDBC implementation belongs in infrastructure. Creation is a short transaction. Flyway versions the schema. Retain routes and DTOs, add unit tests for the use case and an isolated integration with optional local PostgreSQL.

Diagnostics: do you understand the flow of your first API?

Select one answer per question before consulting the explanation. The test guides your study; it is not an official certification.

1. Which check demonstrates that a Java 25 compiler is available?
2. What specific advantage does ./mvnw offer over mvn?
3. What is the main responsibility of GET /api/v1/tareas?
4. A task is created successfully. Which response best communicates the result?
5. Why does the client not send the id in CrearTareaRequest?
6. Which combination rejects an empty or spaces-only title and limits its size?
7. Where should @Valid be applied to validate the DTO received by the controller?
8. What is the correct difference between 400 and 404 in this API?
9. Why use TareaResponse instead of returning the internal Map?
10. A POST test checks only that no exception occurred. What is missing at a minimum?
11. What happens to the example's tasks when the application restarts?
12. Which statement best describes the mini-course's scope?

Your next step: persistence, security and architecture.

Continue with KINTAVOR Professional Java Backend to turn this first vertical slice into applications with Java, SQL, PostgreSQL, JPA, security, comprehensive testing, optional local Docker, CI, cloud and six portfolio projects.

KINTAVOR provides private education. Its full-course certificate is its own and is not official. There is no affiliation with Oracle, OpenJDK, Spring, VMware or Broadcom; trademarks are used descriptively.

View the full courseRequest a campus trial