DEV Community

Marcin Piczkowski
Marcin Piczkowski

Posted on

Framework-less REST API in Java

Photo by Hieu Vu Minh on Unsplash

Java ecosystem is packed with frameworks and libraries.
For sure not as many as in JavaScript world and they don't get old as quickly too but still this fact causes that I dare to think that we've already forgotten how to create a completely framework-less applications.

You may say: Spring is a standard, why to re-invent a wheel. Spark is a nice small REST framework.
Light-rest-4j is yet another.

I tell you, sure, you're right. You get a lot of bells and whistles with a framework, but you also get a lot of magic, learning overhead, additional features which you'll most likely not use and bugs as well.

The more external code is there in your service the more chance its developers made some mistakes.

Open source community is active and there's a great chance that these bugs in framework will be fixed soon, but still I'd like to encourage you to re-think if you really need a framework.

If you're doing a small service or a console application maybe you can live without it.

What you could gain (or loose) by sticking to pure java code? Think of these:

  • your code could be much cleaner and predictable (or a complete mess if you're a bad coder)
  • you'd have more control over your code, you won't be constrained by a framework (but you'd have to often write own code for what framework give you out of the box)
  • your application would deploy and start much quicker, because the framework code does not need to initialise dozen of classes (or not start at all if you messed up the stuff, e.g. multi-threading)
  • if you deploy your app on Docker, your images could be much slimmer because your jar would be slimmer too

I did a little experiment and tried developing a framework-less REST API.

I thought it could be interesting from learning perspective and a bit refreshing.

When I started building this I often came across situations when I missed some features which Spring provides out of the box.

At that times, instead of switching on another Spring capability, I had to rethink it and develop it myself.

It occurred that for real business case I would probably still prefer to use Spring instead of reinventing a wheel.

Still, I believe the exercise was pretty interesting experience.

Beginning.

I will go through this exercise step by step but not always paste a complete code here.
You can always checkout each step from a separate branch of the git repository.

Create a fresh Maven project with an initial pom.xml file:



<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.consulner.httpserver</groupId>
  <artifactId>pure-java-rest-api</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <java.version>11</java.version>
    <maven.compiler.source>${java.version}</maven.compiler.source>
    <maven.compiler.target>${java.version}</maven.compiler.target>
  </properties>

  <dependencies></dependencies>
</project>


Enter fullscreen mode Exit fullscreen mode

Include java.xml.bind module dependency because those modules were removed in JDK 11 by JEP-320.



<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.4.0-b180608.0325</version>
</dependency>


Enter fullscreen mode Exit fullscreen mode

and Jackson for JSON serialization



<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.7</version>
</dependency>



Enter fullscreen mode Exit fullscreen mode

Then we will use Lombok to simplify POJO classes:



<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.0</version>
  <scope>provided</scope>
</dependency>


Enter fullscreen mode Exit fullscreen mode

and vavr for functional programming facilities



<dependency>
  <groupId>io.vavr</groupId>
  <artifactId>vavr</artifactId>
  <version>0.9.2</version>
</dependency>


Enter fullscreen mode Exit fullscreen mode

I started from empty Application main class.

You can get an initial code from step-1 branch.

First endpoint

The starting point of the web application is com.sun.net.httpserver.HttpServer class.
The most simple /api/hello endpoint could look as below:



import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

import com.sun.net.httpserver.HttpServer;

class Application {

    public static void main(String[] args) throws IOException {
        int serverPort = 8000;
        HttpServer server = HttpServer.create(new InetSocketAddress(serverPort), 0);
        server.createContext("/api/hello", (exchange -> {
            String respText = "Hello!";
            exchange.sendResponseHeaders(200, respText.getBytes().length);
            OutputStream output = exchange.getResponseBody();
            output.write(respText.getBytes());
            output.flush();
            exchange.close();
        }));
        server.setExecutor(null); // creates a default executor
        server.start();
    }
}


Enter fullscreen mode Exit fullscreen mode

When you run main program it will start web server at port 8000 and expose out first endpoint which is just printing Hello!, e.g. using curl:



curl localhost:8000/api/hello


Enter fullscreen mode Exit fullscreen mode

Try it out yourself from step-2 branch.

Support different HTTP methods

Our first endpoint works like a charm but you will notice that no matter which HTTP method you'll use it will respond the same.
E.g.:



curl -X POST localhost:8000/api/hello
curl -X PUT localhost:8000/api/hello


Enter fullscreen mode Exit fullscreen mode

The first gotcha when building the API ourselves without a framework is that we need to add our own code to distinguish the methods, e.g.:



        server.createContext("/api/hello", (exchange -> {

            if ("GET".equals(exchange.getRequestMethod())) {
                String respText = "Hello!";
                exchange.sendResponseHeaders(200, respText.getBytes().length);
                OutputStream output = exchange.getResponseBody();
                output.write(respText.getBytes());
                output.flush();
            } else {
                exchange.sendResponseHeaders(405, -1);// 405 Method Not Allowed
            }
            exchange.close();
        }));


Enter fullscreen mode Exit fullscreen mode

Now try again request:



curl -v -X POST localhost:8000/api/hello


Enter fullscreen mode Exit fullscreen mode

and the response would be like:



> POST /api/hello HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.61.0
> Accept: */*
> 
< HTTP/1.1 405 Method Not Allowed


Enter fullscreen mode Exit fullscreen mode

There are also a few things to remember, like to flush output or close exchange every time we return from the api.
When I used Spring I even did not have to think about it.

Try this part from step-3 branch.

Parsing request params

Parsing request params is another "feature" which we'll need to implement ourselves in contrary to utilising a framework.
Let's say we would like our hello api to respond with a name passed as a param, e.g.:



curl localhost:8000/api/hello?name=Marcin

Hello Marcin!



Enter fullscreen mode Exit fullscreen mode

We could parse params with a method like:



public static Map<String, List<String>> splitQuery(String query) {
        if (query == null || "".equals(query)) {
            return Collections.emptyMap();
        }

        return Pattern.compile("&").splitAsStream(query)
            .map(s -> Arrays.copyOf(s.split("="), 2))
            .collect(groupingBy(s -> decode(s[0]), mapping(s -> decode(s[1]), toList())));

    }


Enter fullscreen mode Exit fullscreen mode

and use it as below:



 Map<String, List<String>> params = splitQuery(exchange.getRequestURI().getRawQuery());
String noNameText = "Anonymous";
String name = params.getOrDefault("name", List.of(noNameText)).stream().findFirst().orElse(noNameText);
String respText = String.format("Hello %s!", name);



Enter fullscreen mode Exit fullscreen mode

You can find complete example in step-4 branch.

Similarly if we wanted to use path params, e.g.:



curl localhost:8000/api/items/1


Enter fullscreen mode Exit fullscreen mode

to get item by id=1, we would need to parse the path ourselves to extract an id from it. This is getting cumbersome.

Secure endpoint

A common case in each REST API is to protect some endpoints with credentials, e.g. using basic authentication.
For each server context we can set an authenticator as below:



HttpContext context =server.createContext("/api/hello", (exchange -> {
  // this part remains unchanged
}));
context.setAuthenticator(new BasicAuthenticator("myrealm") {
    @Override
    public boolean checkCredentials(String user, String pwd) {
        return user.equals("admin") && pwd.equals("admin");
    }
});


Enter fullscreen mode Exit fullscreen mode

The "myrealm" in BasicAuthenticator is a realm name. Realm is a virtual name which can be used to separate different authentication spaces.
You can read more about it in RFC 1945

You can now invoke this protected endpoint by adding an Authorization header like that:



curl -v localhost:8000/api/hello?name=Marcin -H 'Authorization: Basic YWRtaW46YWRtaW4='


Enter fullscreen mode Exit fullscreen mode

The text after Basic is a Base64 encoded admin:admin which are credentials hardcoded in our example code.
In real application to authenticate user you would probably get it from the header and compare with username and password store in database.
If you skip the header the API will respond with status



HTTP/1.1 401 Unauthorized



Enter fullscreen mode Exit fullscreen mode

Check out the complete code from step-5 branch.

JSON, exception handlers and others

Now it's time for more complex example.

From my past experience in software development the most common API I was developing was exchanging JSON.

We're going to develop an API to register new users. We will use an in-memory database to store them.

Our user domain object will be simple:



@Value
@Builder
public class User {

    String id;
    String login;
    String password;
}



Enter fullscreen mode Exit fullscreen mode

I'm using Lombok annotations to save me from constructor and getters boilerplate code, it will be generated in build time.

In REST API I want to pass only login and password so I created a separate domain object:



@Value
@Builder
public class NewUser {

    String login;
    String password;
}



Enter fullscreen mode Exit fullscreen mode

Users will be created in a service which I will use in my API handler. The service method is simply storing the user.
In complete application it could do more, like send events after successful user registration.



public String create(NewUser user) {
    return userRepository.create(user);
}


Enter fullscreen mode Exit fullscreen mode

Our in-memory implementation of repository is as follows:




import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import com.consulner.domain.user.NewUser;
import com.consulner.domain.user.User;
import com.consulner.domain.user.UserRepository;

public class InMemoryUserRepository implements UserRepository {

    private static final Map USERS_STORE = new ConcurrentHashMap();

    @Override
    public String create(NewUser newUser) {
        String id = UUID.randomUUID().toString();
        User user = User.builder()
            .id(id)
            .login(newUser.getLogin())
            .password(newUser.getPassword())
            .build();
        USERS_STORE.put(newUser.getLogin(), user);

        return id;
    }
}


Enter fullscreen mode Exit fullscreen mode

Finally, let's glue all together in handler:



protected void handle(HttpExchange exchange) throws IOException {
        if (!exchange.getRequestMethod().equals("POST")) {
            throw new UnsupportedOperationException();
        }

        RegistrationRequest registerRequest = readRequest(exchange.getRequestBody(), RegistrationRequest.class);

        NewUser user = NewUser.builder()
            .login(registerRequest.getLogin())
            .password(PasswordEncoder.encode(registerRequest.getPassword()))
            .build();

        String userId = userService.create(user);

        exchange.getResponseHeaders().set(Constants.CONTENT_TYPE, Constants.APPLICATION_JSON);
        exchange.sendResponseHeaders(StatusCode.CREATED.getCode(), 0);

        byte[] response = writeResponse(new RegistrationResponse(userId));

        OutputStream responseBody = exchange.getResponseBody();
        responseBody.write(response);
        responseBody.close();
    }


Enter fullscreen mode Exit fullscreen mode

It translates JSON request into RegistrationRequest object:



@Value
class RegistrationRequest {

    String login;
    String password;
}


Enter fullscreen mode Exit fullscreen mode

which I later map to domain object NewUser to finally save it in database and write response as JSON.

I need to translate RegistrationResponse object back to JSON string.

Marshalling and unmarshalling JSON is done with Jackson object mapper (com.fasterxml.jackson.databind.ObjectMapper).

And this is how I instantiate the new handler in application main method:



 public static void main(String[] args) throws IOException {
        int serverPort = 8000;
        HttpServer server = HttpServer.create(new InetSocketAddress(serverPort), 0);

        RegistrationHandler registrationHandler = new RegistrationHandler(getUserService(), getObjectMapper(),
            getErrorHandler());
        server.createContext("/api/users/register", registrationHandler::handle);

        // here follows the rest.. 

 }


Enter fullscreen mode Exit fullscreen mode

You can find the working example in step-6 git branch, where I also added a global exception handler which is used
by the API to respond with a standard JSON error message in case, e.g. when HTTP method is not supported or API request is malformed.

You can run the application and try one of the example requests below:



curl -X POST localhost:8000/api/users/register -d '{"login": "test" , "password" : "test"}'


Enter fullscreen mode Exit fullscreen mode

response:



{"id":"395eab24-1fdd-41ae-b47e-302591e6127e"}


Enter fullscreen mode Exit fullscreen mode


curl -v -X POST localhost:8000/api/users/register -d '{"wrong": "request"}'


Enter fullscreen mode Exit fullscreen mode

response:



< HTTP/1.1 400 Bad Request
< Date: Sat, 29 Dec 2018 00:11:21 GMT
< Transfer-encoding: chunked
< Content-type: application/json
< 
* Connection #0 to host localhost left intact
{"code":400,"message":"Unrecognized field \"wrong\" (class com.consulner.app.api.user.RegistrationRequest), not marked as ignorable (2 known properties: \"login\", \"password\"])\n at [Source: (sun.net.httpserver.FixedLengthInputStream); line: 1, column: 21] (through reference chain: com.consulner.app.api.user.RegistrationRequest[\"wrong\"])"}


Enter fullscreen mode Exit fullscreen mode

Also, by chance I encountered a project java-express
which is a Java counterpart of Node.js Express framework
and is using jdk.httpserver as well, so all the concepts covered in this article you can find in real-life application framework :)
which is also small enough to digest the codes quickly.

Top comments (14)

Collapse
 
billoneil profile image
Bill O'Neil

I'm writing a Java blog that doesn't use a framework and all of the code is available on github. I do use Undertow as the web server which gives me a lot of the HTTP parts. stubbornjava.com/

Collapse
 
piczmar_0 profile image
Marcin Piczkowski

Thanks Bill, I'll have a look.

Collapse
 
at198x profile image
at198x

Thank you. I am looking for a way to implement REST API without Spring involved so Kubernetes can check my liveness probe, and your post work perfectly.

Collapse
 
michal1998 profile image
Michaล‚

Can I use an xml file instead of "hello"?

Collapse
 
piczmar_0 profile image
Marcin Piczkowski

Can you explain what you mean exactly?

Collapse
 
michal1998 profile image
Michaล‚

I have a generated XML data file. I want it to be available in Endpoint instead of "hello".

Thread Thread
 
michal1998 profile image
Michaล‚

In conclusion, I want to type localhost: 8080 / XML and see the data from the XML file I have in the folder.

Thread Thread
 
piczmar_0 profile image
Marcin Piczkowski

Instead of hardcoded text just read the file content as bytes. The rest is the same.
I'm sure you can google out plenty of examples like this one:

 Path path = Paths.get("C:/temp/test.xml");
 byte[] data = Files.readAllBytes(path);
Enter fullscreen mode Exit fullscreen mode

Then :

exchange.getResponseHeaders().put("Content-Type", "text/xml");
exchange.sendResponseHeaders(200, data.length);
OutputStream output = exchange.getResponseBody();
 output.write(data);
 output.flush();
 exchange.close();

Enter fullscreen mode Exit fullscreen mode

Haven't checked it compiles but give it a try.

Collapse
 
kareemradwan profile image
Kareem Radwan

it's very nice

Collapse
 
piczmar_0 profile image
Marcin Piczkowski

Thank you.

Collapse
 
c62eu19 profile image
Stan Zajdel

You can simplify this even further by simply using a servlet which handles much of what you have coded in your example.

Collapse
 
piczmar_0 profile image
Marcin Piczkowski

Probably, but I would not like to develop my own servlet container from scratch when we have plenty of them already. I would use Tomcat or Jetty or other if I went this route. The purpose was to make sth simple from ground up, but the experiment shows it's not that simple after all when the API grows, cause eventually we would come up with reinventing a wheel. My feelings, weather to use it in real app, or stick to well known frameworks like Spring are mixed. Still, I think it's good to know underlying techniques that the frameworks base on.

Collapse
 
kimf profile image
K

Super useful article, thanks!

Collapse
 
piczmar_0 profile image
Marcin Piczkowski

Hi, thanks. Let me know if you find some parts of it applicable in production ready app. I'm curious.