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
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.