System Design: Build URL Shortening Service using Ktor (Part-3)

In this blog, we are going to learn how to expose domain to routing and build a full-fledged backend app

System Design: Build URL Shortening Service using Ktor (Part-3)

This is the third part of the series. Check out the series here.

Till now you have built the url feature, the DI, and the providers. You have built all the use cases and that will be used not in routing.

You have built three use cases, i.e CreateShortUrlUseCase, FindShortUrlUseCase and FindUrlHitCountUseCase.

Now using this you will build your routes.

In this blog, you are going to learn how to use routing and locations to complete your app.

Let us start before any delay.

Setup Location.

Now, you need to have two routes basically. One for creating short URLs and one for using that routes.

Basically, like /v1/url which will be POST, and other /{url} which will be GET. Additionally, you will also create one more route to get the hit counts of the short URL.

First, create a companion object in the UrlEntity to create endpoints.

data class UrlEntity(
    @BsonId
    val urlId: String = ObjectId().toString(),
    val originalUrl: String,
    val shortUrl: String,
    val createdAt: String,
    val urlHitCount: Int = 0
) {
    companion object {
        const val URL = "/v1/url"
        const val URL_COUNT = "/v1/{url}/count"
        const val SHORT_URL = "/{url}"
    }
}

I use this as my recommended way to create endpoints.

Now, you will create Locations for these routes. If you want to learn more about Location and Routine check the following post.

Create a package routing in,

feature -> url -> routing

In this create two files, UrlRouting and UrlLocation.

In the UrlLocation file, create a location for:

Creating a short URL.

@KtorExperimentalLocationsAPI
@Location(UrlEntity.URL)
class UrlLocation

Here, you can see, UrlLocation does not take any parameter as it will take the body params.

Using short URL

@KtorExperimentalLocationsAPI
@Location(UrlEntity.SHORT_URL)
data class ShortUrlLocation(val url: String)

Here, ShortUrlLocation takes a param url which will act as a path parameter for /{url} route.

Short URL Count

@KtorExperimentalLocationsAPI
@Location(UrlEntity.URL_COUNT)
data class UrlCount(val url: String)

This will also take a param url which will act as a path parameter for /v1/{url}/count route.

You are done here setting up the Location for your app.

Setup Routing.

Now using these locations, you are going to setup your routing in the UrlRouting, which is an extension function, looks like,

fun Application.urlRoutes() {

    routing {
        post<UrlLocation> {

        }

        get<ShortUrlLocation> { request ->

        }

        get<UrlCount> { request ->

        }
    }
}

You are going to setup all the logic here where you need to use use-cases to do the transaction between the user and the DB.

As a parameter, you need to pass, DomainProvider and ExceptionProvider to the urlRoutes extension function.

The update code will look like,

fun Application.urlRoutes(domainProvider: DomainProvider, exceptionProvider: ExceptionProvider) {

}

UrlLocation

Let us talk about UrlLocation first.

This is a post request and here you are going to pass the data in the body. The request will look like this,

{
  "url" : "my_url_which_has_to_be_shortened"
}

To fetch this body, you use,

val urlRequest = call.receive<UrlRequest>()

where UrlRequest looks like,

data class UrlRequest(val url: String)

which is mapper to the above JSON.

Now, you need to check that the url which you are passing is valid or not. For that create is isValid in Util.kt and that looks like,

fun isValid(url: String?): Boolean {
    return try {
        URL(url).toURI()
        true
    } catch (e: Exception) {
        false
    }
}

Now, the updated code of the UrlLocation, will look like,

post<UrlLocation> {
   val urlRequest = call.receive<UrlRequest>()
   if (isValid(urlRequest.url)) {
       val response =
                    domainProvider.provideCreateShortUrlUseCase().invoke(urlRequest.url)
        call.respond(response)
   } else {
         call.respond(
               HttpStatusCode.BadRequest,
               exceptionProvider.respondWithGenericException("Url is not valid!")
        )
    }
}

Here, isValid is checking for the request URL. If that is valid use domainProvider to use that specific CreateShortUrlUseCase and will respond that response to the user.

and if it's invalid, return the exception using exceptionProvider with a BadRequest status code.

ShortUrlLocation

In this, you have to use FindShortUrlUseCase to invoke the request in the path to generating a short URL. The updated code,

get<ShortUrlLocation> { request ->
     val shortUrl = request.url
     val response = domainProvider.provideFindShortUrlUseCase().invoke(shortUrl)
     when {
         response != null -> call.respondRedirect(response)
         else -> call.respond(
               HttpStatusCode.NotFound,
               exceptionProvider.respondWithNotFoundException("Url not found!!!")
          )
      }
}

UrlCount

In this, you have to use FindUrlHitCountUseCase to invoke the request in the path to get the count of short URLs.

The updated code look like,

get<UrlCount> { request ->
    val shortUrl = request.url
    val response = domainProvider.provideFindUrlHitCountUseCase().invoke(shortUrl)
    call.respond(response)
}

You are done setting up all the routes you require for the project. As the last step, we have the urlRoutes extension function which we need to register in the configureRouting() extension function.

val domainLocator = DomainLocator
val exceptionLocator = ExceptionLocator

fun Application.configureRouting() {
    install(Locations)
    routing {
        urlRoutes(domainLocator.provideDomainProvider(), exceptionLocator.provideExceptionProvider())
    }
}

At last, we need to register all the Application extensions functions in the Application file's module like,

fun main(args: Array<String>): Unit =
    io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {
    configureStatusPages()
    configureRouting()
    configureSerialization()
}

Here, configureStatusPages() in extension in the,

base -> Status

and configureSerialization() is the extension in the,

base -> Serialization

configureSerialization() looks like,

fun Application.configureSerialization() {
    install(ContentNegotiation) {
        gson {
            setPrettyPrinting()
        }
    }
}

In this configureSerialization(), we install ContentNegotiation which helps in serialization of the JSON request and response.

This is all that we have to need to create our own URL Shortening Service.

Definitely, we can add a lot of other things to this like,

  • Having an expiry time to the short URL.

If you like the series, do share it with folks who want to read and learn :)

Check the code here.

Thank you for reading :) Hope you learned something from my experience.

If you have anything more to discuss, I will be happy to get connected on twitter.

Did you find this article valuable?

Support Himanshu Singh by becoming a sponsor. Any amount is appreciated!