Spring Boot RESTful Web Services CRUD Example

Spring provides a very good framework to building RESTful Web Services, and this support are extended in Spring Boot. This tutorial will explain in detail about building CRUD RESTful web services using Spring Boot.

For building a RESTful Web Services, we need to add the Spring Boot Starter Web dependency into the build configuration file. We already build a sample project in previous tutorial: Spring Boot + JPA/Hibernate + PostgreSQL RESTful CRUD API Example. We will continue to extend those project.

RestController

The controller handles all incoming HTTP requests from the user and returns an appropriate response. Let check what we have in ContactController.

package com.dariawan.contactapp.controller;

import com.dariawan.contactapp.domain.Contact;
import com.dariawan.contactapp.exception.BadResourceException;
import com.dariawan.contactapp.exception.ResourceAlreadyExistsException;
import com.dariawan.contactapp.exception.ResourceNotFoundException;
import com.dariawan.contactapp.service.ContactService;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ContactController {
    
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    private final int ROW_PER_PAGE = 5;
    
    @Autowired
    private ContactService contactService;
    
    @GetMapping(value = "/contacts", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<Contact>> findAll(
            @RequestParam(value="page", defaultValue="1") int pageNumber,
            @RequestParam(required=false) String name) {
         ...
         return ...
    }

    @GetMapping(value = "/contacts/{contactId}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Contact> findContactById(@PathVariable long contactId) {
        ...
        return ...
    }
    
    @PostMapping(value = "/contacts")
    public ResponseEntity<Contact> addContact(@RequestBody Contact contact) 
            throws URISyntaxException {
        ...
        return ...
    }
    
    @PutMapping(value = "/contacts/{contactId}")
    public ResponseEntity<Contact> updateContact(@RequestBody Contact contact, 
            @PathVariable long contactId) {
        ...
        return ...
    }
    
    @PatchMapping("/contacts/{contactId}")
    public ResponseEntity<Void> updateAddress(@PathVariable long contactId,
        @RequestBody Address address) {
        ...
        return ...
    }
    
    @DeleteMapping(path="/contacts/{contactId}")
    public ResponseEntity<Void> deleteContactById(@PathVariable long contactId) {
        ...
        return ...
    }
}
                    

The @RestController and @RequestMapping annotations are not specific to Spring Boot, they are part of Spring MVC annotations that help to create spring boot rest controller.

@RestController is a convenience annotation that is itself annotated with @Controller and @ResponseBody. The @Controller annotation represents a class with endpoints and the @ResponseBody indicates that a method return value should be bound to the web response body. This annotation is applied to a class to mark it as a request handler, and used for RESTful web services using Spring MVC.

@RequestMapping annotation maps HTTP requests to handler methods of Spring MVC and REST controllers, handling any request type.

Besides @RequestMapping, Spring annotations for handling different HTTP request types:

  • @GetMapping — For a GET request, a short form for @RequestMapping(value="...", method=RequestMethod.GET)
  • @PostMapping — For a POST request, a short form for @RequestMapping(value="...", method=RequestMethod.POST)
  • @PutMapping — For a PUT request, a short form for @RequestMapping(value="...", method=RequestMethod.PUT)
  • @PatchMapping — For a PATCH request, a short form for @RequestMapping(value="...", method=RequestMethod.PATCH)
  • @DeleteMapping — For a DELETE request, a short form for @RequestMapping(value="...", method=RequestMethod.DELETE)

@RequestMapping("/api") declares that the url for all the apis in this controller will start with /api. The @GetMapping("/contacts") indicates that the url of this method will start with /contacts, so altogether we will be able to access http://<server>:<port>/api/contacts as our endpoint. Here the list of REST endpoints from ContactController:

HTTP MethodsURIFunctionDescription
GET/api/contactsfindAllReturn contacts filtered by name with pagination
GET/api/contacts/{contactId}findContactByIdFind single contact that matched {contactId}
POST/api/contactsaddContactCreate new contact an return contact URI
PUT/api/contacts/{contactId}updateContactUpdate contact that identified by {contactId}
PATCH/api/contacts/{contactId}updateAddressUpdate contact's address that identified by {contactId}
DELETE/api/contacts/{contactId}deleteContactByIdDelete contact that identified by {contactId}

Now let’s take a look at the implementation of the APIs one by one:

GET /api/contacts — Get Contacts

@GetMapping(value = "/contacts", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<Contact>> findAll(
        @RequestParam(value="page", defaultValue="1") int pageNumber,
        @RequestParam(required=false) String name) {
    if (StringUtils.isEmpty(name)) {
        return ResponseEntity.ok(contactService.findAll(pageNumber, ROW_PER_PAGE));
    }
    else {
        return ResponseEntity.ok(contactService.findAllByName(name, pageNumber, ROW_PER_PAGE));
    }
}
                    

@RequestParam annotation used to accept query parameters in Controller’s handler methods.

Method findAll will retrieve contacts from the database based on name criteria (if any name parameter given) and return it in pagination. Let's test it with Postman:

Spring Boot CRUD - Postman test GET

Spring Boot CRUD Web Services - Postman test GET

GET /api/contacts/{contactId} — Get a Single Contact

@GetMapping(value = "/contacts/{contactId}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Contact> findContactById(@PathVariable long contactId) {
    try {
        Contact book = contactService.findById(contactId);
        return ResponseEntity.ok(book);  // return 200, with json body
    } catch (ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); // return 404, with null body
    }
}
                    

@PathVariable annotation used to extract any value which is embedded in the URL itself.

We search a contact by their unique id, and throwing a ResourceNotFoundException whenever a Contact with the given id is not found.

Spring Boot CRUD - Postman test GET byId

Spring Boot CRUD Web Services - Postman test GET byId

POST /api/contacts — Create new Contact

@PostMapping(value = "/contacts")
public ResponseEntity<Contact> addContact(@Valid @RequestBody Contact contact) 
        throws URISyntaxException {
    try {
        Contact newContact = contactService.save(contact);
        return ResponseEntity.created(new URI("/api/contacts/" + newContact.getId()))
                .body(contact);
    } catch (ResourceAlreadyExistsException ex) {
        // log exception first, then return Conflict (409)
        logger.error(ex.getMessage());
        return ResponseEntity.status(HttpStatus.CONFLICT).build();
    } catch (BadResourceException ex) {
        // log exception first, then return Bad Request (400)
        logger.error(ex.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }
}
                    

Spring Boot CRUD - Postman test POST

Spring Boot CRUD Web Services - Postman test POST

Upon successful creation of new Contact, a Location/api/contacts/29 is returned in the response header

PUT /api/contacts/{contactId} — Update a Contact

@PutMapping(value = "/contacts/{contactId}")
public ResponseEntity<Contact> updateContact(@Valid @RequestBody Contact contact, 
        @PathVariable long contactId) {
    try {
        contact.setId(contactId);
        contactService.update(contact);
        return ResponseEntity.ok().build();
    } catch (ResourceNotFoundException ex) {
        // log exception first, then return Not Found (404)
        logger.error(ex.getMessage());
        return ResponseEntity.notFound().build();
    } catch (BadResourceException ex) {
        // log exception first, then return Bad Request (400)
        logger.error(ex.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }
}
                    

If contact is not exists, a ResourceNotFoundException will be thrown.

Spring Boot CRUD - Postman test PUT

Spring Boot CRUD Web Services - Postman test PUT

PATCH /api/contacts/{contactId} — Update a Contact's Address

@PatchMapping("/contacts/{contactId}")
public ResponseEntity<Void> updateAddress(@PathVariable long contactId,
        @RequestBody Address address) {
    try {
        contactService.updateAddress(contactId, address);
        return ResponseEntity.ok().build();
    } catch (ResourceNotFoundException ex) {
        // log exception first, then return Not Found (404)
        logger.error(ex.getMessage());
        return ResponseEntity.notFound().build();
    }
}
                    

Similar like it's PUT counterpart, if contact is not exists, a ResourceNotFoundException will be thrown.

Spring Boot CRUD - Postman test PATCH

Spring Boot CRUD Web Services - Postman test PATCH

DELETE /api/contacts/{contactId} — Delete a Contact

@DeleteMapping(path="/contacts/{contactId}")
public ResponseEntity<Void> deleteContactById(@PathVariable long contactId) {
    try {
        contactService.deleteById(contactId);
        return ResponseEntity.ok().build();
    } catch (ResourceNotFoundException ex) {
        logger.error(ex.getMessage());
        return ResponseEntity.notFound().build();
    }
}
                    

Similar like it's PUT and PATCH counterpart, if contact is not exists, a ResourceNotFoundException will be thrown.

Spring Boot CRUD - Postman test DELETE

Spring Boot CRUD Web Services - Postman test DELETE

HTTP response 200 - OK means delete successful right?

RESTful API in XML

REST does not define a standard message exchange format. You can build REST services with both JSON and XML. JSON is a more popular format for REST, that's the reason for any Spring @RestController in a Spring Boot application will render JSON response by default as long as Jackson2 (jackson-databind-2.x.x.x) is on the class-path. If you want to enable XML representation, Jackson XML extension must be present on the class-path: jackson-dataformat-xml dependency should be added to your project:

<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>

In order to get XML response instead of JSON, client is expected to send appropriate ‘Accept’ header with value text/xml or application/xml.

$ curl http://localhost:8080/api/contacts?name=Sanji
[{"id":5,"name":"Vinsmoke Sanji","phone":"09056789012","email":"[email protected]","address1":null,"address2":null,"address3":null,"postalCode":null,"note":null}]
$ curl http://localhost:8080/api/contacts?name=Sanji -H "Accept: application/xml"
<Map><timestamp>2019-10-18T14:55:52.066+0000</timestamp><status>406</status><error>Not Acceptable</error><message>Could not find acceptable representation</message><path>/api/contacts</path></Map>

Oh ok, still one problem here. Because we only specified that /api/contacts will only produces JSON:

@GetMapping(value = "/contacts", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<Contact>> findAll(
        @RequestParam(value="page", defaultValue="1") int pageNumber,
        @RequestParam(required=false) String name) {
    ...
}
                    

We need to neutralize those setting, either by removing it...

@GetMapping(value = "/api/contacts")

Or allow to produce XML:

@GetMapping(value = "/contacts", produces = { "application/json", "application/xml" })

When we try again... voila!

$ curl http://localhost:8080/api/contacts?name=Sanji -H "Accept: application/xml"
<List><item><id>5</id><name>Vinsmoke Sanji</name><phone>09056789012</phone><email>[email protected]</email><address1/><address2/><address3/><postalCode/><note/></item></List>