Implementing CREATE operation

In this step, you'll write a service method for creating a blog post and a test method to verify the service method. By completing this step, you'll learn to map your service with the HTTP POST (@Post) method, make your own request converter (@RequestConverter), and utilize a server extension for testing (ServerExtension).

What you need

You need to have the following files obtained from previous steps. You can always download the full version, instead of creating one yourself.

  • Main.java
  • BlogPost.java
  • BlogService.java

1. Map HTTP method

Let's start mapping the HTTP POST method with our service method:

  1. Declare a service method, createBlogPost(), in the class BlogService.
  2. Map this service method with the HTTP POST method by adding the @Post annotation.
  3. Bind the endpoint /blogs to the method.
BlogService.java
import com.linecorp.armeria.server.annotation.Post;

public final class BlogService {
  ...

  @Post("/blogs")
  public void createBlogPost(BlogPost blogPost) {}
}

2. Handle parameters

Let's receive blog post information through a request body. Armeria's request converter converts request parameters in HTTP messages into Java objects for you. In the request converter, we define what keys of a JSON object to map with what properties of a Java object.

Let's first write a request converter and then register the request converter to the service method.

Write a request converter

Armeria's request converter converts a request body from a client into a Java object for you.

We can use Armeria's default JacksonRequestConverterFunction as is, but here let's give a go at customizing a request converter for our blog post requests. We want to convert blog post details into a Java object.

  1. Create a BlogPostRequestConverter.java file and declare a class, implementing the RequestConverterFunction interface. For the sake of simplicity, generate impromptu IDs for this tutorial.

    BlogRequestConverter.java
    package example.armeria.server.blog;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.linecorp.armeria.server.annotation.RequestConverterFunction;
    import java.util.concurrent.atomic.AtomicInteger;
    
    final class BlogPostRequestConverter implements RequestConverterFunction {
      private static final ObjectMapper mapper = new ObjectMapper();
      private AtomicInteger idGenerator = new AtomicInteger(); // Blog post ID
    }
  2. Add a method retrieving a value of a given key in a JSON object:

    BlogRequestConverter.java
    import com.fasterxml.jackson.databind.JsonNode;
    
    final class BlogPostRequestConverter implements RequestConverterFunction {
      ...
    
      static String stringValue(JsonNode jsonNode, String field) {
        JsonNode value = jsonNode.get(field);
        if (value == null) {
          throw new IllegalArgumentException(field + " is missing!");
        }
        return value.textValue();
      }
    }
  3. Customize the default convertRequest() method as follows.

    BlogRequestConverter.java
    import com.linecorp.armeria.server.ServiceRequestContext;
    import com.linecorp.armeria.common.AggregatedHttpRequest;
    import com.linecorp.armeria.common.annotation.Nullable;
    import java.lang.reflect.ParameterizedType;
    
    final class BlogPostRequestConverter implements RequestConverterFunction {
      ...
    
      @Override
      public Object convertRequest(ServiceRequestContext ctx,
        AggregatedHttpRequest request, Class<?> expectedResultType,
        @Nullable ParameterizedType expectedParameterizedResultType)
          throws Exception {
        if (expectedResultType == BlogPost.class) {
          JsonNode jsonNode = mapper.readTree(request.contentUtf8());
          int id = idGenerator.getAndIncrement();
          String title = stringValue(jsonNode, "title");
          String content = stringValue(jsonNode, "content");
          return new BlogPost(id, title, content); // Create an instance of BlogPost object
        }
        return RequestConverterFunction.fallthrough();
      }
      ...
    }

Register a request converter

In this step, assign the request converter we customized to our service method. Annotate the service method with @RequestConverter and specify the RequestConverterFunction class as BlogPostRequestConverter.class.

BlogService.java
import com.linecorp.armeria.server.annotation.RequestConverter;

public final class BlogService {
  ...

  @Post("/blogs")
  @RequestConverter(BlogPostRequestConverter.class)
  public void createBlogPost(BlogPost blogPost) {
    // Implement blog service
  }
}

3. Implement service code

