How to compose REST API for microservice

Many modern applications uses REST API for communication between microservices, or backend vs frontend. Many developers have used, or still using (yuck) SOAP Web Services for these scenarios. When they switch to REST they often end with intricate and awful API. In this blog I will try to show you how to create clean API and how to convert service oriented thinking to rest like.

Let’s say you need to create endpoint for handling of invoices. For start lets introduce standard CRUD operations. First you need to define endpoint class – I will use standard JEE api from javax.ws.rs package.

Endpoint configuration

@Path( "/" )
@Interceptors( value = {MethodValidatorInterceptor.class} )
@Transactional
public class InvoiceEndpoint
{
   @Inject
   private InvoiceService invoiceService;
   ..
}

As you can see, there are standard annotations like @Path for path definition, @Transactional to mark endpoint transaction aware and last annotations @Interceptors which contains MethodValidatorInterceptor.class. This aspect is used to handle validation of input parameters as well as object parameters ( in case of @POST or @PUT operations). Here is hibernate validator based implementation I have used in several projects:

import org.hibernate.validator.HibernateValidator;

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.executable.ExecutableValidator;
import java.util.Set;

public class MethodValidatorInterceptor
{
   @AroundInvoke
   public Object validate( InvocationContext invocationContext ) 
      throws Throwable
   {
     if ( invocationContext.getParameters() != null )
     {
        // create validator
        Validator validator = Validation.byProvider( HibernateValidator.class )
                                     .configure()
                                     .buildValidatorFactory()
                                     .getValidator();

        // create method validator
        ExecutableValidator methodValidator = validator.forExecutables();

        // validate method parameters
        Set<ConstraintViolation<Object>> violations = methodValidator.validateParameters(
        invocationContext.getTarget(),
        invocationContext.getMethod(),
        invocationContext.getParameters() );

        // load validation group from method
        ValidationGroup validationGroup = invocationContext.getMethod().getAnnotation( ValidationGroup.class );

        if ( validationGroup != null && validationGroup.ignore() )
        {
           return invocationContext.proceed();
        }

        Class[] groups = validationGroup != null ? validationGroup.value() : new Class[]{};

        // validate parameters objects
        for ( Object parameter : invocationContext.getParameters() )
        {
          Set<ConstraintViolation<Object>> parameterViolations = validator.validate( parameter, groups );
          violations.addAll( parameterViolations );
        }

        // throw exception if violations are not empty
        if ( !violations.isEmpty() )
        {
          throw new MethodValidationException( violations );
        }
      }

      return invocationContext.proceed();
    }
}

@Qualifier
@Retention( RetentionPolicy.RUNTIME )
@Target( {ElementType.METHOD} )
public @interface ValidationGroup
{
    Class<?>[] value() default {};

    boolean ignore() default false;
}
public class MethodValidationException
        extends RuntimeException
{
    private Set<ConstraintViolation<Object>> violations;

    public MethodValidationException( Set<ConstraintViolation<Object>> violations) {
        this.violations = violations;
    }

    public Set<ConstraintViolation<Object>> getViolations() {
        return violations;
    }

    @Override
    public String toString() {
        return "MethodValidationException{" +
                "violations=" + violations +
                "} " + super.toString();
    }
}

Definition of CRUD operations

Let’s define @POST for invoice creation:

@POST
@Path( "/invoices" )
@Consumes( {MediaType.APPLICATION_JSON + ";charset=UTF-8"} )
@Produces( {MediaType.APPLICATION_JSON + ";charset=UTF-8"} )
public Invoice createInvoice( Invoice invoice )
{
    com.pojo.backend.model.Invoice backendInvoice = mapper.map(invoice, com.pojo.backend.model.Invoice.class );
    backendInvoice = service.create( backendInvoice );
    return mapper.map( backendInvoice,Invoice.class );
}

