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
Top comments (4)
Hey, that's quite a cool reference for someone wanting to quickly test out Spring Boot, however I have some comments about your approach:
@Autowired
). Even IntelliJ should give you a warning about it. The preferred way is the constructor injection. A neat way to avoid boilerplate code is to declare injected beans as private final, and then use@RequiredArgsConstructor
from Lombok. It might be a bit too magical depending on the target audience of your article (which I assume are beginners), but either way anti-patterns should not be taught.BookService
as an interface and a separate Impl class just for the sake of it. Throughout my teaching career I've noticed that this confuses students to hell, because they don't understand the reason for it. It would make sense if you've also showed a unit test having its own implementation of the interface returning values in the testing scope. Overall I consider Interface + single Impl class to be an overcomplication just for the sake of it, but that's another discussion.java.util.Date
which is quite archaic and has a clunky API. For any new code you should consider the new objects fromjava.time
package which was introduced since 1.8.LocalDate
andLocalDateTime
have extremely nice APIs. However in your case for creation dates, I would useInstant
. You could just doInstant.now()
which looks a lot nicer than getting system millis and nesting many constructor calls.ResponseEntity
as controller method returns, because at the very least it hides the true return type of the endpoint (there are very few cases where I would ever useResponseEntity
overall). The better approach is to just return a plainBook
object.@ResponseStatus
annotation. And errors shouldn't be caught inside the constructor (especially not the globalException e
, because this will plainly eat any exception and hide the stacktrace from developers). You should allow those exceptions to either propagate, because it IS a real exception, OR you should create a proper informative exception (such asBookNotFoundException
) and then catch that in a separate exception handler (either in a separate class with@RestControllerAdvise
or inside the same controller class).Optional
API. There's reason why Spring's default repositories wrap the result inside theOptional
object. You shouldn't dooptional.get()
, because that's an anti-pattern in most cases anyway. If you want to drop theOptional
wrapper inside a service, which is a totally valid approach, you should do it by chaining an.orElseThrow()
, with ideally a proper informative exception inside.Now something that is outside of the scope of beginner's tutorial, but should definitely be considered for production, in case anyone is reading this and wondering: the model you return from controller should be separate from your data entity. Right now you are creating a
Book
model and then also annotating it as a@Document
which is basically exposing your data model. In simple CRUD applications the data and view models are mostly the same, that's why people do this, however as soon as you start expanding it, the view model might contain data from many different data models, or frontend might require a completely different variable naming compared to what is saved in the database. So a split intoBookEntity
andBookView
might make sense in production.And finally something I would do different, but this falls more under the opinion / religious debate, so take it with a grain of salt:
application.yaml
instead ofapplication.properties
With that said, this has become quite an extensive comment (like a pull request), and I would like to make into a full followup article. I hope you won't mind.
Thanks for your input...appreciate
Do you mind if I take this as an example for my own article? These are very common mistakes for beginners, because a lot of tutorials teach exactly that. I think addressing those would benefit the community greatly!
At ease, you can use it