APEX Educational Institute

Build a REST API with Spring Boot Step by Step (CRUD Example)

Create a complete CRUD REST API in Spring Boot with an entity, repository, service and controller, plus validation, proper HTTP status codes and testing with curl.

Intermediate | 3 min read | Updated

Spring Boot is the most popular way to build Java backends. In this tutorial you build a small Course API with Create, Read, Update and Delete (CRUD) endpoints using Spring Web and Spring Data JPA.

Project setup

Go to start.spring.io and choose:

  • Project: Maven, Language: Java 21, latest stable Spring Boot 3.x
  • Dependencies: Spring Web, Spring Data JPA, Validation, H2 Database

H2 is an in-memory database, so you can run everything without installing a database. Switch to PostgreSQL or MySQL later by changing the dependency and application.properties.

The layers

LayerResponsibility
EntityMaps a Java class to a database table
RepositoryDatabase access (Spring generates the code)
ServiceBusiness rules
ControllerHTTP endpoints and status codes

Step 1: Entity

java
@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    private String title;

    @Min(1)
    private int durationWeeks;

    // getters and setters
}

Step 2: Repository

java
public interface CourseRepository extends JpaRepository<Course, Long> {
    List<Course> findByTitleContainingIgnoreCase(String text);
}

Spring Data creates the SQL for save, findById, findAll, deleteById and even the custom findByTitleContainingIgnoreCase from the method name.

Step 3: Service

java
@Service
public class CourseService {
    private final CourseRepository repo;

    public CourseService(CourseRepository repo) { this.repo = repo; }

    public List<Course> all() { return repo.findAll(); }

    public Course get(Long id) {
        return repo.findById(id)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Course not found"));
    }

    public Course create(Course c) { return repo.save(c); }

    public Course update(Long id, Course input) {
        Course c = get(id);
        c.setTitle(input.getTitle());
        c.setDurationWeeks(input.getDurationWeeks());
        return repo.save(c);
    }

    public void delete(Long id) { repo.delete(get(id)); }
}

Constructor injection (no @Autowired needed) makes the class easy to unit test.

Step 4: Controller

java
@RestController
@RequestMapping("/api/courses")
public class CourseController {
    private final CourseService service;

    public CourseController(CourseService service) { this.service = service; }

    @GetMapping
    public List<Course> list() { return service.all(); }

    @GetMapping("/{id}")
    public Course get(@PathVariable Long id) { return service.get(id); }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Course create(@Valid @RequestBody Course c) { return service.create(c); }

    @PutMapping("/{id}")
    public Course update(@PathVariable Long id, @Valid @RequestBody Course c) { return service.update(id, c); }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) { service.delete(id); }
}

@Valid triggers the @NotBlank and @Min checks; invalid input returns 400 Bad Request automatically.

Step 5: Test with curl

bash
curl -X POST localhost:8080/api/courses -H "Content-Type: application/json" \
     -d '{"title":"Playwright with AI","durationWeeks":8}'

curl localhost:8080/api/courses
curl -X PUT localhost:8080/api/courses/1 -H "Content-Type: application/json" \
     -d '{"title":"Playwright + AI","durationWeeks":10}'
curl -X DELETE localhost:8080/api/courses/1 -i

REST status codes to remember

CodeMeaningWhen
200OKSuccessful GET or PUT
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestValidation failed
404Not FoundID does not exist

Interview questions

  • `@Controller` vs `@RestController`? @RestController = @Controller + @ResponseBody, so return values are written as JSON.
  • PUT vs PATCH? PUT replaces the whole resource; PATCH updates only the given fields.
  • Why use DTOs? To avoid exposing entity internals and to control exactly what the API accepts and returns.

Next steps

Add DTOs, global exception handling with @RestControllerAdvice, and pagination with Pageable. Learn security, microservices and deployment in the Java + Spring Boot + AI course.

Master it hands-on

Development + AI

Java + Spring Boot + AI

Java, Spring Boot, Hibernate, REST APIs, microservices and React, with AI.

26 weeks Beginner to Advanced
Online LiveRecorded Course