Hello everyone, today we'll look at how we can use Java and Spring Boot to create a simple book API that performs basic CRUD operations. We will also use MongoDb, a NoSQL database, to store the data.
Prerequisite
๐ฏ Integrated Development Environment (IntelliJ IDEA recommended) ๐ฏ Have Java installed on your computer ๐ฏ Java fundamentals ๐ฏ Postman for API testing ๐ฏ MongoDB Atlas or MongoDB Compass
โ Project Setup
Go to the Spring initializr website and select the necessary setup. Link
Next, unzip the folder and open it with your IDE, in my case IntelliJ, and create a project out of the pom.xml
file.
Let's quickly change our port number to '3456'. You can use the default, '8080,' but that port is already in use by another service on my machine.
Starting your application
To begin, navigate to your main entry file, in my case the YomiApplication
class, and click the green play button highlighted below.
After a successful build, your server should be launched at the port we specified earlier.
Packages
Let's now create packages to modularize our application.
A package in Java is used to group related classes. Think of it as a folder in a file directory. We use packages to avoid name conflicts, and to write a better maintainable code.
Model
Let's create our book entity, or the fields that our Book model will have. A model class is typically used in your application to "shape" the data. For example, you could create a Model class that represents a database table or a JSON document.
Create a Book Class within the model package and write the code snippet below.
package com.example.yomi.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
@Setter //@Setter is a Lombok annotation that generates setter methods for all fields in the class.
@Getter //@Getter is a Lombok annotation that generates getter methods for all fields in the class.
@AllArgsConstructor //@AllArgsConstructor is a Lombok annotation that generates a constructor with all fields in the class.
@NoArgsConstructor //@NoArgsConstructor is a Lombok annotation that generates a constructor with no arguments.
@Document(collection = "books") // @Document annotation is used to specify the collection name (MongoDB collection name)
public class Book {
@Id
private String id; // private means only this class can access this field
private String name;
private String title;
private Boolean published;
private String author;
private Date createdAt;
private Date updatedAt; // Date is a class in Java that represents a date and time
}
Book service
Let's move on to the service now that we've structured our Book A client will use a Service class to interact with some functionality in your application.
We will create a BookService Interface and a BookServiceImplementation Class in the service package.
.
We will implement a method to create a single book in the BookService
interface.
package com.example.yomi.service;
import com.example.yomi.model.Book; // we import the Book model class
public interface BookService { // interface is a contract that defines the behavior of a class
public void createBook(Book book); // the `book` in lowercase is the name of the parameter that we will pass to the method
// the `Book` in uppercase is the name of the class that we will pass to the method
}
Let us now put this createBook method into action in the BookServiceImpl
class
package com.example.yomi.service;
import com.example.yomi.model.Book;
import com.example.yomi.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service // this annotation tells Spring that this class is a service
public class BookServiceImpl implements BookService{ // we implement the BookService
@Autowired // this annotation tells Spring to inject the BookRepository dependency
private BookRepository bookRepository; // we inject the BookRepository dependency
public void createBook(Book book) {
book.setCreatedAt(new Date(System.currentTimeMillis()));
bookRepository.save(book);
}
}
Before proceeding, we must connect our application to MongoDB. We will proceed in two stages.
1)
Enter the mongodb configuration in the application.properties
file.
spring.data.mongodb.uri=mongodb://127.0.0.1:27017/book-app
spring.data.mongodb.database=book-app
2) In the book repository file BookRepository
package com.example.yomi.repository;
import com.example.yomi.model.Book;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
// Repository should be created as an interface
@Repository
public interface BookRepository extends MongoRepository<Book, String> {
// we extend the MongoRepository interface and pass the Book model class and the type of the id field
}
Let's finish our 'BookServiceImpl' that we started earlier.
package com.example.yomi.service;
import com.example.yomi.model.Book;
import com.example.yomi.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // this annotation tells Spring that this class is a service
public class BookServiceImpl implements BookService{ // we implement the BookService
@Autowired // this annotation tells Spring to inject the BookRepository dependency
private BookRepository bookRepository; // we inject the BookRepository dependency
public void createBook(Book book) {
bookRepository.save(book);
}
}
Before we can test, we must first create our API controller.
Let's make a BookController
class in the controller package.
package com.example.yomi.controller;
import com.example.yomi.model.Book;
import com.example.yomi.repository.BookRepository;
import com.example.yomi.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController // this annotation tells Spring that this class is a controller
public class BookController {
@Autowired // this annotation tells Spring to inject the BookRepository dependency
private BookRepository bookRepository; // we inject the BookService dependency
@Autowired // this annotation tells Spring to inject the BookService dependency
private BookService bookService; // we inject the BookService dependency
@PostMapping("/books") // this annotation tells Spring that this method is a POST request
public ResponseEntity<?> createBook(@RequestBody Book book) { // the ? means that the return type is a generic type
bookService.createBook(book);
bookRepository.save(book);
return new ResponseEntity<>(book, HttpStatus.CREATED); //ResponseEntity is a generic type that returns a response
}
}
After restarting your app, use Postman to test the Create Book API.
We get the document uploaded in the mongo compass when we check the database.
Let's take a step back and look at the application's connection chain. In this order, we have the model, repository, service, implementation, database, and client.
model -> repository -> service -> serviceImplementation -> controller -> database -> client
Let's now make the get all books request.
Let's make a request for all of the books.
We add a new method called getAllbooks
to the BookController.
//...
@GetMapping("/books")
public ResponseEntity<?> getAllBooks() { // the ? means that the return type is a generic type
List<Book> books = bookRepository.findAll();
return new ResponseEntity<>(books, books.size() > 0 ? HttpStatus.OK: HttpStatus.NOT_FOUND ); //ResponseEntity is a generic type that returns a response
// books.size() means that if the size of the books list is greater than 0, return HttpStatus.OK, else return HttpStatus.NOT_FOUND
}
//...
In BookService
we create a new method
//..
List<Book> getAllBooks();
//...
Let's put the getAllBooks method into action by right-clicking on the BookServiceImpl.
And then select methods to implement.
Then we call the bookRepository.findAll();
@Override
public List<Book> getAllBooks() {
List<Book> books = bookRepository.findAll();
return books;
}
Let us restart the server and run some tests in Postman. We get:
Let's add a new book just to make sure we have enough documents.
Let's retry the read all with the GET request.
We now have two documents in our database.
We are almost done. Hang in there Let's use the id to create a get single book, delete it, and update it. All we need to do is edit the controller, bookService, and bookServiceImplementation files in the same format.
I'll include a link to the source code as well as the entire set of snippets.
Simply follow the same steps as we did for the Post and Get requests.
//BookController
package com.example.yomi.controller;
import com.example.yomi.model.Book;
import com.example.yomi.repository.BookRepository;
import com.example.yomi.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController // this annotation tells Spring that this class is a controller
public class BookController {
@Autowired // this annotation tells Spring to inject the BookRepository dependency
private BookRepository bookRepository; // we inject the BookService dependency
@Autowired // this annotation tells Spring to inject the BookService dependency
private BookService bookService; // we inject the BookService dependency
@PostMapping("/books") // this annotation tells Spring that this method is a POST request
public ResponseEntity<?> createBook(@RequestBody Book book) { // the ? means that the return type is a generic type
bookService.createBook(book);
bookRepository.save(book);
return new ResponseEntity<>(book, HttpStatus.CREATED); //ResponseEntity is a generic type that returns a response
}
@GetMapping("/books")
public ResponseEntity<?> getAllBooks() { // the ? means that the return type is a generic type
List<Book> books = bookRepository.findAll();
return new ResponseEntity<>(books, books.size() > 0 ? HttpStatus.OK: HttpStatus.NOT_FOUND ); //ResponseEntity is a generic type that returns a response
// books.size() means that if the size of the books list is greater than 0, return HttpStatus.OK, else return HttpStatus.NOT_FOUND
}
@GetMapping("/books/{id}")
public ResponseEntity<?> getBookById(@PathVariable("id") String id) { // the ? means that the return type is a generic type
try {
Book book = bookService.getBookById(id);
return new ResponseEntity<>(book, HttpStatus.OK); //ResponseEntity is a generic type that returns a response
} catch (Exception e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND);
}
}
@PutMapping("/books/{id}")
public ResponseEntity<?> updateBookById(@PathVariable("id") String id, @RequestBody Book book) {
try {
bookService.updateBook(id, book);
return new ResponseEntity<>("Updated Book with id "+id, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); //ResponseEntity is a generic type that returns a response
}
}
@DeleteMapping("/books/{id}")
public ResponseEntity<?> deleteBookById(@PathVariable("id") String id) {
try {
bookService.deleteBook(id);
return new ResponseEntity<>("Deleted Book with id "+id, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); //ResponseEntity is a generic type that returns a response
}
}
}
//BookService
package com.example.yomi.service;
import com.example.yomi.model.Book; // we import the Book model class
import java.util.List;
public interface BookService { // interface is a contract that defines the behavior of a class
public void createBook(Book book);
// the `book` in lowercase is the name of the parameter that we will pass to the method
// the `Book` in uppercase is the name of the class that we will pass to the method
List<Book> getAllBooks();
public Book getBookById(String id);
public void updateBook(String id, Book book);
public void deleteBook(String id);
}
BookServiceImpl
package com.example.yomi.service;
import com.example.yomi.model.Book;
import com.example.yomi.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
@Service // this annotation tells Spring that this class is a service
public class BookServiceImpl implements BookService{ // we implement the BookService
@Autowired // this annotation tells Spring to inject the BookRepository dependency
private BookRepository bookRepository; // we inject the BookRepository dependency
public void createBook(Book book) {
book.setCreatedAt(new Date(System.currentTimeMillis()));
bookRepository.save(book);
}
@Override
public List<Book> getAllBooks() {
List<Book> books = bookRepository.findAll();
return books;
}
@Override
public Book getBookById(String id) {
Book book = bookRepository.findById(id).get();
return book;
}
@Override
public void updateBook(String id, Book book) {
Book bookToUpdate = bookRepository.findById(id).get();
bookToUpdate.setTitle(book.getTitle());
bookToUpdate.setAuthor(book.getAuthor());
bookToUpdate.setCreatedAt(book.getCreatedAt());
bookToUpdate.setUpdatedAt(new Date(System.currentTimeMillis()));
bookRepository.save(bookToUpdate);
}
@Override
public void deleteBook(String id) {
bookRepository.deleteById(id);
}
}
Update post
Result
Get Single Book using the ID
Delete book using Id
Conclusion
I understand that this is a lengthy read. Give yourself enough time to read between the lines. There are some topics that are not covered, such as input validation and entity association. I am hoping to continue in the near future. Keep an eye out.
Continue to learn, and may the force be with you. Peace