You can use backend Invoice to expose directly in endpoint but I do not recommend it. You should always create DTO copy of backend object – first reason is that API should be stable (if you add or just rename backend property it will propagate into endpoint), second reason is that DTO object does not always reflect every property and also it can contains properties or whole objects which does not exists in backend, or they are designed in different structure. For easier mapping of DTO vs backend objects I often use Orika bean mapper – https://orika-mapper.github.io/orika-docs/.

Move on to @PUT operation for invoice update:

@PUT
@Path( "/invoices/{id}" )
@Consumes( {MediaType.APPLICATION_JSON + ";charset=UTF-8"} )
@Produces( {MediaType.APPLICATION_JSON + ";charset=UTF-8"} )
public Invoice updateInvoice( @PathParam("id") Long id, Invoice invoice )
{
    com.pojo.backend.model.Invoice backendInvoice = service.getInvoiceById( id );
    if ( backendInvoice == null ) 
    {
       throw new NotFoundException("Invoice for id: " + id + " does not exists");
    }
    
    backendInvoice = mapper.map( invoice, backendInvoice );
    backendInvoice = service.update( backendInvoice );
    return mapper.map( backendInvoice,Invoice.class );
}

For update operation you should declare @PathParam for definition of identifier – in this case ‘id’. Lot of developers find this unnecessary because ‘id’ is already defined in Invoice object. But remember that Invoice is resource from REST point of view – and resource needs to be uniquely identifiable.

As you can see from code we try to load our object. If it does not exists we throw NotFoundException – REST framework will correctly translate this to 404 http error code. If object exists we continue with similar approach like we did in create scenario.

Ok, we created and updated our resource, now lets try to get it by id:

@GET
@Path( "/invoices/{id}" )
@Produces( {MediaType.APPLICATION_JSON + ";charset=UTF-8"} )
public Invoice getInvoice( @PathParam("id") Long id )
{
    com.pojo.backend.model.Invoice backendInvoice = service.getInvoiceById( id );
    if ( backendInvoice == null ) 
    {
       throw new NotFoundException("Invoice for id: " + id + " does not exists");
    }
 
    return mapper.map( backendInvoice, Invoice.class );
}

Nothing special here, there is actually the same code as in update scenario. So we will continue to invoice deletion:

@DELETE
@Path( "/invoices/{id}" )
public void deleteInvoice( @PathParam("id") Long id )
{
    com.pojo.backend.model.Invoice backendInvoice = service.getInvoiceById( id );
    if ( backendInvoice == null ) 
    {
       throw new NotFoundException("Invoice for id: " + id + " does not exists");
    }
 
    service.deleteInvoice( backendInvoice );
}

The last thing for standard CRUD operations are list and count.

Definition of query parameters in method is not good approach so instead I recommend to wrap them in QueryFilter – abstract filter which contains standard properties for list limitation and ordering:

public abstract class QueryFilter
{
    public static final int MAX_RESULTS = 10;

    @DefaultValue( "0" )
    @QueryParam( "firstResult" )
    private long firstResult = 0;

    @DefaultValue( "10" )
    @QueryParam( "maxResult" )
    private long maxResult = MAX_RESULTS;

    @QueryParam( "orderBy" )
    private String orderBy;

    @DefaultValue( "false" )
    @QueryParam( "ascending" )
    private boolean ascending;
    
    ...
}

Now extend QueryFilter and create InvoiceFilter in which you will define ‘invoiceNumber’ parameter:

public class InvoiceFilter extends QueryFilter
{
    @QueryParam( "invoiceNumber" )
    private String invoiceNumber;

    ...
}

Our list operation will now look like this:

@GET
@Path( "/invoices" )
@Produces( {MediaType.APPLICATION_JSON + ";charset=UTF-8"} )
public List<Invoice> listInvoice( @BeanParam InvoiceFilter filter )
{
    List<com.pojo.backend.model.Invoice> backendInvoices = service.getInvoices( filter );
    return mapper.mapAsList( backendInvoice, Invoice.class );
}

Clean and simple – and if you need to add another query parameter you do not have to fix all your clients across all microservices which uses Invoice endpoint.

How to make count? There are several approaches to create ‘count’ operation, I personally prefer to add ‘/count’ suffix:

