UltraMega Blog
27Mar/0922

Creating a TinyURL Clone

This tutorial will explain how to create a basic clone of the TinyURL service. If you've never heard of it, TinyURL allows you to turn long URLs into shorter links so they can be easily sent via email or other means. It might be fun to create your own personalized version that works just as well, and it's very easy to do. We'll go over the database structure, creating the required PHP script, and using mod_rewrite to make nicer URLs.

First of all, we need a database set up. Here is the SQL to create the database table:

1
2
3
4
5
6
7
CREATE TABLE `turl` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `key` VARCHAR(32) DEFAULT NULL,
  `url` text NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `key` (`key`)
);

This will create a table called "turl" (you can call it anything) with 2 fields:
"id" = type: int, length: 11, primary key, auto_increment, default: NULL
"key" = type: varchar, length: 32, unique key
"url" = type: text

If you use a graphical database management system, this should be enough info to get it set up. The "key" field will store a unique string to identify the link, and "url" will store the actual URL.

Now we need to start working with PHP. First we'll set up our configuration variables:

1
2
3
4
5
6
<?php
$baseurl = 'http://example.com/'; //address to use as the first part of the short URLs (note trailing slash)
$sql_host = 'localhost'; //host of your mysql database (usually localhost)
$sql_user = 'turl'; //mysql user with access to your database
$sql_pass = 'password'; //password for this user
$sql_db = 'turl'; //the name of your database

You can read the comments to figure out what each variable is. This will make it easier to move or reuse the script by only changing these variables.

Now we need to connect to the database:

8
9
$db = mysql_connect($sql_host, $sql_user, $sql_pass);
mysql_select_db($sql_db, $db);

Now, let's create the part that redirects your URL to the original. All we need to do is find the URL that matches the given key and set the headers to redirect to it. We'll check if the GET parameter called "k" exists and then use it as the key (after sanitizing it, of course):

11
12
13
14
15
16
17
18
19
20
21
if(isset($_GET['k'])) {
   $k = mysql_real_escape_string($_GET['k'], $db);
   if($result = mysql_query("SELECT `url` FROM `turl` WHERE `key` = '" . $k . "'", $db)) {
      if(mysql_num_rows($result) > 0) {
         $row = mysql_fetch_row($result);
         header('HTTP/1.1 301 Moved Permanently');
         header('Location: ' . $row[0]);
         exit;
      }
   }
}

Now it's time to handle the creation of the short URLs. We'll start with the PHP skeleton and the HTML form:

23
24
25
26
27
28
29
30
31
32
33
34
35
if($_POST['submit'], $_POST['url'])) {
   // create short URL
}
else {
?>
<form method="post" action="short.php">
<label>URL: <input type="text" name="url" id="url" /></label>
<br />
<input type="submit" name="submit" id="submit" value="Shorten" />
</form>
<?php
}
?>

First we check if the "url" was posted by the form, and if it wasn't we show the form. The form only needs a URL field and a submit button. Also, be sure to change the action parameter to whatever your script is called.

Now for the part that creates the short URL. The process goes like this: check if the URL is valid (optional), check if the URL has already been shortened, insert the URL into the database, create the key, and show the new link.

Validate and sanitize URL. Here, we use the filter_var function as a quick and easy way to validate the URL:

23
24
25
if(isset($_POST['submit'], $_POST['url'])) {
   if(filter_var($_POST['url'], FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED) !== false) {
      $url = mysql_real_escape_string($_POST['url'], $db);

Check if the URL already exists and get the key if it exists:

26
27
28
29
30
31
      if($result = mysql_query("SELECT `key` FROM `turl` WHERE `url` = '" . $url . "'", $db)) {
         if(mysql_num_rows($result) > 0) {
            $row = mysql_fetch_row($result);
            $key = $row[0];
         }
      }

If the URL is fresh, insert it into the database, then convert the id to base 36 to use as the visible key. Using a key based on an auto_increment field ensures there are no duplicate keys. After we are done checking/inserting, we display the final link:

32
33
34
35
36
37
38
39
      if(!isset($key) && mysql_query("INSERT INTO `turl` (`url`) VALUES ('" . $url . "')", $db)) {
         $id = mysql_insert_id($db);
         $key = base_convert($id, 10, 36);
         mysql_query("UPDATE `turl` SET `key` = '" . $key . "' WHERE `id` = '" . $id . "'");
      }
      echo '<a href="' . $baseurl . $key . '" target="_blank">' . $baseurl . $key . '</a>';
   }
}

Here is the finished product:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
$baseurl = 'http://example.com/'; //address to use as the first part of the short URLs (note trailing slash)
$sql_host = 'localhost'; //host of your mysql database (usually localhost)
$sql_user = 'turl'; //mysql user with access to your database
$sql_pass = 'password'; //password for this user
$sql_db = 'turl'; //the name of your database
 
$db = mysql_connect($sql_host, $sql_user, $sql_pass);
mysql_select_db($sql_db, $db);
 
if(isset($_GET['k'])) {
   $k = mysql_real_escape_string($_GET['k'], $db);
   if($result = mysql_query("SELECT `url` FROM `turl` WHERE `key` = '" . $k . "'", $db)) {
      if(mysql_num_rows($result) > 0) {
         $row = mysql_fetch_row($result);
         header('HTTP/1.1 301 Moved Permanently');
         header('Location: ' . $row[0]);
         exit;
      }
   }
}
 
