URL Shortener
I was trying to get to sleep on a Wednesday night - I check my phone, it’s 2:54 AM. A feeling of dread comes over me as I realise I’m not going to be able to get more than 5 hours of sleep - again.
I’ve been a software developer for close to 10 years now. How did it get to this?
My deployment broke production at 10pm (because I never learn) and I had to deal with our infrastructure coupled with acute stress for the next 3 hours. As I sat there contemplating my life choices, I had an idea. Can we do better?
I don’t want to have to deal with Terraform and Kubernetes at midnight. I want to write scalable code and just get it deployed. I want my dependencies generated and managed for me. I want to be able to sleep at night.
It’s 2023. Surely we can do better. As I stared blankly at my white ceiling, I decided to see if it was possible.
I suddenly sat up in bed. Can I create a useful app, with some sort of database state, a custom subdomain, focus only on my application code without needing to worry about infrastructure and get it done in 10 minutes or less? I’ll write a URL shortener or something. I’ll write in Rust. I’ll write it tonight.
Design
I got out of bed and turned on the lights in my office. I sat down on my ergonomic chair and power up a comically large curved monitor. Arch boots up. I quickly message a friend of mine on Signal to remind them that I use Arch. Now I’m ready to code. Let’s build this thing.
The API is going to be simple. No reason for GUIs or anything like that - I am engineer, therefore I’ve convinced myself that UI’s peaked with the 1970’s teletype.
Life’s short so I’m going to build an HTTP API. The simplest thing I can come up with.
You can shorten URLs like this:
curl -X POST -d 'https://www.google.com' https://myapp.com
https://myapp.com/uvAivJ
And you get redirected like this:
curl https://myapp.com/uvAivJ
< HTTP/2 301
...
Yeah that’ll work.
Next I’ll need some sort of database to store the urls. I briefly considered using a bijective compression scheme without needing database state, but let’s face it I’m not really sure what a bijection is and it’s already 3:02 AM.
I’ll just get a Postgres instance with a basic schema:
CREATE TABLE urls (
id VARCHAR(6) PRIMARY KEY,
url VARCHAR NOT NULL
);
Genius.
I’ll add an index or something so that the database doesn’t do a linear search on every request. No one is really going to use this - but I can already feel the judgement of anyone who happens to glance over my source code. I need to be able to explain to people that you can search for urls in constant time, implying I understand complexity theory.
Ok I’m ready. It’s 3:05 AM. I have 10 minutes. I pick up my black vape and take a large hit. Smoke fills up the room and I can’t see the screen any more. Whatever. I try to crack my fingers and neck for some dramatic flair, fail, and open a terminal.
Building the Barebones - 09:59 minutes remaining
I’m using Shuttle for this project. It’s a serverless platform built for Rust and I don’t have to deal with provisioning databases, or subdomains or any of that gunk. I already have the CLI installed.
I stop and think for a little bit - which web framework do I want to use? I think I’m going to go with Rocket. It’s pretty much production ready with a sweet API and I’m reasonably proficient with it.
To initialize a rocket project with boilerplate, I can use the Shuttle CLI init
command:
cargo shuttle init --template rocket url-shortener
This leaves me with the following main.rs
file:
#[macro_use]
extern crate rocket;
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
#[shuttle_runtime::main]
async fn rocket() -> shuttle_rocket::ShuttleRocket {
let rocket = rocket::build().mount("/", routes![index]);
Ok(rocket.into())
}
As you can see, this imports a few dependencies. Luckily, the init
command takes care
of this as well, leaving us with the following Cargo.toml
:
[package]
name = "url-shortener"
version = "0.1.0"
edition = "2021"
[dependencies]
rocket = "0.5.0"
shuttle-rocket = "0.48.0"
shuttle-runtime = "0.48.0"
tokio = "1.26.0"
The init
command also created a new Shuttle project for us. This starts an
isolated container in Shuttle’s infrastructure.
Now, to deploy:
$ cargo shuttle deploy
Packaging url-shortener v0.1.0 (/private/shuttle/examples/url-shortener)
Archiving Cargo.toml
Archiving Cargo.toml.orig
Archiving src/main.rs
Compiling tracing-attributes v0.1.20
Compiling tokio-util v0.6.9
Compiling multer v2.1.0
Compiling hyper v0.14.27
Compiling rocket_http v0.5.0
Compiling rocket_codegen v0.5.0
Compiling rocket v0.5.0
Compiling shuttle-rocket v0.48.0
Compiling shuttle-runtime v0.48.0
Compiling url-shortener v0.1.0 (/opt/shuttle/crates/url-shortener)
Finished dev [unoptimized + debuginfo] target(s) in 1m 01s
Project: url-shortener
Deployment Id: 3d08ac34-ad63-41c1-836b-99afdc90af9f
Deployment Status: DEPLOYED
Host: url-shortener.shuttleapp.rs
Created At: 2022-04-13 03:07:34.412602556 UTC
Ok… this seemed a little too easy, let’s see if it works.
$ curl https://url-shortener.shuttleapp.rs/
Hello, world!
Hm, not bad. I pour myself another shot…
Adding Postgres - 07:03 minutes remaining
This is the part of my journey where I usually get a little flustered. I’ve set up databases before, but it’s always a pain. You need to provision a VM, make sure storage isn’t ephemeral, install and spin up the database, create an account with the correct privileges and secure password, store the password in some sort of secrets manager in CI, add your IP address and your VM’s IP address to the list of acceptable hosts etc etc etc. Oof that sounds like a lot of work.
Shuttle does a lot of this stuff for you - I just didn’t remember how. I
quickly head over to the
Shuttle shared database
section in the docs. I added the sqlx
dependency to Cargo.toml
and change
one line in main.rs
:
#[shuttle_runtime::main]
async fn rocket(#[shuttle_shared_db::Postgres] pool: PgPool) -> ShuttleRocket {
// [...]
}
By adding a parameter to the main rocket
function, Shuttle
will
automatically provision a Postgres database for you, create an account and hand
you back an authenticated connection pool which is usable from your application
code.
Lets deploy it and see what happens:
cargo shuttle deploy
...
Finished dev [unoptimized + debuginfo] target(s) in 19.50s
Project: url-shortener
Deployment Id: 538e41cf-44a9-4158-94f1-3760b42619a3
Deployment Status: DEPLOYED
Host: url-shortener.shuttleapp.rs
Created At: 2022-04-13 03:08:30.412602556 UTC
Database URI: postgres://***:***@pg.shuttle.rs/db-url-shortener
I have a database! I couldn’t help but chuckle a little bit. So far so good.
Setting up the Schema - 06:30 minutes remaining
The database provisioned by Shuttle
is completely empty - I’m going to need to
either connect to Postgres and create the schema myself, or write some sort of
code to automatically perform the migration. As I start to ponder this seemingly
existential question I decide not to overthink it. I’m just going to go with
whatever is easiest.
I connect to the database provisioned by Shuttle using pgAdmin using the provided database URI and run the following script:
CREATE TABLE urls (
id VARCHAR(6) PRIMARY KEY,
url VARCHAR NOT NULL
);
As I was ready to Google ‘how to create index postgres’ I realised that since
the id
used for the url lookup is a primary key, which is implicitly a
‘unique’ constraint, Postgres would create the index for me. Cool.
Writing the Endpoints - 05:17 remaining
The app’s going to need two endpoints - one to shorten
URLs and one to
retrieve URLs and redirect
the user.
I quickly created two stubs for the endpoints while I thought about the actual implementation:
#[get("/<id>")]
async fn redirect(id: String, pool: &State<PgPool>) -> Result<Redirect, Status> {
unimplemented!()
}
#[post("/", data = "<url>")]
async fn shorten(url: String, pool: &State<PgPool>) -> Result<String, Status> {
unimplemented!()
}
I decided to start with the shorten method. The simplest implementation I could think of is to generate a
unique id on the fly using the nanoid crate
and then running an INSERT
statement. Hm - what about duplicates? I decided
not to overthink it 🤷.
#[post("/", data = "<url>")]
async fn shorten(url: String, pool: &State<PgPool>) -> Result<String, Status> {
let id = &nanoid::nanoid!(6);
let p_url = Url::parse(&url).map_err(|_| Status::UnprocessableEntity)?;
sqlx::query("INSERT INTO urls(id, url) VALUES ($1, $2)")
.bind(id)
.bind(p_url.as_str())
.execute(&**pool)
.await
.map_err(|_| Status::InternalServerError)?;
Ok(format!("https://url-shortener.shuttleapp.rs/{id}"))
}
Next I implemented the redirect
method in a similar spirit. At this point I
started to panic as it was really getting close to the 10 minute mark. I’ll do a
SELECT *
and pull the first url that matches with the query id. If the id does
not exist, you get back a 404
:
#[get("/<id>")]
async fn redirect(id: String, pool: &State<PgPool>) -> Result<Redirect, Status> {
let url: (String,) = sqlx::query_as("SELECT url FROM urls WHERE id = $1")
.bind(id)
.fetch_one(&**pool)
.await
.map_err(|e| match e {
Error::RowNotFound => Status::NotFound,
_ => Status::InternalServerError
})?;
Ok(Redirect::to(url.0))
}
Whoops there’s a typo in the SQL query.
After I fixed my typo and sorted out the various unresolved dependencies by letting my IDE do the heavy lifting for me, I deployed to Shuttle for the last time.
Moment of truth - 00:25 minutes remaining
Feeling like an off-brand Tom Cruise in mission impossible I stared intently at
the clock counting down as Shuttle deployed my url-shortener. 19.3 seconds and
we’re live. As soon as the DEPLOYED
dialog came up, I instantly tested it out:
$ curl -X POST -d "https://google.com" https://url-shortener.shuttleapp.rs
https://s.shuttleapp.rs/XDlrTB⏎
I then copy/pasted the shortened URL to my browser and, lo an behold, was redirected to Google.
I did it.
Retrospective - 00:00 minutes remaining
With a sigh of relief I pushed myself back from my desk. I refilled my mug, picked it up and headed to my derelict balcony. As I slid open the the windows and the cold air flowed into my apartment, I took two steps forward to rest my elbows and mug on the railing.
I sat there for a while reflecting on what had just happened. I had succeeded. I’d successfully built a somewhat trivial app quickly without needing to worry about provisioning databases or networking or any of that jazz.
But how would this measure up in the real world? Real software engineering is complex, involving collaboration across different teams with different skill-sets. The entire world of software is barely keeping it together. Is it really feasible to replace our existing, tried and tested cloud paradigms with a new paradigm of not having to deal with infrastructure at all? What I knew for sure is I wasn’t going to get to the bottom of this one tonight.
As I went back to my bedroom and laid once more in bed, I noticed I was grinning. There’s a chance we really can do better. Maybe we’re not exactly there yet, but my experience tonight had given me a certain optimism that we aren’t as far as I once thought. With the promise of a brighter tomorrow, I turned on my side and fell asleep.
Was this page helpful?