Challenge #4 – Taking what’s long and making it short

I’m going to be honest upfront, Challenge 4 of our coding challenge feels, well, less than challenge 3. I thoroughly enjoyed the 3rd challenge, so this was a little bit of a letdown. We went from making a search tool against Star Wars movie scripts to a URL shortener. Right? Not as much fun, but learning isn’t always fun, I tell myself 😁 .

What are the requirements? Let’s check them out:


What should it do?

Your service should receive a long URL and return a short code. The short code will be used to retrieve the long URL. If the long URL was already shortened, the same short code should be provided. 

Acceptance Criteria

  1. Receive a URL and return a short code
  2. ​​​​​​​If the same long URL is provided, return the same shorter version previously provided
  3. Receive a short code and return the original long URL

Extra Credit!

  1. When the shortened code is provided, perform the actual redirection to the original URL (probably only possible in UI based versions)
  2. Capture analytics on how many times a code/URL was used/retrieved and provide a way to view the most accessed URLs

This is very similar to what a service like bitly provides. It allows you to send in a really long URL, and then get a shortened URL. When you go to that short URL it’ll redirect you to the original long URL.

Here’s a quick overview of what this entire solution might look:

The first diagram illustrates how to create the short URL. The user provides the long URL through the front end, then the API shrinks it and returns it to the front end.

The second diagram illustrates how to use the short URL. The front end has to be running on the short domain, i.e. bit.ly/xxx. When the user goes to the short URL, the front end gets the short code from the URL and then calls the API to get the long URL. The long URL is returned to the front end and then the front end immediately redirects the user to the long URL. The goal here is the user doesn’t even see the front end, they just go to the long URL.

I wanted to illustrate this because I am only working on the API, the blue boxes above. I created two APIs, one that shrinks and one that expands by getting the original URL. I am not handling the front end work of parsing the short URL or performing the redirect.

It wouldn’t be hard to take this one step further and build the front end, maybe I will after the challenge. I have thought about creating my own redirecter before, but with bitly.com and rebrandly.com, I’ve been happy.

What I Built

I created the APIs to support a URL redirector solution with a few end points:

/shrink that takes a long URL and returns the short code.

/expand that takes the short code to return the long URL.

/analytics that takes the short code and returns the usage analytics.

Watch a walkthrough of it working and the code:

AC 1: Shrink the URL

This is simply handled by calling the shrink endpoint with the URL to be shrunk:

/shrink?longurl=https://davidlozzi.com/2021/04/19/ill-be-learning-java-spring-boot-through-a-coding-challenge-join-me/

This then returns my short code, with the long url as confirmation:

{
    "shortUrl": "UpGHUtRHkE",
    "originalUrl": "https://davidlozzi.com/2021/04/19/ill-be-learning-java-spring-boot-through-a-coding-challenge-join-me/",
    "usageCount": 0,
    "_links": {
        "self": {
            "href": "http://localhost:8080/redirect/shrink?longurl=https://davidlozzi.com/2021/04/19/ill-be-learning-java-spring-boot-through-a-coding-challenge-join-me/"
        },
        "expand": {
            "href": "http://localhost:8080/redirect/expand?shorturl=UpGHUtRHkE"
        },
        "analytics": {
            "href": "http://localhost:8080/redirect/analytics?shorturl=UpGHUtRHkE"
        }
    }
}

AC 2: Use the same code for the same URL

Yup, done, when I call the same /shrink endpoint with the same long URL, I get back the same short code every time.

AC 3: Get the long URL

With short code in hand, I can now call the expand endpoint to get my long URL back:

/expand?shorturl=UpGHUtRHkE

Gets us a similar payload we got when we created it:

{
    "shortUrl": "UpGHUtRHkE",
    "originalUrl": "https://davidlozzi.com/2021/04/19/ill-be-learning-java-spring-boot-through-a-coding-challenge-join-me/",
    "usageCount": 1,
    "_links": {
        "self": {
            "href": "http://localhost:8080/redirect/expand?shorturl=UpGHUtRHkE"
        },
        "analytics": {
            "href": "http://localhost:8080/redirect/analytics?shorturl=UpGHUtRHkE"
        }
    }
}

With this, the front end can now redirect the user to the long original URL.

Extra Credit: Analytics

EC1: do the actual redirect. I can’t do this since I’m running it only as an API. I thought about adding a front end to this but I don’t have that kind of time right now. As I shared above, I might make this a real hosted service, stay tuned.

EC2: capture usage analytics. This was an interesting item so I decided to take that on. Anytime a URL is pulled out using a short code, we track it with the current date/time in a new file. This allows us to view, over time, the usage of a short code.

To get analytics, we call:

/analytics?shorturl=UpGHUtRHkE

This then gives us all the nitty gritty usage details:

[
    {
        "shortUrl": "UpGHUtRHkE",
        "date": "2021-05-26T22:36:45.646+00:00"
    },
    ...
    {
        "shortUrl": "UpGHUtRHkE",
        "date": "2021-05-26T22:38:27.110+00:00"
    }
]

From here, the front end can chart this, summarize it, or whatever is needed to report on the analytics.

You’ll also notice in the /expand endpoint that we include the usageCount with each call as well. I thought it’d be nice to sneak in a little analytics there too

How I improved over last challenge

I’m not sure. I didn’t add anything new here or learn anything. This one was more of an exercise in everything I learned. It came together quickly, which was really nice to see. I’m also doing more Java development at my client site, so I’m feeling these Java muscles growing rapidly.

How can I make this better

I have been thinking about adding unit testing, I think it’s time. For this challenge and all my previous ones, I can further add to them by adding unit testing.