When the request for creation is received, our request converter creates an instance of a blog post object for us. We want to save the blog post object in the map (blogPosts) created in the BlogService class.

Let's store the blog post information in the map by adding line 4, in the createBlogPost() method.

BlogService.java
1@Post("/blogs")
2@RequestConverter(BlogPostRequestConverter.class)
3public void createBlogPost(BlogPost blogPost) {
4  blogPosts.put(blogPost.getId(), blogPost);
5}

4. Return response

Now, it's time to return a response to our client. As the response, return the information received, with additional information including the ID of the post, created time, plus the modified time which would be identical to the created time.

Let's return a response for blog post creation:

  1. Replace the return type of the createBlogPost() method from void to HttpResponse.

  2. Create and return an HTTP response using Armeria's HttpResponse with the information of the post created.

    BlogService.java
    import com.linecorp.armeria.common.HttpResponse;
    
    public final class BlogService {
      ...
      public HttpResponse createBlogPost(BlogPost blogPost) {
        ...
        return HttpResponse.ofJson(blogPost);
      }
    }

5. Create a test file

Let's start writing test code. We'll use test code to verify what we implement along the way.

Create the BlogServiceTest.java file as follows. You can see the full version of the file here.

BlogServiceTest.java
package example.armeria.server.blog;

import com.fasterxml.jackson.databind.ObjectMapper;

class BlogServiceTest {
  private static final ObjectMapper mapper = new ObjectMapper();
}

6. Register a ServerExtension

Armeria's ServerExtension automatically handles set-up and tear-down of a server for testing. This is convenient as it eliminates the need to execute the main method to set up a server before running our tests.

In the BlogServiceTest class, register a ServerExtension as follows. Note that the service instance is added to the configuration.

BlogServiceTest.java
import org.junit.jupiter.api.extension.RegisterExtension;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;

class BlogServiceTest {
...
  @RegisterExtension
  static final ServerExtension server = new ServerExtension() {
    @Override
    protected void configure(ServerBuilder sb) throws Exception {
      sb.annotatedService(new BlogService());
    }
  };
}

7. Test creating a blog post

Let's test if we can create a blog post.

  1. In the BlogServiceTest class, add a private method as follows.

    BlogServiceTest.java
    import java.util.Map;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.linecorp.armeria.common.HttpRequest;
    import com.linecorp.armeria.common.MediaType;
    ...
    private static HttpRequest createBlogPostRequest(Map<String, String> content)
          throws JsonProcessingException {
      return HttpRequest.builder()
                  .post("/blogs")
                  .content(MediaType.JSON_UTF_8, mapper.writeValueAsString(content))
                  .build();
    }
  2. Add a test method as follows to test creating a blog post.

    BlogServiceTest.java
    import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson;
    import org.junit.jupiter.api.Test;
    import com.linecorp.armeria.client.WebClient;
    import com.linecorp.armeria.common.AggregatedHttpResponse;
    ...
    @Test
    void createBlogPost() throws JsonProcessingException {
        final WebClient client = WebClient.of(server.httpUri());
        final HttpRequest request = createBlogPostRequest(Map.of("title", "My first blog",
                  "content", "Hello Armeria!"));
        final AggregatedHttpResponse res = client.execute(request).aggregate().join();
    
        final Map<String, Object> expected = Map.of("id", 0,
                  "title", "My first blog",
                  "content", "Hello Armeria!");
    
        assertThatJson(res.contentUtf8()).whenIgnoringPaths("createdAt", "modifiedAt")
                  .isEqualTo(mapper.writeValueAsString(expected));
    }
  3. Run the test case on your IDE or using Gradle.

    ./gradlew test

    The service worked as expected if you see the test case passed.

You can test this also with Armeria's Documentation service. See Using DocService after adding service methods for instructions.

Next step

In this step, we've written a method to implement a CREATE operation and used Armeria's annotations; @Post and @RequestConverter. We've also registered ServerExtension to our test and written a test method.

Next, at Step 5. Implement READ, we'll implement a READ operation to read a single post and also multiple posts.