@GET
@Path( "/invoices/count" )
@Produces( {MediaType.TEXT_PLAIN} )
public int listCountInvoice( @BeanParam InvoiceFilter filter )
{
    return service.getInvoicesCount( filter );
}

Transformation of SERVICE like operations into REST.

Common mistake of SOAP developers is that they want to create one to one copy of business service class. For instance, let’s say we want to mark invoice as paid. Usual (wrong) approach would be to create service like method in our endpoint – markAsPaid:

@POST
@Path( "/invoices/mark-as-paid" )
@Produces( {MediaType.APPLICATION_JSON + ";charset=UTF-8"} )
public Invoice markInvoiceAsPaid( Invoice invoice )
{
  ...
}

The problem with this code is that this method is not really a resource.

You have two options to make it better – if marking of invoice as paid is actually just changing of status you can use regular update method and change status by client. However if business behind marking invoice as paid is more complicated (for instance it means that you will need to call external service, send e-mail, create payment, etc) you need to create separate endpoint method like this:

@POST
@Path( "/invoices/{id}/payments" )
@Produces( {MediaType.APPLICATION_JSON + ";charset=UTF-8"} )
public Payment createPayment( @PathParam("id") Long id, Payment payment )
{
 ...
}

Now you are referring to specific invoice (resource) with it’s id and it’s sub-resource – payment. In your Payment object you define by client the amount of money the client has paid. Business logic then updates status of invoice and persists Payment.

Exception handling

Every code will throw time to time some exceptions. If you need to handle them inside microservice it’s easy – you just catch specific exception and do appropriate action. But what to do when exception will propagate outside your endpoint – how to transform them into meaningful response?

First we need to define ApiServerErrors – holder for application exceptions:

public class ApiServerErrors
{
    public static final String UNEXPECTED = "UNEXPECTED";

    private List<ApiError> errors = new ArrayList<>();

    public List<ApiError> getErrors()
    {
        return errors;
    }

    public void setErrors( List<ApiError> errors )
    {
        this.errors = errors;
    }

    // convenient method for transforming of validation exceptions into ApiError
    public void fromConstraintViolation( ConstraintViolation<Object> violation )
    {
        StringBuilder code = new StringBuilder();
        code.append( "VALIDATION_CONSTRAINT" );
        code.append( ":" );
        code.append( violation.getRootBeanClass().getName() ).append( "." ).append( violation.getPropertyPath() );
        code.append( ":" );
        code.append( violation.getMessageTemplate().replace( "{", "" ).replace( "}", "" ) );

        ApiError error = new ApiError();
        error.setCode( code.toString() );
        error.setMessage( violation.getRootBeanClass().getName() + "." + violation.getPropertyPath() + " " + violation.getMessage() );
        error.setDescription( violation.toString() );
        getErrors().add( error );
    }

    @Override
    public String toString()
    {
        return "ApiErrors{" +
                "errors=" + errors +
                '}';
    }

    public static class ApiError
    {
        private String code = UNEXPECTED;

        private String message;

        private String description;

        public String getCode()
        {
            return code;
        }

        public void setCode( String code )
        {
            this.code = code;
        }

        public String getMessage()
        {
            return message;
        }

        public void setMessage( String message )
        {
            this.message = message;
        }

        public String getDescription()
        {
            return description;
        }

        public void setDescription( String description )
        {
            this.description = description;
        }
    }
}

Second, define api exception mapper – class for handling of exceptions:

