REST API Backend
Single Page Application Frontend
20-Lesson Intensive Course | 2 Hours Daily
Lessons 1-5: Building Robust REST APIs
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
// Book.java
public class Book {
private Long id;
private String title;
private String author;
// constructors, getters, setters
public Book() {}
public Book(Long id, String title, String author) {
this.id = id;
this.title = title;
this.author = author;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
// BookController.java (modified for PUT and DELETE - in-memory)
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.http.HttpStatus;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
// Assuming Book class is defined as in Lesson 2 - Part 2
@RestController
@RequestMapping("/api/book")
public class BookController {
// Re-using the in-memory list and nextId from Lesson 2 - Part 2 for demonstration
private List<Book> book = new ArrayList<>();
private Long nextId = 1L;
public BookController() {
book.add(new Book(nextId++, "Book Title 1", "Book Author 1"));
book.add(new Book(nextId++, "Book Title 2", "Book Author 2"));
}
@GetMapping
public List<Book> getAllBook() {
return book;
}
@GetMapping("/{id}")
public Book getBookById(@PathVariable Long id) {
return book.stream()
.filter(bookItem -> bookItem.getId().equals(id))
.findFirst()
.orElse(null);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Book createBook(@RequestBody Book book) {
book.setId(nextId++);
this.book.add(book);
return book;
}
@PutMapping("/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book updatedBook) {
return book.stream()
.filter(bookItem -> bookItem.getId().equals(id))
.findFirst()
.map(bookItem -> {
bookItem.setTitle(updatedBook.getTitle());
bookItem.setAuthor(updatedBook.getAuthor());
return bookItem;
})
.orElseThrow(() -> new IllegalArgumentException("Book not found"));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteBook(@PathVariable Long id) {
if (!book.removeIf(bookItem -> bookItem.getId().equals(id))) {
throw new IllegalArgumentException("Book not found");
}
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
// Book.java (Entity)
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
// constructors, getters, setters
public Book() {}
public Book(Long id, String title, String author) {
this.id = id;
this.title = title;
this.author = author;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
// BookRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {
}
// BookController.java (modified for JPA Repository)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import java.util.List;
@RestController
@RequestMapping("/api/book")
public class BookController {
// 1. Inject BookRepository into BookController
@Autowired
private BookRepository bookRepository;
// 2. Replace in-memory list with repository calls
// 3. Update GET endpoints to use repository.findAll()
@GetMapping
public List<Book> getAllBook() {
return bookRepository.findAll();
}
@GetMapping("/{id}")
public Book getBookById(@PathVariable Long id) {
return bookRepository.findById(id).orElse(null);
}
// 4. Update POST endpoint to use repository.save()
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Book createBook(@RequestBody Book book) {
return bookRepository.save(book);
}
// 5. Update PUT endpoint to use repository.save()
@PutMapping("/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book updatedBook) {
return bookRepository.findById(id)
.map(book -> {
book.setTitle(updatedBook.getTitle());
book.setAuthor(updatedBook.getAuthor());
return bookRepository.save(book);
})
.orElseThrow(() -> new IllegalArgumentException("Book not found"));
}
// 6. Update DELETE endpoint to use repository.deleteById()
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteBook(@PathVariable Long id) {
if (!bookRepository.existsById(id)) {
throw new IllegalArgumentException("Book not found");
}
bookRepository.deleteById(id);
}
}
# application.yaml
spring:
application:
name: BookIJ
h2:
console:
enabled: true
datasource:
url: jdbc:h2:mem:bookdb
driverClassName: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
# Test H2 console at:
http://localhost:8080/h2-console
docker compose up -d # start the container in detached mode
docker ps # list all containers
docker stop <container_id> # stop a container
docker rm <container_id> # remove a container
docker exec -it <container_id> bash # open a shell to a container
docker logs <container_id> # view logs of a container
docker stats # view stats of all containers
docker top <container_id> # view processes of a container
docker inspect <container_id> # view detailed information of a container
# docker-compose.yaml
name: frontoffice
services:
postgresql:
image: postgres:17.4
# volumes:
# - ~/Downloads/frontoffice/frontoffice_db/postgresql/:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=frontoffice_user
- POSTGRES_DB=frontoffice_db
- POSTGRES_HOST_AUTH_METHOD=trust
healthcheck:
test: ['CMD-SHELL', 'pg_isready --username=$${POSTGRES_USER} --dbname=$${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
# If you want to expose these ports outside your dev PC,
# remove the "127.0.0.1:" prefix
ports:
- 127.0.0.1:5432:5432
spring:
application:
name: frontoffice
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/frontoffice_db
username: frontoffice_user
password:
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
show-sql: true
// BAD: Showing the kitchen to customers
@RestController
public class DishController {
public Dish getDish() {
return dishRepository.findById(1L);
// Returns everything: IDs, timestamps,
// internal fields, kitchen secrets!
}
}
// GOOD: Show customers only what they need to see
public class DishDTO {
private String name; // What customers see
private String type; // What customers see
private String price; // What customers see
private String ingredients; // What customers see
// NO internal details!
}
@RestController
public class DishController {
public DishDTO getDish() {
Dish dish = dishRepository.findById(1L);
return convertToMenu(dish); // Only show menu items
}
}
@Entity
public class Book {
@Id
private Long id; // ID
private String title; // Menu item
private String author; // Menu item
private String internalCode; // secret (internal)
}
public class BookDTO {
private String title; // What customers see
private String author; // What customers see
// No IDs, no timestamps, no internal codes!
}
// Entity exposes everything:
{
"id": 123,
"title": "Spring Boot Guide",
"author": "John Doe",
"internalCode": "SECRET_123", // OOPS! Security leak!
"createdBy": "admin" // OOPS! Exposed internal user!
}
// DTO shows only safe fields:
{
"title": "Spring Boot Guide",
"author": "John Doe"
// No security risks!
}
// You can change your kitchen without changing the menu:
public class Book {
// Changed internal field names
private Long internalId; // Was 'id'
private String bookTitle; // Was 'title'
// Kitchen changed, menu stays the same!
}
public class BookDTO {
private String title; // Menu unchanged!
private String author; // Menu unchanged!
}
// DTO lets you serve exactly what's needed:
public class BookDTO {
private String title;
private String author;
// No unnecessary data from database
}
// Book.java
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private String internalCode;
// Constructors
public Book() {}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getInternalCode() {
return internalCode;
}
public void setInternalCode(String internalCode) {
this.internalCode = internalCode;
}
}
// BookDTO.java
public class BookDTO {
private Long id;
private String title;
private String author;
// Constructors
public BookDTO() {}
public BookDTO(Long id, String title, String author) {
this.id = id;
this.title = title;
this.author = author;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
// BookService.java
package nl.tochbedrijf.frontoffice.services;
import nl.tochbedrijf.frontoffice.domain.Book;
import nl.tochbedrijf.frontoffice.repository.BookRepository;
import nl.tochbedrijf.frontoffice.services.dtos.BookDTO;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public List<BookDTO> getAllBooks() {
return bookRepository.findAll()
.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
public BookDTO getBookById(Long id) {
return bookRepository.findById(id)
.map(this::convertToDto)
.orElseThrow(() ->
new RuntimeException(
"Book not found with ID: " + id));
}
public List<BookDTO> findBooksByTitleContains(String title) {
return bookRepository.findBooksByTitleContains(title)
.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
public BookDTO createBook(BookDTO bookDTO) {
Book newBook = convertToEntity(bookDTO);
Book savedBook = bookRepository.save(newBook);
return convertToDto(savedBook);
}
public BookDTO updateBook(Long id, BookDTO updatedBook) {
return bookRepository.findById(id)
.map(bookItem -> {
bookItem.setTitle(updatedBook.getTitle());
bookItem.setAuthor(updatedBook.getAuthor());
return convertToDto(bookRepository.save(bookItem));
})
.orElseThrow(() ->
new RuntimeException(
"Book not found with ID: " + id));
}
public void deleteBook(Long id) {
if (bookRepository.existsById(id)) {
bookRepository.deleteById(id);
} else {
throw new RuntimeException("Book not found with ID: " + id);
}
}
// Utils code
private BookDTO convertToDto(Book book) {
return new BookDTO(book.getId(), book.getTitle(), book.getAuthor());
}
private Book convertToEntity(BookDTO bookDTO) {
Book book = new Book();
book.setId(bookDTO.getId()); // ID might be null for new book
book.setTitle(bookDTO.getTitle());
book.setAuthor(bookDTO.getAuthor());
book.setInternalCode(UUID.randomUUID().toString());
return book;
}
}
// BookController.java (modified)
import nl.tochbedrijf.frontoffice.services.BookService;
import nl.tochbedrijf.frontoffice.services.dtos.BookDTO;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping
public ResponseEntity<List<BookDTO>> getAllBooks() {
return ResponseEntity.ok(bookService.getAllBooks());
}
@GetMapping("/{id}")
public ResponseEntity<BookDTO> getBookById(@PathVariable Long id) {
return ResponseEntity.ok(bookService.getBookById(id));
}
@GetMapping("/titleContains/{title}")
public ResponseEntity<List<BookDTO>> findBooksByTitleContains(@PathVariable String title) {
return ResponseEntity.ok(bookService.findBooksByTitleContains(title));
}
@PostMapping
public ResponseEntity<BookDTO> createBook(@RequestBody BookDTO bookDTO) {
return ResponseEntity.ok(bookService.createBook(bookDTO));
}
@PutMapping("/{id}")
public ResponseEntity<BookDTO> updateBook(@PathVariable Long id, @RequestBody BookDTO bookDTO) {
return ResponseEntity.ok(bookService.updateBook(id, bookDTO));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
return ResponseEntity.noContent().build();
}
}
// BookNotFoundException.java
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(String message) {
super(message);
}
}
// GlobalExceptionHandler.java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<String> handleBookNotFoundException(BookNotFoundException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
// Generic exception handler
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException(Exception ex) {
return new ResponseEntity<>("An unexpected error occurred: "
+ ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// BookController.java (modified for exception handling)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import java.util.List;
@RestController
@RequestMapping("/api/book")
public class BookController {
@Autowired
private BookRepository bookRepository;
@GetMapping
public List<Book> getAllBook() {
return bookRepository.findAll();
}
@GetMapping("/{id}")
public Book getBookById(@PathVariable Long id) {
return bookRepository.findById(id)
.orElseThrow(() -> new BookNotFoundException("Book not found with ID: " + id));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Book createBook(@RequestBody Book book) {
return bookRepository.save(book);
}
@PutMapping("/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book updatedBook) {
return bookRepository.findById(id)
.map(book -> {
book.setTitle(updatedBook.getTitle());
book.setAuthor(updatedBook.getAuthor());
return bookRepository.save(book);
})
.orElseThrow(() -> new BookNotFoundException("Book not found with ID: " + id));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteBook(@PathVariable Long id) {
if (!bookRepository.existsById(id)) {
throw new BookNotFoundException("Book not found with ID: " + id);
}
bookRepository.deleteById(id);
}
}
// BookController.java (move business logic to service layer)
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/book")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping
public List<BookDTO> getBookList() {
return bookService.getAll();
}
@GetMapping("/{id}")
public BookDTO getBookById(@PathVariable Long id) {
return bookService.getById(id);
}
@GetMapping("/titleContains/{title}")
public List<BookDTO> getBooksByTitleContains(
@PathVariable String title) {
return bookService.findByTitleContains(title);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public BookDTO createBook(@RequestBody BookDTO bookDTO) {
return bookService.create(bookDTO);
}
@PutMapping("/{id}")
public BookDTO updateBook(@PathVariable Long id
, @RequestBody BookDTO updatedBook) {
return bookService.update(id, updatedBook);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteBook(@PathVariable Long id) {
bookService.delete(id);
}
}
// BookRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface BookRepository extends JpaRepository<Book, Long> {
List<Book> findBooksByTitleContains(String title);
}
// BookService.java
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public List<BookDTO> getAll() {
return bookRepository.findAll()
.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
public BookDTO getById(Long id) {
Optional<Book> optById = bookRepository.findById(id);
if (optById.isPresent()) {
return convertToDto(optById.get());
} else {
throw new BookNotFoundException(
"Book not found with ID: " + id);
}
}
public List<BookDTO> findByTitleContains(String title) {
return bookRepository.findBooksByTitleContains(title)
.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
public BookDTO create(BookDTO bookDTO) {
Book newBook = convertToEntity(bookDTO);
Book savedBook = bookRepository.save(newBook);
return convertToDto(savedBook);
}
public BookDTO update(Long id, BookDTO updatedBook) {
return bookRepository.findById(id)
.map(bookItem -> {
bookItem.setTitle(updatedBook.getTitle());
bookItem.setAuthor(updatedBook.getAuthor());
return convertToDto(bookRepository.save(bookItem));
})
.orElseThrow(() ->
new BookNotFoundException(
"Book not found with ID: " + id));
}
public void delete(Long id) {
if (bookRepository.existsById(id)) {
bookRepository.deleteById(id);
} else {
throw new BookNotFoundException("Book not found with ID: " + id);
}
}
// Utils code
private BookDTO convertToDto(Book book) {
return new BookDTO(book.getId(), book.getTitle(), book.getAuthor());
}
private Book convertToEntity(BookDTO bookDTO) {
Book book = new Book();
book.setId(bookDTO.getId()); // ID might be null for new book
book.setTitle(bookDTO.getTitle());
book.setAuthor(bookDTO.getAuthor());
book.setInternalCode(UUID.randomUUID().toString());
return book;
}
}
You are now a Full-Stack Developer!
Keep Building, Keep Learning! 🚀
#FullStackMastery