REST API Design

Bad Design

  • /users/addUser
  • /users/a1b2c3/verifyPassword
  • /roles/s8g3j8/updateRole
  • /tasks/submitTask

Resource Oriented

  • GET - read or find
  • POST - create
  • PUT - update
  • DELETE - delete

CRUD Operations

Support for client:
* POST /users (json body for creation)
* GET /users?age=50
* GET /tasks?state=open&sort=-priority,created_at
* GET /users/123
* GET /users/1234.xml (change format, default is json)
* GET /users?limit=25&offset=50 (pagination)
* GET /users?fields=name,age,location (pick the fields you want)
* GET /users/123/portfolios (get childrens)
* DELETE /users/a1b2c3
* POST /users/123 (json body - update)

//use verb but not noun for action other than CRUD
* GET /users/[action] (for those to the list of users)
* GET /users/123/[action] (for those to the object)
* GET /users/search?q=xxxx
* GET /users/count
---

//class level
@Path("/users")
@Produces({ MediaType.APPLICATION_JSON })
...

@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.TEXT_HTML })
public Response createUser(User user) throws AppException {
    Long userId = accountService.createUser(user);
    return Response.status(Response.Status.CREATED)// 201
            .entity("An new user has been created")
            .header("Location",
                    "http://localhost:8888/app/users/"
                            + String.valueOf(userId)).build();
}

@GET
public Response getUsers( @QueryParam("age") Integer age)
{
    if(age < 0 || age > 150) {
        //Invalid input for year so return HTTP Status 400
        return Response.status(Response.Status.BAD_REQUEST).build();
    }
    List<User> users = accountService.findUsersByAge(age);
    return Response.status(Response.Status.OK).entity(users).build();
}


@GET
@Path("{id}")
public Response getUser(@PathParam("id") Long id){
    User user = accountService.getUserById(id);
    return Response.status(Response.Status.OK).entity(user).build();
}

@DELETE
@Path("{id}")
@Produces({ MediaType.TEXT_HTML })
public Response deleteUser(@PathParam("id") Long id) {
    accountService.deleteUserById(id);
    return Response.status(Response.Status.NO_CONTENT)// 204
            .entity("User successfully removed from database").build();
}

@GET
@Path("{userId}/portfolios/")
public Response getPortfolios(@PathParam("userId") Long userId) {
    List<Porfolio> portfolios PortfolioService.getPorfoliosByUserId(userId);
    return Response.status(Response.Status.OK).entity(portfolios).build();
}


Header, URL and Form Handling

Form Handling

If it is a form post, you may want to get info like below:

@POST
@Consumes("application/x-www-form-urlencoded")
public void createUser(@FormParam("name") String name) {
    // create the user routine
    ...
}

OR

@POST
@Consumes("application/x-www-form-urlencoded")
public void createUser(MultivaluedMap<String, String> formParams) {
    // Store the message
}

Sometimes you may need to access the http request Header info OR to parse URL your own when the query parameters are very dynamic and you cannot simply create a method to capture it. Here is how:

@GET
public String get(@Context HttpHeaders hh) {
    MultivaluedMap<String, String> headerParams = hh.getRequestHeaders();
    Map<String, Cookie> pathParams = hh.getCookies();
}

Obtain the URL

@GET
public String get(@Context UriInfo ui) {
    MultivaluedMap<String, String> queryParams = ui.getQueryParameters();
    MultivaluedMap<String, String> pathParams = ui.getPathParameters();
}

HTTP status codes

HTTP defines a bunch of meaningful status codes that can be returned from your API. These can be leveraged to help the API consumers route their responses accordingly. I've curated a short list of the ones that you definitely should be using:

  • 200 OK - Response to a successful GET, PUT, PATCH or DELETE. Can also be used for a POST that doesn't result in a creation.
  • 201 Created - Response to a POST that results in a creation. Should be combined with a Location header pointing to the location of the new resource
  • 204 No Content - Response to a successful request that won't be returning a body (like a DELETE request)
  • 304 Not Modified - Used when HTTP caching headers are in play
  • 400 Bad Request - The request is malformed, such as if the body does not parse
  • 401 Unauthorized - When no or invalid authentication details are provided. Also useful to trigger an auth popup if the API is used from a browser
  • 403 Forbidden - When authentication succeeded but authenticated user doesn't have access to the resource
  • 404 Not Found - When a non-existent resource is requested
  • 405 Method Not Allowed - When an HTTP method is being requested that isn't allowed for the authenticated user
  • 409 Conflict (object already exist)
  • 410 Gone - Indicates that the resource at this end point is no longer available. Useful as a blanket response for old API versions
  • 415 Unsupported Media Type - If incorrect content type was provided as part of the request
  • 422 Unprocessable Entity - Used for validation errors
  • 429 Too Many Requests - When a request is rejected due to rate limiting


Can I throw appication Exception in the API?

The answer is yes. If you don't want to capture all excpetions and create response, you can do the following:

Define the Global Exception class

public class BusinessException extends Exception {

    private static final long serialVersionUID = 1L;

    private List messages;

    public BusinessException(List messages) {
        super();
        this.messages = messages;
    }

    public List getMessages() {
        return messages;
    }
}

Create exception mapper and register as provider thru annotation.

@Provider
public class ExceptionHttpStatusResolver implements
        ExceptionMapper {

    @Override
    public Response toResponse(BusinessException exception) {
        Response.Status httpStatus = Response.Status.INTERNAL_SERVER_ERROR;

        if (exception instanceof BusinessException)
            httpStatus = Response.Status.BAD_REQUEST;

        return Response.status(httpStatus).entity(exception.getMessages())
                .build();
    }
}

Then you can throw the BusinessException in your api and it will take care of the response creation for you.

See how we can throw the final BusinessException out and add in error messages to give user more info what is going wrong.

@GET
@Path("/list")
public Response getMoviesByGenre(@QueryParam("order") String order, @QueryParam("genre") String genre) 
throws BusinessException {
List errorMessages = new ArrayList();

    if(order == null || order.length() == 0) {
        errorMessages.add("order is required");
    }

    if(genre == null || genre.length() == 0) {
        errorMessages.add("genre is required");
    }

    if(!"ASC".equals(order) && !"DESC".equals(order)) {
        errorMessages.add("order of either ASC or DESC must be specified");
    }

    if(!"Action".equals(genre) &&  !"Drama".equals(genre)) {
        errorMessages.add("genre of either Action or Drama must be specified");
    }
    if(!errorMessages.isEmpty()) {
        throw new BusinessException(errorMessages);
    }

    List list = listMoviesByGenre(genre,order);
    return Response.status(Response.Status.OK).entity(list).build();
}

Output

Response headers:
Status Code: 400 Bad Request
Content-Type: application/json

Response body:
[
"genre is required",
"order of either ASC or DESC must be specified",
"genre of either Action or Drama must be specified"
]


Reference