@Provider
public class ApiExceptionMapper
        implements ExceptionMapper<Exception>
{
    @Override
    public Response toResponse( Exception e )
    {
        Throwable exception = e;

        // handle rest generic exceptions
        if ( exception instanceof ClientErrorException )
        {
            ClientErrorException clientErrorException = ( ClientErrorException ) exception;
            return clientErrorException.getResponse();
        }

        // retrieve EJB exceptions cause
        if ( exception instanceof EJBException )
        {
            exception = exception.getCause();
        }

        // convert to ApiServerErrors
        ApiServerErrors apiErrors = new ApiServerErrors();
        toApiErrors( exception, apiErrors );

        // return response
        return Response
                .status( status( exception ) )
                .type( MediaType.APPLICATION_JSON_TYPE )
                .encoding( "UTF-8" )
                .entity( apiErrors )
                .build();
    }

    /**
     * Return status code based on exception type
     *
     * @param exception exception
     * @return status code
     */
    protected int status( Throwable exception )
    {
        return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
    }

    /**
     * Convert exception to {@link ApiServerErrors}
     *
     * @param exception {@link Throwable}
     * @param apiErrors {@link ApiServerErrors}
     */
    protected void toApiErrors( Throwable exception, ApiServerErrors apiErrors )
    {
        // handle method validation
        if ( exception instanceof MethodValidationException )
        {
            MethodValidationException validationException = ( MethodValidationException ) exception;
            for ( ConstraintViolation<Object> violation : validationException.getViolations() )
            {
                apiErrors.fromConstraintViolation( violation );
            }
        }
        // handle application exceptions
        else
        {
            ApiError apiError = new ApiError();
            apiErrors.getErrors().add( apiError );

            if ( exception instanceof ApiException )
            {
                apiError.setCode( ( ( ApiException ) exception ).getCode() );
            }
            apiError.setMessage( exception.getMessage() );
            apiError.setDescription( StackTraceResolver.resolve( exception ) );
        }
    }
}

There is special condition for ApiException – it is special exception which is used to set api error code for recognition in clients:

public interface ApiException
{
    /**
     * Return code - useful for determine type of exception for other APIs
     * @return exception code
     */
    String getCode();
}

As an example we will introduce new exception ‘InvoiceAlreadyPaid’ which will be thrown in our ‘mark invoice as paid’ scenario when invoice has been already paid:

public class InvoiceAlreadyPaid implements ApiException
{
  String getCode(){
     return INVOICE_ALREADY_PAID;
  }
}

When this exception occur, error response will look like this:

{[
 {
    "code": "INVOICE_ALREADY_PAID", // code can be localised
    "message": "Invoice has been already paid", // can be used by client to display in UI without the need of further localisation
    "description": "-stack trace goes here-"
 }
]}

Authentication

When you call REST endpoints from UI (web browser, mobile, TV) you need to authorise every call made for your API. There are several authentication types and frameworks but I recommend to use OAuth with JWT tokens. To enable authentication you need to:

a) implement your own oauth sever (do not do it, unless you really need to)

or

b) use existing service, for instance – Firebase

Example of authentication filter – it uses ctoolkit-client-firebase dependency for retrieving of JWT token and verify it against firebase:

@Priority( Priorities.AUTHENTICATION )
@javax.ws.rs.ext.Provider
public class Authenticator
        implements ContainerRequestFilter
{
 private Logger log = LoggerFactory.getLogger( Authenticator.class ); 

 private IdentityHandler identityHandler;

 private HttpServletRequest request;

 public Authenticator()
 {
 }

 @Inject
 public Authenticator( IdentityHandler identityHandler, @Context HttpServletRequest request )
 {
   this.identityHandler = identityHandler;
   this.request = request;
 }
 
 @Override
 public void filter( ContainerRequestContext requestContext ) throws IOException
 {
   try
   {
     Identity identity = identityHandler.resolve( request );

     if ( identity != null )
     {
       // request is authenticated, you can retrieve email or other useful parameters from Identity for further processing
       return;
     }
   } 
   catch( Exception e ) 
   {
     log.error( "Unable to verify token", e );
   }
 
   // request is not authenticated - return 401 error code
   requestContext.abortWith( Response.status( Response.Status.UNAUTHORIZED ).build() );
 }
}

Summary

If you are a former SOAP developer try to forget how you designed your API because REST comes with different approach – resource based. You need to handle a lot of boilerplate code like exception handling, security, validation of input parameters – use existing frameworks to let you help address these issues or use my snippets to make it less pain.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s