DEV Community

Cover image for RestAPI using Java
Abayomi Ogunnusi
Abayomi Ogunnusi

Posted on

RestAPI using Java

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.

started

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

Image description

Next, unzip the folder and open it with your IDE, in my case IntelliJ, and create a project out of the pom.xml file.

Image description


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.

Image description


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.

Image description

After a successful build, your server should be launched at the port we specified earlier.

Image description


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.

Image description


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
}


Enter fullscreen mode Exit fullscreen mode

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..
Image description

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
}

Enter fullscreen mode Exit fullscreen mode

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);
    }


}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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);
    }


}
Enter fullscreen mode Exit fullscreen mode

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
    }
}

Enter fullscreen mode Exit fullscreen mode

After restarting your app, use Postman to test the Create Book API.

Image description

We get the document uploaded in the mongo compass when we check the database.

Image description


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
Enter fullscreen mode Exit fullscreen mode

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
    }
//...
Enter fullscreen mode Exit fullscreen mode

In BookService we create a new method

//..
 List<Book> getAllBooks();
//...
Enter fullscreen mode Exit fullscreen mode

Let's put the getAllBooks method into action by right-clicking on the BookServiceImpl.
Image description

And then select methods to implement.

Image description

Then we call the bookRepository.findAll();

@Override
    public List<Book> getAllBooks() {
        List<Book> books = bookRepository.findAll();
        return books;

    }
Enter fullscreen mode Exit fullscreen mode

Let us restart the server and run some tests in Postman. We get:

Image description

Let's add a new book just to make sure we have enough documents.

Image description

Let's retry the read all with the GET request.

Image description

We now have two documents in our database.


We are almost done. Hang in there
image
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
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

//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);

}

Enter fullscreen mode Exit fullscreen mode

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);
    }


}

Enter fullscreen mode Exit fullscreen mode

Update post

Image description

Result

Image description

Get Single Book using the ID

Image description

Delete book using Id

Image description

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

Resources

Source code
Spring boot and MongoDB REST API CRUD Tutorial

Oldest comments (4)

Collapse
 
rytis profile image
Rytis

Hey, that's quite a cool reference for someone wanting to quickly test out Spring Boot, however I have some comments about your approach:

  1. Don't use field injection (@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.
  2. Since you are targeting beginners with this article, I don't think it makes sense to make a 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.
  3. You are using java.util.Date which is quite archaic and has a clunky API. For any new code you should consider the new objects from java.time package which was introduced since 1.8. LocalDate and LocalDateTime have extremely nice APIs. However in your case for creation dates, I would use Instant. You could just do Instant.now() which looks a lot nicer than getting system millis and nesting many constructor calls.
  4. I wouldn't use 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 use ResponseEntity overall). The better approach is to just return a plain Book object.
  5. Then stemming from my previous point you might ask how to deal with different response codes and errors that might arise. For that you have @ResponseStatus annotation. And errors shouldn't be caught inside the constructor (especially not the global Exception 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 as BookNotFoundException) and then catch that in a separate exception handler (either in a separate class with @RestControllerAdvise or inside the same controller class).
  6. You SHOULD use Optional API. There's reason why Spring's default repositories wrap the result inside the Optional object. You shouldn't do optional.get(), because that's an anti-pattern in most cases anyway. If you want to drop the Optional 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 into BookEntity and BookView 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:

  1. Consider using Gradle instead of Maven
  2. Consider using application.yaml instead of application.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.

Collapse
 
drsimplegraffiti profile image
Abayomi Ogunnusi

Thanks for your input...appreciate

Collapse
 
rytis profile image
Rytis

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!

Thread Thread
 
drsimplegraffiti profile image
Abayomi Ogunnusi

At ease, you can use it