wilsenhc's avatar

UUID/ULID vs Nanoids

Hello! This is my first post here in Laracast.

I am currently in the process of reworking an API. One of the requirements that have come up is about to stop using the Autoincrement IDs in the URLs.

I would like to hear some opinions about the usage of UUID/ULIDs vs Nanoids. Specifically in Laravel. If you have any recommenndations/package for using Nanoids in Laravel it would be appreciated.

Part of the rework consists of upgrading to Laravel 10, so no worries for installing a separate UUID/ULIDs package since that is already integrated.

0 likes
2 replies
LaryAI's avatar
Level 58

Hello and welcome to the Laracasts forum!

The decision between using UUID/ULIDs and Nanoids often comes down to your specific requirements and preferences. Both are excellent choices for generating unique identifiers that are not sequential like auto-increment IDs, which can expose the number of records in your database and potentially lead to enumeration attacks.

Here are some considerations for each:

UUID/ULIDs:

  • UUIDs are standardized and widely used, which means they are well-tested and supported in many systems.
  • Laravel 10 comes with built-in support for UUIDs, making it easy to integrate without additional packages.
  • ULIDs are similar to UUIDs but are lexicographically sortable and contain a timestamp component.

Nanoids:

  • Nanoids are URL-friendly, more compact, and offer a customizable alphabet and size.
  • They are less standardized than UUIDs but can be more efficient in terms of storage and generation speed.
  • There is no built-in Laravel support for Nanoids, but you can use a package like hidehalo/nanoid-php to integrate them into your application.

To use Nanoids in Laravel, you can install the hidehalo/nanoid-php package via Composer:

composer require hidehalo/nanoid-php

Then, you can use it in your models to generate Nanoid identifiers. Here's an example of how you might do this:

use Hidehalo\Nanoid\Client as NanoidClient;

class YourModel extends Model
{
    public $incrementing = false;
    protected $keyType = 'string';

    protected static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            $nanoid = new NanoidClient();
            $model->{$model->getKeyName()} = $nanoid->generateId($size = 21); // You can specify the size you want
        });
    }
}

In this example, we're overriding the boot method of the model to hook into the creating event. Before a new model is saved for the first time, we generate a Nanoid and set it as the primary key.

Remember to set $incrementing to false and $keyType to string to let Eloquent know that you're using a non-incrementing string as the primary key.

Ultimately, the choice between UUID/ULIDs and Nanoids will depend on your specific needs for uniqueness, readability, and storage efficiency. Both are valid options, and Laravel can accommodate either through its built-in features or with the help of additional packages.

min-mahatara's avatar

Note: I used ChatGPT to help structure parts of this explanation, but the approach described is from my actual production experience.

Laravel 12.x supports UUIDv7 out of the box (and Laravel 11.x introduced it). However, if you want to use both UUID7 and ULID together on the same model, you'll need a custom solution.

✅ Internal vs. External Identifiers

Using UUIDv7 for primary keys (e.g., id) is an excellent choice—especially for scalable applications. UUIDs are globally unique and make tasks like data merging and system interoperability much safer by minimizing the risk of key collisions.

One of the key advantages of using UUIDs as internal identifiers is that you can perform direct database operations without relying solely on APIs. This encourages cleaner, more decoupled API design.

However, UUIDs (especially v7) are long and not user-friendly. Therefore, I strongly recommend not exposing UUIDs to end users. Instead, use ULIDs for public-facing IDs.

For example:

  • Use id (UUIDv7) internally for primary keys, foreign keys, and unique constraints.
  • Use public_id (ULID) externally in URLs, APIs, and UI displays.

This separation protects internal infrastructure and keeps your system's internals abstracted from external consumers.

🛠 CRUD Operations and Routing

Always expose and interact with the public_id for all CRUD operations. This adds a slight abstraction layer but helps shield internal identifiers and improves system design.

Laravel allows you to customize route model binding to use public_id:

public function getRouteKeyName(): string
{
    return 'public_id';
}

⚖️ Performance Considerations

It's true that UUIDs and ULIDs can be slightly slower than auto-incrementing integers due to their size and index performance. However, the benefits—global uniqueness, scalability, and flexibility—far outweigh these drawbacks, especially for distributed systems.

UUIDv7 offers the additional benefit of lexical sortability. Because it encodes a timestamp in milliseconds, newly generated UUIDs are naturally ordered. This improves index locality and query performance compared to UUIDv4.

🔄 Data Portability

Another major benefit of UUIDs: they're ideal for syncing data across environments or services. Since UUIDs are unique across systems, there's very little risk of collision when importing or migrating data.


✅ Recommended Strategy

  • Internal IDs (id, FK, PK, UK): Use UUIDv7. Never expose.
  • External IDs (public_id): Use ULID. Always expose.

🧩 Custom Trait: HasUuidsAndPublicId

Since Laravel does not currently support using both HasUuids and HasUlids traits together in a single model, you can define a custom trait to achieve this:

Use this trait on your models:

class User extends Model
{
    use HasUuidsAndPublicId;

    protected $table = 'users';
}

⚠️ Note: Trying to use both HasUuids and HasUlids traits simultaneously, like this:

use HasUuids, HasUlids;

will not work, as Laravel does not currently support combining both traits on the same model.


Final Thoughts

For scalable Laravel applications:

  • Use UUIDv7 for internal keys.
  • Use ULIDs for external identifiers.
  • Encapsulate this logic in custom traits like HasUuidsAndPublicId.

This strategy balances performance, security, and maintainability, and is already adopted by many large-scale systems.

1 like

Please or to participate in this conversation.