What’s a slug
A slug
is a human-readable, unique identifier, used to identify a resource instead of a less human-readable identifier like an id
. You use a slug
when you want to refer to an item while preserving the ability to see, at a glance, what the item is.
Typically slugs are used when making search-engine optimised urls, so for example the url of this post is https://medium.com/@davesag/whats-a-slug-f7e74b6c23e0. There are actually two slugs in that url, my username
, @davesag
and the slug for this specific post, whats-a-slug-f7e74b6c23e0
. Of that the whats-a-slug
part comes from the title, and, for reasons only known to the people at Medium, they’ve added a short uuid
to the end of it. The only reason I’d think of for doing that is if a user was able to write two stories with the same title, so I guess that can happen.
An example
A more interesting example is when your system stores a set of Products, and each product has a Category.
In pseudo-code, here’s a fairly typical set of models you’d see in a database.
Product {
id: integer (auto-incremented, unique),
name: string,
slug: string (derivedFrom(name), unique),
category: belongsTo(Category, id)
}Category {
id: integer (auto-incremented, unique),
name: string,
slug: string (derivedFrom(name), unique),
products: hasMany(Products, id)
}
Now let’s say you want to offer an API that lists the products for a given category.
You could define a route as follows:
GET /category/:id/products
But that’s going to give you a rather ugly URL.
So it’s better to define the route as:
GET /category/:slug/products
So if you have a category
‘books’ then someone calling your API will call:
GET /category/books/products
Lean API Design
General principles
An API shall:
- not return the internal
id
used within the database, but return aslug
instead, - return the minimum amount of data to be useful, and
- return enough data to minimise the need for repeated requests
Example data based on the model above
Category
{
id: 1,
name: "Books",
slug: "books"
}
Products
[
{
id: 1,
name: "Old Man's War",
slug: "old-mans-war",
category: 1
},
{
id: 2,
name: "All Systems Red",
slug: "all-systems-red",
category: 1
}
]
A call to GET /category/books
would return:
{
name: "Books",
slug: "books",
products: [
{
name: "Old Man's War",
slug: "old-mans-war"
},
{
name: "All Systems Red",
slug: "all-systems-red"
}
]
}
There is no need to return the category
information with each product
as the requester already knows the category
, however by the same logic there should also not be a need to return the category
’s slug
either.
For the sake of consistency it’s always good practice to return an item’s slug
, even if that information is redundant. The consumer of the API can usually save itself some complexity if the slug
is returned.
A more interesting example
Localisation is often handled by using a data structure of the form:
{
[locale]: string
}
In pseudo-code, here’s how that would be structured in the database:
Product {
id: integer (auto-incremented, unique),
name: hstore({
[locale]: string
}),
slug: string (derivedFrom(name), unique),
category: belongsTo(Category, id)
}Category {
id: integer (auto-incremented, unique),
name: hstore({
[locale]: string
}),
slug: string (derivedFrom(name), unique),
products: hasMany(Products, id)
}
Each model would need an instance function like:
function getLocalised(field, locale) {
return this[field][locale]
}
The API would need a way to know which locale
to return. This is typically done by either adding a locale
param to the request
, or in the request
’s Accept-Language
header, or if the request
is authenticated, then you might associate the desired locale
with a user
record.
Example data based on the model above
Category
{
id: 1,
name: {
en: "Books",
vn: "Sách"
},
slug: "books"
}
Products
[
{
id: 1,
name: {
en: "Old Man's War",
vn: "Chiến tranh của lão già"
},
slug: "old-mans-war",
category: 1
},
{
id: 2,
name: {
en: "All Systems Red",
vn: "Tất cả các hệ thống đều đỏ"
},
slug: "all-systems-red",
category: 1
}
]
A request to GET /product/all-systems-red?locale=vn
would return:
{
name: "Tất cả các hệ thống đều đỏ",
slug: "all-systems-red",
category: {
name: "Sách",
slug: "books"
}
}
Seeding your database
When setting up a system you want to avoid having to specify object ids
in seed data. Seed data is often defined in a yml
file. Using the database model defined above we can write a seed file as follows:
seed-data.yml
- slug: books
name:
en: Books
vn: Sách
products:
- slug: old-mans-war
name:
en: Old Man's War
vn: Chiến tranh của lão già
- slug: all-systems-red
name:
en: All Systems Red
vn: Tất cả các hệ thống đều đỏ
Data like this are easy to import.
Logging
When writing out information to logs it’s fairly normal for those logs to be intended for both human and machine consumption.
It’s generally a bad idea to write ids
out into logs. Writing out slugs
on the other hand makes logs easily readable by people, and because slugs
function as ids
as well, they are still useful for machines.
How to make a slug
A good slug is short, descriptive, lower-case, has no accents, or ambiguous or hard to read-at-a-glance characters, and is unique.
Links
Slugify
in different langauges
Javascript
https://github.com/simov/slugifyRust
https://github.com/mattgathu/slugifyJava
https://github.com/slugify/slugifyRuby
https://github.com/Slicertje/SlugifyPython
https://github.com/un33k/python-slugifySwift
https://github.com/nodes-vapor/slugifyGo
https://github.com/mozillazg/go-slugify
Books
—
Like this but not a subscriber? You can support the author by joining via davesag.medium.com.