Building a REST API with Java, Spring Boot, and PostgreSQL – Best Practices & Full Example

3 min read

Building a REST API with Java, Spring Boot, and PostgreSQL – Best Practices & Full Example

Developing RESTful APIs is a cornerstone of modern backend development. When using Java, Spring Boot is the go-to framework for quickly building robust and scalable APIs. Coupled with PostgreSQL, you get a powerful combination for handling data reliably.

In this article, you’ll learn:

  • The best practices for building REST APIs with Spring Boot
  • How to structure a Spring Boot project
  • How to connect to PostgreSQL
  • A full working example (CRUD API for a Book entity)

Best Practices Overview

Before diving into code, let’s establish some best practices:

AreaBest Practice
Project StructureFollow a layered architecture (Controller → Service → Repository)
DTO UsageUse DTOs to separate internal models from API contracts
ValidationUse javax.validation annotations for request validation
Exception HandlingUse @ControllerAdvice to handle errors globally
Database AccessUse Spring Data JPA with proper pagination and sorting
SecuritySecure endpoints with authentication (out of scope for this example)
DocumentationUse Swagger/OpenAPI for documentation

Project Setup

Tools Used

  • Java 17
  • Spring Boot 3.x
  • PostgreSQL
  • Maven
  • IntelliJ

Dependencies (in pom.xml)

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.0.2</version>
  </dependency>
</dependencies>

Project Structure

src/main/java/com/example/bookapi
├── controller
│   └── BookController.java
├── dto
│   └── BookDto.java
├── entity
│   └── Book.java
├── exception
│   ├── GlobalExceptionHandler.java
│   └── ResourceNotFoundException.java
├── repository
│   └── BookRepository.java
├── service
│   └── BookService.java
├── BookApiApplication.java

application.yml (or application.properties)

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/bookdb
    username: postgres
    password: yourpassword
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect

Entity

// entity/Book.java
package com.example.bookapi.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String author;
}

DTO

// dto/BookDto.java
package com.example.bookapi.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.*;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BookDto {
    private Long id;

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

    @NotBlank(message = "Author is required")
    private String author;
}

Repository

// repository/BookRepository.java
package com.example.bookapi.repository;

import com.example.bookapi.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {}

Service Layer

// service/BookService.java
package com.example.bookapi.service;

import com.example.bookapi.dto.BookDto;
import com.example.bookapi.entity.Book;
import com.example.bookapi.exception.ResourceNotFoundException;
import com.example.bookapi.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BookService {

    @Autowired
    private BookRepository bookRepository;

    public BookDto createBook(BookDto dto) {
        Book book = Book.builder()
                .title(dto.getTitle())
                .author(dto.getAuthor())
                .build();
        return toDto(bookRepository.save(book));
    }

    public List<BookDto> getAllBooks() {
        return bookRepository.findAll().stream().map(this::toDto).toList();
    }

    public BookDto getBookById(Long id) {
        Book book = bookRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Book not found with id " + id));
        return toDto(book);
    }

    public BookDto updateBook(Long id, BookDto dto) {
        Book book = bookRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Book not found"));
        book.setTitle(dto.getTitle());
        book.setAuthor(dto.getAuthor());
        return toDto(bookRepository.save(book));
    }

    public void deleteBook(Long id) {
        Book book = bookRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Book not found"));
        bookRepository.delete(book);
    }

    private BookDto toDto(Book book) {
        return BookDto.builder()
                .id(book.getId())
                .title(book.getTitle())
                .author(book.getAuthor())
                .build();
    }
}

Controller

// controller/BookController.java
package com.example.bookapi.controller;

import com.example.bookapi.dto.BookDto;
import com.example.bookapi.service.BookService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/books")
public class BookController {

    @Autowired
    private BookService bookService;

    @PostMapping
    public BookDto createBook(@RequestBody @Valid BookDto dto) {
        return bookService.createBook(dto);
    }

    @GetMapping
    public List<BookDto> getAllBooks() {
        return bookService.getAllBooks();
    }

    @GetMapping("/{id}")
    public BookDto getBook(@PathVariable Long id) {
        return bookService.getBookById(id);
    }

    @PutMapping("/{id}")
    public BookDto updateBook(@PathVariable Long id, @RequestBody @Valid BookDto dto) {
        return bookService.updateBook(id, dto);
    }

    @DeleteMapping("/{id}")
    public void deleteBook(@PathVariable Long id) {
        bookService.deleteBook(id);
    }
}

Global Exception Handler

// exception/ResourceNotFoundException.java
package com.example.bookapi.exception;

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}
// exception/GlobalExceptionHandler.java
package com.example.bookapi.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> handleNotFound(ResourceNotFoundException ex) {
        return new ResponseEntity<>(Map.of("error", ex.getMessage()), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleValidation(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

Test with Swagger

Once the application is running, navigate to:

http://localhost:8080/swagger-ui.html

You’ll see an interactive Swagger UI to test all endpoints.


Summary

You’ve built a production-ready, well-structured REST API using Java, Spring Boot, and PostgreSQL with best practices:

Layered architecture
DTO and validation
Exception handling
Swagger documentation
Clean code with Lombok and JPA

🤞 Never miss a story from us, get weekly updates to your inbox!

Leave a Reply

Your email address will not be published. Required fields are marked *