Also, I want to explore some patterns and try to jam that in here too. Patterns are important to help keep your code clean and organized and help the next developer align to what you built and how. Not entirely sure which one to narrow in on yet.

We’ll see how I do next time!

Now for some code

As always, the code is up in the same repo as the other challenges on GitHub. Check it out here: https://github.com/DavidLozzi/SlalomCodingChallenge/tree/main/4_urlredirect.

Below is my primary controller with the shrink, expand, and analytics endpoints, code located at 4_urlredirect/src/main/java/com/davidlozzi/urlredirector/UrlredirectorApplication.java

  @GetMapping("/redirect/shrink")
  @ResponseBody
  public ResponseEntity shrink(@RequestParam(value = "longurl", defaultValue = "") String longurl) {
    try {
      if (longurl.trim().length() == 0) {
        ApiErrorResponse err = new ApiErrorResponse("No longurl provided", 400);
        return new ResponseEntity<>(err, HttpStatus.BAD_REQUEST);
      }

      UrlData url = JsonUtil.getUrlByShort(longurl);
      if (url == null) {
        url = JsonUtil.getUrlByLong(longurl);
      }

      while (url == null) {
        String shorturl = Utils.getNewId();
        System.out.print("trying " + shorturl);
        if (JsonUtil.getUrlByShort(shorturl) == null) {
          url = new UrlData(shorturl, longurl);
          JsonUtil.saveUrl(url);
        }
      }

      url.add(linkTo(methodOn(UrlredirectorApplication.class).shrink(longurl)).withSelfRel());
      url.add(linkTo(methodOn(UrlredirectorApplication.class).expand(url.getShortUrl())).withRel("expand"));
      url.add(linkTo(methodOn(UrlredirectorApplication.class).analytics(url.getShortUrl())).withRel("analytics"));
      return new ResponseEntity<>(url, HttpStatus.OK);
    } catch (Exception ex) {
      ApiErrorResponse err = new ApiErrorResponse(ex.toString(), 500);
      return new ResponseEntity<>(err, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

Easy, right?

  • The /shrink end point receives longurl.
  • If no longurl is provided, we return an error response stating as much.
  • We check to see if longurl is a short code first. You never know, users be users. If it is a short code we go ahead and retrieve it and return it, like the /expand end point.
  • If the longurl is not a short code, then we check if it’s an existing long URL. If so, return that.
  • Finally, if the URL hasn’t been shrunk yet, we shrink it. I’m using a while here. Getting a short code is random, and as such we could randomly get the same code more than once, so if we do, we get another short code and try again. In my testing, this never happened, but it could. This will loop indefinitely, ideally, I’d put a break in there after 10 tries or something and then throw an error.
  • Then I add a few HATEOAS links and return the URL object.
  @GetMapping("/redirect/expand")
  @ResponseBody
  public ResponseEntity expand(@RequestParam(value = "shorturl", defaultValue = "") String shorturl) {
    try {
      if (shorturl.trim().length() == 0) {
        ApiErrorResponse err = new ApiErrorResponse("No shorturl provided", 400);
        return new ResponseEntity<>(err, HttpStatus.BAD_REQUEST);
      }
      UrlData url = JsonUtil.getUrlByShort(shorturl);
      if (url != null) {
        url.add(linkTo(methodOn(UrlredirectorApplication.class).expand(shorturl)).withSelfRel());
        url.add(linkTo(methodOn(UrlredirectorApplication.class).analytics(shorturl)).withRel("analytics"));
        return new ResponseEntity<>(url, HttpStatus.OK);
      }
      ApiErrorResponse err = new ApiErrorResponse("shorturl " + shorturl + " was not found", 404);
      return new ResponseEntity<>(err, HttpStatus.NOT_FOUND);

    } catch (Exception ex) {
      ApiErrorResponse err = new ApiErrorResponse(ex.toString(), 500);
      return new ResponseEntity<>(err, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

Even easier than /shrink is /expand:

  • It collects the shorturl, which is really the short code. Sorry for the variable naming confusion.
  • If no shorturl is provided, we return an error stating that.
  • Then we retrieve the data using the shorturl.
  • If it exists, we HATEOAS it up and return it.
  • If it doesn’t exist, we return a 404 not found error.
  @GetMapping("/redirect/analytics")
  @ResponseBody
  public ResponseEntity analytics(@RequestParam(value = "shorturl", defaultValue = "") String shorturl) {
    try {
      if (shorturl.trim().length() == 0) {
        ApiErrorResponse err = new ApiErrorResponse("No shorturl provided", 400);
        return new ResponseEntity<>(err, HttpStatus.BAD_REQUEST);
      }
      List<AnalyticData> analytics = JsonUtil.getAnalyticByShort(shorturl);
      if (analytics != null) {
        return new ResponseEntity<>(analytics, HttpStatus.OK);
      }
      ApiErrorResponse err = new ApiErrorResponse("shorturl " + shorturl + " was not found", 404);
      return new ResponseEntity<>(err, HttpStatus.NOT_FOUND);

    } catch (Exception ex) {
      ApiErrorResponse err = new ApiErrorResponse(ex.toString(), 500);
      return new ResponseEntity<>(err, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

Finally, we have our /analytics end point:

  • It receives shorturl as the short code
  • Check if it has a value, if not, return an error.
  • We then get all of the analytics using the shorturl
  • If the analytics are not null then we return the entire list of analytics.
  • If it is null, then we return a 404 not found error.

Let me know what you think!

Until next time, happy coding!

Series Navigation

Leave a Reply

Up ↑

%d bloggers like this: