Java Rest API best practices. Guidelines with Spring Boot

Development of Restful API with Java can become straightforward when following best practices, leveraging compiler checks and code consistency.

Here is an example of an application which follows Java API developmnet best practices with Spring Boot and Docker.

Run the application

The source code is freely available on github: https://github.com/melphi/java-api-best-practices. You can run the application from Maven, from Docker or as Java executable. To build the application you will need Maven and Java8.

To run from maven:

mvn spring-boot:run

To run with docker:

sh build-docker.sh
sudo docker run java-api-guidelines

To run as java standalone executable:

mvn package
java -jar target/java-api-guidelines.jar

After few seconds the application should be ready, for example http://localhost:8080/health returns the health status.

The code can be freely customised with your implementation, just double check all the TODO to see the parts which need some configuration.

Folders structure

The project structure follows these guidelines:

java-api-best-practices
|-- pom.xml
|-- Dockerfile
|-- src/main/java/net/dainco
                           |-- container
                                       |-- configuration
                                       |-- filter
                                       |-- WebApplication.java
                           |-- module
                                    |-- common
                                    |-- abc
                                           |-- exception
                                           |-- domain
                                           |-- dao
                                           |-- service
                                           |-- rest
                                           |-- util

Packages

Container

The application container configuration, for example the startup class, security and persistence configuration, web filters, etc. In the example the WebApplication.java is the entry point which starts the application.

Module

Application is split in modules, think of a module of as a separated domain. For example user module contains all the classes related to user operations. 

When another module, for example company module, needs to comunicate with the user module it will use the UserService interface. Ideally the application should be designed in a way in which modules can be split in separated applications following microservice principles.

A common module contains base classes used by other modules.

Domain

The domain contains interfaces, dto and entities.

Dto, which stands for Data Transfer Object, are object returned by the service layer and serialised to Json.

Entities are the object to database mapping, which can use for example Hibernate or Spring Data annotations.

Interfaces are used to enforce compile time checks while converting Entities to Dto. This way if a new field is introduced the compiler can be used to check that all the related objects contain the required fields.

A best practice is to structure the Dto as an immutable object as follow:

public class UserPrivateDto implements UserPrivate {
  private String id;

  private String email;

  private String fullName;

  private String phoneNumber;

  public UserPrivateDto() {
    // Intentionally empty.
  }

  public UserPrivateDto(UserPrivate user) {
    this(user.getId(), user.getEmail(), user.getFullName(), user.getPhoneNumber());
  }

  public UserPrivateDto(String id, String email, String fullName, String phoneNumber) {
    this.id = id;
    this.email = email;
    this.fullName = fullName;
    this.phoneNumber = phoneNumber;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public String getFullName() {
    return this.fullName;
  }

  @Override
  public String getPhoneNumber() {
    return this.phoneNumber;
  }

  @Override
  public String getEmail() {
    return this.email;
  }
}
 

We can have a version of Dto for public data and for private data or compose various domain objects in one in order to produce the required JSON output.

Dao

Dao stands for Data Access Object and contains the interfaces to get and store objects in the database. It returns entitiy objects but not dto, which are returned by the service layer. The Dao depends on the persistence implemetation used, for example Hibernate or MongoDB.

In Spring Boot a Dao could be implemented following this guideline.

@Component
public class UserDaoImpl extends AbstractDao<UserEntity> implements UserDao {
  @Autowired
  public UserDaoImpl(MongoTemplate mongoTemplate) {
    super(mongoTemplate);
  }
  @Override
  public UserPrivate createUser(UserRegistration userRegistration) {
    // TODO: Implement this method.
  }
}
 

Service

Service classes implement all the the business logic. They connect to the Dao layer via dao interfaces. Also they return Dto object which will be serialised from the rest layer as Json object.

This is an example wich follows some best API practices:

@Component
public class UserServiceImpl implements UserService {
	private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);
	
	private UserDao userDao;

	@Autowired
	public UserServiceImpl(UserDao userDao) {
		this.userDao = userDao;
	}

	@Override
	public UserRegistrationResponseDto registerUser(UserRegistrationDto userRegistrationDto)
			throws UserRegistrationException {
		validateUserRegistration(userRegistrationDto);
		try {
			userDao.createUser(userRegistrationDto);
			return new UserRegistrationResponseDto();
		} catch (Exception e) {
			throw new UserRegistrationException(e.getMessage(), e);
		}
	}
}

Rest

Rest layer contains the REST API controllers. They link to the service layer via service interfaces. A rest layer should never implement any business logic, but just the endpoint mapping, request parameters and security.

@Api(tags = "users")
@RestController
@RequestMapping(value = "/users")
public class UserController {
  private UserService userService;

  @Autowired
  public UserController(UserService userService) {
    this.userService = userService;
  }

  @ApiOperation(value = "Registers a new user.", tags = "users")
  @RequestMapping(path = "/register", method = RequestMethod.POST)
  public UserRegistrationResponseDto registerUser(
      @RequestBody UserRegistrationDto userRegistrationDto) throws UserRegistrationException {
    return userService.registerUser(userRegistrationDto);
  }
}

Exception and Util

It is a good practice to use your own custom exceptions, which can be defined in the exception package.

Also exception messages should be encoded with a prefix, for example ERROR_, this way the user interface can translate all the messages starting with ERROR_. The error messages and other utility classes can be stored in the util package.

Conculsion

The source code with Java API best practices can be found at https://github.com/melphi/java-api-best-practices and can be freely extended.

You can just follow the guidelines of the skeleton application and complete the TODO tasks. Some missin parts are:

  • A full security implementation, for example OAuth2
  • Persistence layer, for example Spring Data with Mongo or a RBMS with Hibernate