Store Files by ID, Not by Slug

📖 2 minutes read

One of the easiest ways to create accidental “data migrations” is to put user-facing strings in places you later treat as stable identifiers.

A common example: storing uploaded files under a directory that includes a slug or title. It feels tidy… until someone renames the record and suddenly your storage path no longer matches reality.

The rule of thumb

Use an immutable identifier for storage paths. Keep human-readable names as metadata.

That usually means:

  • A stable key (ULID/UUID/integer id) for file paths and URLs
  • A separate column for the original filename / display name
  • A separate column for the current “pretty” label (which can change)

A simple Laravel approach

Create a model that owns the upload, and give it a stable public identifier.

// database/migrations/xxxx_xx_xx_create_documents_table.php
Schema::create('documents', function ($table) {
    $table->id();
    $table->ulid('public_id')->unique();

    $table->string('display_name');      // can change
    $table->string('original_name');     // what the user uploaded
    $table->string('storage_path');      // immutable once set

    $table->timestamps();
});

On upload, generate the storage path from the immutable identifier (not the title):

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

public function store(Request $request)
{
    $file = $request->file('file');

    $publicId = (string) Str::ulid();
    $originalName = $file->getClientOriginalName();

    $path = "documents/{$publicId}/" . Str::random(16) . "-" . $originalName;

    Storage::disk('public')->put($path, file_get_contents($file->getRealPath()));

    $document = Document::create([
        'public_id' => $publicId,
        'display_name' => pathinfo($originalName, PATHINFO_FILENAME),
        'original_name' => $originalName,
        'storage_path' => $path,
    ]);

    return response()->json([
        'id' => $document->public_id,
    ]);
}

Why this pays off

  • Renames are trivial: update display_name, nothing else.
  • Downloads don’t break when labels change.
  • You can safely rebuild “pretty URLs” later without moving files.

If you want tidy URLs, you can still add a slug — just don’t make your storage system depend on it.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *