A Spring Data REST Primer

Behrang Saeedzadeh 21 January 2018

Overview

Spring Boot and Spring MVC have managed to eliminate much of the boilerplate necessary to develop RESTful APIs with Java. However, even with Spring MVC, for every resource you want to expose in your API you have to:

  • Create a JPA entity (assuming you use JPA),
  • Create a Spring Data repository (assuming you use Spring Data),
  • Create a Spring MVC controller, and
  • Create a service for transaction management.

Spring Data REST goes a step further and eliminates the need to write controllers and services for your APIs.

In this post, I will show you how to create a simple RESTful API with Spring Data REST to:

  • Create an Article resource,
  • Create two Tag resources, and
  • Link the article to the tags.

I finally conclude this article by listing some of the limitations of Spring Data REST that in my opinion make it a questionable choice for building production-grade APIs.

Source code

You can grab the source code for this article from GitHub.

Step 1: Create the skeleton for your project

I normally create the skeleton of Spring Boot projects from within my IDE, IntelliJ IDEA, but here, let’s use the online Spring Initializer tool to generate the skeleton of our project.

  1. Keep Maven Project, Java, and Spring Boot 1.5.9 unchanged (or make sure they are selected).

  2. Then in Project Metadata, choose:
    • Group: org.behrang.examples
    • Artifact: spring-data-rest
  3. Finally, for Dependencies go ahead and choose JPA, H2, Rest Repositories, and Lombok and the press the Generate Project button. We need H2 as our embedded development database and Lombok to make our code more concise.

  4. Then extract the downloaded archive and open the spring-data-rest folder with your IDE.

The Maven POM file is quite simple and should have five dependencies:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-rest</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>    
</dependencies>

Ensure everything is in order by building the project from the CLI:

$ ./mvnw clean install

For IntelliJ IDEA to work properly with Lombok go to the Build, Execution Deployment > Compiler > Annotation Processors setting and check these two options:

  • ☑ Enable annotation processing.
  • ○ Obtain processors from project classpath.

Step 2: Define the Article and Tag entities

The Article entity:

@Entity
@Data
public class Article {

    @Id
    @GeneratedValue(strategy = AUTO)
    private Long id;

    @NotNull
    @NotBlank
    @Size(min = 1, max = 100)
    private String title;

    @NotNull
    @NotBlank
    @Size(min = 1, max = 5000)
    private String body;

    @ManyToMany
    @JoinTable(
            name = "ARTICLE_TAGS",
            joinColumns = @JoinColumn(name = "ARTICLE_ID", referencedColumnName = "ID"),
            inverseJoinColumns = @JoinColumn(name = "TAG_ID", referencedColumnName = "ID")
    )
    private List<Tag> tags;

}

The Tag entity:

@Entity
@Data
public class Tag {

    @Id
    @GeneratedValue(strategy = AUTO)
    private Long id;

    @NotNull
    @NotBlank
    @Size(min = 1, max = 50)
    private String value;

    @ManyToMany(mappedBy = "tags")
    private List<Article> articles;

}

Step 3: Define the Article and Tag repositories

The ArticleRepository:

public interface ArticleRepository extends PagingAndSortingRepository<Article, Long> {
}

The TagRepository:

public interface TagRepository extends PagingAndSortingRepository<Tag, Long> {
}

Our RESTful API is almost ready! Before going ahead and running our API, let’s rename the main class from the auto-generated SpringDataRestApplication to Main:

@SpringBootApplication
public class Main {

	public static void main(String[] args) {
		SpringApplication.run(Main.class, args);
	}
}

Now we can run the app using Maven from the CLI:

$ ./mvnw spring-boot:run

Step 4: Creating the Article and Tag resources and associating the article with its tags

In order to create an article, we should send a POST request with the application/json content type and the article’s fields in a JSON document to the http://localhost:8080/articles endpoint. I use the fabulous HTTPie tool to issue the HTTP requests from the CLI.

$ http POST localhost:8080/articles title="Hello, World" body="First Article"
HTTP/1.1 201
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Jan 2018 02:35:50 GMT
Location: http://localhost:8080/articles/1
Transfer-Encoding: chunked

{
    "_links": {
        "article": {
            "href": "http://localhost:8080/articles/1"
        },
        "self": {
            "href": "http://localhost:8080/articles/1"
        },
        "tags": {
            "href": "http://localhost:8080/articles/1/tags"
        }
    },
    "body": "First Article",
    "title": "Hello, World"
}

Spring Data REST conforms to the HAL specification and as you can see, other than the fields of the article resource, the response contains the location of the just created article as well as a link to its tags.

Let’s verify that our article is indeed created by sending a GET request to http://localhost:8080/articles/1:

$ http localhost:8080/articles/1
HTTP/1.1 200
Content-Type: application/hal+json;charset=UTF-8
Date: Sun, 21 Jan 2018 02:42:38 GMT
Transfer-Encoding: chunked

{
    "_links": {
        "article": {
            "href": "http://localhost:8080/articles/1"
        },
        "self": {
            "href": "http://localhost:8080/articles/1"
        },
        "tags": {
            "href": "http://localhost:8080/articles/1/tags"
        }
    },
    "body": "First Article",
    "title": "Hello, World"
}

Yeap. It’s there. Now let’s create two tags named foo and bar respectively:

$ http POST localhost:8080/tags value="foo"
HTTP/1.1 201
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Jan 2018 02:43:59 GMT
Location: http://localhost:8080/tags/1
Transfer-Encoding: chunked

{
    "_links": {
        "articles": {
            "href": "http://localhost:8080/tags/1/articles"
        },
        "self": {
            "href": "http://localhost:8080/tags/1"
        },
        "tag": {
            "href": "http://localhost:8080/tags/1"
        }
    },
    "value": "foo"
}

$ http POST localhost:8080/tags value="bar"
HTTP/1.1 201
Content-Type: application/json;charset=UTF-8
Date: Sun, 21 Jan 2018 02:44:26 GMT
Location: http://localhost:8080/tags/2
Transfer-Encoding: chunked

{
    "_links": {
        "articles": {
            "href": "http://localhost:8080/tags/2/articles"
        },
        "self": {
            "href": "http://localhost:8080/tags/2"
        },
        "tag": {
            "href": "http://localhost:8080/tags/2"
        }
    },
    "value": "bar"
}

Finally, let’s associate our article with the tags we just created. In order to link the article with the tags, we have to:

  1. Send a POST request to the article’s tag associations URI: localhost:8080/articles/1/tags,
  2. Set the Content-Type header of the request to text/uri-list[IANA↗], and
  3. Send a list of tag URIs we want to associate with our article, one URI per line:
http://localhost:8080/tags/1
http://localhost:8080/tags/2
$ echo $'http://localhost:8080/tags/1\nhttp://localhost:8080/tags/2' | http -v \
       POST localhost:8080/articles/1/tags Content-Type:text/uri-list
POST /articles/1/tags HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 58
Content-Type: text/uri-list
Host: localhost:8080
User-Agent: HTTPie/0.9.8

http://localhost:8080/tags/1
http://localhost:8080/tags/2

HTTP/1.1 204
Date: Sun, 21 Jan 2018 05:38:45 GMT

We can verify the tags have indeed been added to the article by querying for our article’s tags:

$ http localhost:8080/articles/1/tags
HTTP/1.1 200
Content-Type: application/hal+json;charset=UTF-8
Date: Sun, 21 Jan 2018 05:39:33 GMT
Transfer-Encoding: chunked

{
    "_embedded": {
        "tags": [
            {
                "_links": {
                    "articles": {
                        "href": "http://localhost:8080/tags/1/articles"
                    },
                    "self": {
                        "href": "http://localhost:8080/tags/1"
                    },
                    "tag": {
                        "href": "http://localhost:8080/tags/1"
                    }
                },
                "value": "Bar"
            },
            {
                "_links": {
                    "articles": {
                        "href": "http://localhost:8080/tags/2/articles"
                    },
                    "self": {
                        "href": "http://localhost:8080/tags/2"
                    },
                    "tag": {
                        "href": "http://localhost:8080/tags/2"
                    }
                },
                "value": "Foo"
            }
        ]
    },
    "_links": {
        "self": {
            "href": "http://localhost:8080/articles/1/tags"
        }
    }
}

Why Spring Data REST is not ready for production-use yet?

As we saw in this example, in order to create an article and link it to two tags, we had to issue four HTTP requests:

  1. One POST request to create the article
  2. Two POST requests to create each of the foo and bar tags respectively, and
  3. One POST request to assign the article to the tags

Each of these requests are executed in a distinct transaction. You might end up in a situation where the first three requests succeed but the last request to associate the article with the tags fail (e.g. due to network error) and have an article with no tags and two dangling, orphan tags in the database.

Consequently front-end clients have to send four requests instead of one to your API to create the article and its associated tags:

  await createArticle({
    title: 'Hello, World',
    body: 'First article.',
    tags: [{
      value: 'foo'
    }, {
      value: 'bar'
    }]
  });

Your code would end up looking like this:

let article = await createArticle({
  title: 'Hello, World',
  body: 'First article.'
});  

let tags = await createTags([{
  value: 'foo'
}, {
  value: 'bar'
}]);

await addTagsToArticle(tags, article);

These aspects and limitations of Spring Data REST make it unsuitable for production-grade APIs and limit its use to prototyping and implementation of trivial RESTful APIs. You can mix and match Spring Data REST repositories with plain-old Spring MVC REST controllers, but that defeats the whole purpose of Spring Data REST.