if(isset($_POST['submit'], $_POST['url'])) {
   if(filter_var($_POST['url'], FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED) !== false) {
      $url = mysql_real_escape_string($_POST['url'], $db);
      if($result = mysql_query("SELECT `key` FROM `turl` WHERE `url` = '" . $url . "'", $db)) {
         if(mysql_num_rows($result) > 0) {
            $row = mysql_fetch_row($result);
            $key = $row[0];
         }
      }
      if(!isset($key) && mysql_query("INSERT INTO `turl` (`url`) VALUES ('" . $url . "')", $db)) {
         $id = mysql_insert_id($db);
         $key = base_convert($id, 10, 36);
         mysql_query("UPDATE `turl` SET `key` = '" . $key . "' WHERE `id` = '" . $id . "'");
      }
      echo '<a href="' . $baseurl . $key . '" target="_blank">' . $baseurl . $key . '</a>';
   }
}
else {
?>
<form method="post" action="short.php">
<label>URL: <input type="text" name="url" id="url" /></label>
<br />
<input type="submit" name="submit" id="submit" value="Shorten" />
</form>
<?php
}
?>

Of course, you can wrap a nice HTML template around the whole thing to make it look nice, but this is the basic setup for a URL shortener service.

Now, as a finishing touch, we can use mod_rewrite to make URLs look nicer (and shorter) if your server supports it. Place this code in a file named ".htaccess" in the same directory as your script:

RewriteEngine On
RewriteCond %{REQUEST_URI} \/([0-9a-z]+)$ [NC]
RewriteRule ^(.*) short.php?k=%1 [L]

This allows you to use example.com/xxxxxx instead of example.com/short.php?k=xxxxxx. Make sure to change the file name on the last line to reflect the actual name of your script.

So there you go, you now know how to make your own TinyURL clone!

Update: Changed from generating random strings to using an auto_increment converted to base 36 to avoid checking for duplicates. Also added checking for existing shortened URLs before generating a new one. Thanks to Mattle for the comments about making improvements!

Posted by Steve

Comments (22) Trackbacks (0)
  1. I think generating a random 6 character string then checking for that string in the table every time you create a shortened url is not the optimal solution. The number of times you would hit an already existing string would increase with the size of the table. This would mean that the more popular your service becomes, the slower it would deliver shortened urls.

    Also, this solution leaves open the possibility that two users could generate the same key. If you check for the existence of a key, find it doesn’t exist, then before you can write to the table, I also find the same key doesn’t exist, then you could write your key and url to the table, and when I try and write my key and url to the table, it will error due to duplicate primary keys.

    I think there is a way to solve both issues. If the table had an auto-incrementing primary key, then we wouldn’t need to try and guess a unique key, and new records could be written much quicker and far more reliably. The race condition that lead to the key collision above would also disappear.

    This primary key could then be encoded or hashed in some way to appear as 6 alpha or alpha-numeric characters, which would also need to be recorded to facilitate look ups when people click the links, and to find already shortened urls.

    Your solution also creates new shortened urls for the same original url, no matter how many times it’s been shortened before. This would make your table extremely large, very quickly, adding to the time it takes to deliver your service. It would be necessary to check for the existence of any url submitted for shortening. This will also create the most unique shortened urls in the limit of 6 characters, you don’t want to waste 100,000 of your precious urls all pointing to the same place.

    • You make some very good points. I do intend to update the code to check for duplicate URL’s and return the already created short URL. That’s also a good idea to create a hash based on an auto_increment field. Though I think the chance of two users simultaneously generating the same key is very small, it would help performance to avoid checking for duplicates. Thanks for the feedback.

    • Alright, I updated the tutorial with your suggestions. Thanks again!

  2. Hi!
    Got some problems with the rewrite engine.
    It only seems to allow characters 0-9 and a-z, but not e.g. 10 or 15. Then I just get page not found errors.
    I’ve chanched your script a bit to also add hits when a shortened url is visited and the ones that give errors are also not updated for a hit.
    Any suggestions?

  3. Found out following:

    When using this : RewriteCond %{REQUEST_URI} \/([0-9a-z]{2})$ [NC]
    so adding the {2} it will work for e.g. 10 or 15 or 1b. But 1 of 5 or b or 130 still does not work 🙁

  4. How about the huge issue with spamming bots? How do you block them?

  5. hey, you say you can write it like this, exempel.com/xxxxxx but can you also write it like this xxxxxx.exempel.com then? 😛

  6. Hi,
    It’s really nice work , however I noticed something wrong happens when using www in my site url , for example http://mysite.com/123 will always work great but http://www.mysite.com/123 may work and may not , I think there is something to do with .htaccess file to force http://www.mysite.com/123 to http://mysite.com/123 but i’m very bad in regEx and in editing .htaccess

  7. Hi, can you also add alias in this as well? (eg http://website.com/random goes to http://website1.com)

  8. I was create all like it is in tut.. Only table name isnt turl, its 001.
    PHP code is in go.php and form is in start.html

    In php code action=”go.php”.
    When i start domain.com/start.html its ok..
    when click to shorten back url in new windows is root (domain.com)..
    Where d fak is error??

  9. Thanks! .htaccess code was useful to me.

  10. Hello,
    i got an basic error, dunno why ?
    The requested URL /3 was not found on this server.
    anyone can help me thx

    • It sounds like URL rewriting isn’t set up. Are you using Apache and is your .htaccess file set up? You’ll have to use another technique for other servers or stick to the long version (/?k=xxx instead of /xxx) for links.

  11. hum.. i understood, u can just tinyUrl your own website?

    But i would know how to use this like TinyUrl, for all websites ..

  12. Hi,

    i’m getting an ‘internal server error’ when i added the .htaccess file

  13. I think that, by the time that random 6 character phrases are repeating (1 chance in 2 176 782 336) no-one would use your service because the key is 10 digits long, as apposed to bit.ly or tinyurl.com

  14. Thanks for your tutorials, but it never work when i’m trying its


Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

No trackbacks yet.