Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Short Url Laravel Package

andrey-helldar/short-url

A Laravel package for generating, storing, and resolving short URLs. It provides a simple API and model-based management for creating short links and redirecting visitors, with support for custom URL generators and familiar Laravel conventions.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is deliberately small — a migration, a model, a config file, a route registration, and a redirect handler. Treat src/ as the documentation: read it once end-to-end (it's a handful of files) before writing any integration code.

Install and boot

composer require andrey-helldar/short-url

# Publish config + migration (grab the provider/tag from
# vendor/andrey-helldar/short-url/composer.json -> extra.laravel)
php artisan vendor:publish

php artisan migrate
php artisan route:list | grep -i short

Where to look first, in this order

  1. config/short-url.php (after publishing) — route prefix, domain, table name, redirect behaviour.
  2. database/migrations/*_create_*_table.php — the real schema: destination column, code column, uniqueness, timestamps. This tells you more about the package than the README does.
  3. The route file / service provider boot() — how the short-code route is registered and what middleware wraps it.
  4. The model — what's fillable and whether code generation lives in a mutator, an observer, or a static factory.

First use case: shorten-and-redirect

Get the smallest possible loop working before customising anything: create one record, hit /{prefix}/{code}, confirm a redirect lands on the destination. Do this on a scratch route or in php artisan tinker:

$link = ShortUrl::create(['url' => 'https://example.com/a/very/long/path']);
$link->code; // the generated short code

If that works, the rest of the package is configuration. If it doesn't, the failure is almost always the migration not being published/run, or the route being shadowed by a catch-all in routes/web.php.

Implementation Patterns

Wrap creation in your own service, not scattered ::create() calls. The package's job is code generation and persistence; validation, authorisation, and deduplication are yours.

final class LinkShortener
{
    public function shorten(string $url, ?int $userId = null): ShortUrl
    {
        // Deduplicate: reuse an existing row for the same destination
        $existing = ShortUrl::where('url', $url)->first();

        return $existing ?? ShortUrl::create([
            'url'      => $url,
            'user_id'  => $userId, // requires adding the column yourself
        ]);
    }
}

Add your own columns on top of the published migration. The shipped table is minimal. In practice you will want user_id, hits, expires_at, is_active, and deleted_at. Edit the published migration before the first php artisan migrate in production, or ship a follow-up migration that Schema::table(...) adds them.

Expose it as an API endpoint with a FormRequest rather than letting the package see raw input:

class StoreLinkRequest extends FormRequest
{
    public function rules(): array
    {
        return ['url' => ['required', 'url', 'max:2048', 'starts_with:http://,https://']];
    }
}

// Controller
return response()->json([
    'short' => url($link->code), // or route('short-url.redirect', $link->code)
], 201);

Build the full short URL from the route name, never by string concatenation. That way changing the prefix or domain in config doesn't silently break every link you've already handed out:

public function getShortAttribute(): string
{
    return route('short-url.redirect', $this->code);
}

The exact route name is whatever the provider registers — confirm with php artisan route:list.

Customise the prefix/domain in config, not in code. Prefix and domain belong in config/short-url.php driven by env vars (SHORT_URL_PREFIX, SHORT_URL_DOMAIN), so staging and production can differ and your short links can live on a dedicated host like s.example.com. Add the domain to config/session.php / config/cors.php if the redirect route needs to share cookies or be fetched cross-origin.

Attach middleware to the redirect route. Since the package registers the route for you, the practical hook is a config-driven middleware array (if supported) or overriding the route after the provider boots. Rate-limit it — an unthrottled redirect endpoint is a free traffic amplifier and a scraping target:

// AppServiceProvider::boot()
RateLimiter::for('short-url', fn ($request) => Limit::perMinute(120)->by($request->ip()));

Count hits without blocking the redirect. The redirect is a hot path; don't do a synchronous write on every request unless you must. Queue an increment, or use Cache::increment() and flush to the table periodically.

Test the redirect, not the generator. The valuable feature test is the HTTP contract:

it('redirects a short code', function () {
    $link = ShortUrl::create(['url' => 'https://example.com/target']);

    $this->get(route('short-url.redirect', $link->code))
        ->assertRedirect('https://example.com/target');
});

it('404s an unknown code', function () {
    $this->get('/s/does-not-exist')->assertNotFound();
});

Gotchas and Tips

  • Archived since 2021. Last release March 2021, 10 stars, no maintenance. Do not build on this expecting fixes. Either vendor the few files into your own app/ namespace, or fork it. If you're starting fresh, ashallendesign/short-url or 30 lines of your own code is a better bet. If you must use it, pin the exact version and add a note in your README.
  • Laravel version constraint will bite first. A 2021 package targets Laravel 7/8-era APIs. On Laravel 10/11 you'll likely hit composer conflicts or a service provider using removed methods. Check composer.json's require block before promising this in a sprint.
  • config:cache traps env values. If prefix/domain come from env() inside the config file, changing .env on a cached-config server does nothing. Always php artisan config:clear after deploy tweaks — and never call env() outside config/.
  • Publishing the migration is one-shot. Use vendor:publish --force cautiously; once you've edited the migration for production, re-publishing overwrites your columns. Prefer a separate migration for additions.
  • Code collisions are your problem at scale. The default alphabet is small and short codes are short. A 5-character code over ~36 symbols is ~60M combinations, but birthday-problem collisions appear in the low thousands of rows. Rely on the DB unique index and retry generation in a loop, or lengthen the code in config.
  • Watch the unique index on the destination URL. If the schema enforces uniqueness on url, a second shorten of the same target throws — which may be desirable (dedupe) or a hard failure you didn't plan for. Check the migration before writing your service.
  • Reserved-word and shadowing collisions. A short code like admin, api, login, or assets can be swallowed by an earlier route, or can swallow one of yours if the short route uses a permissive {code} constraint. Constrain it (->where('code', '[A-Za-z0-9]{4,10}')) and keep the prefix distinctive (/s/, /l/) rather than root-level.
  • Codes are case-sensitive in your generation, case-insensitive in the real world. People retype links. If your alphabet mixes case, either normalise lookups or generate case-insensitively to avoid two codes that differ only by case.
  • 301 vs 302 matters more than you think. A permanent redirect is cached by browsers essentially forever — you can never repoint that link, and users who hit a deleted link keep going to the old target. Default to 302/307 unless the destination is genuinely immutable. If the package hardcodes one, that's a fork-level change.
  • Open redirect risk. The package stores whatever URL it's given. Validate the scheme (http/https only — no javascript:, data:, file:) and reject internal/private hosts if untrusted users can create links. Also decide whether the redirect should carry Referrer-Policy: no-referrer.
  • No built-in analytics, expiry, or deactivation. There's no hit tracking, no expires_at, no "revoke" story. Every one of those is a column plus a redirect-time check you write yourself. Plan for that in the estimate.
  • Deleted vs missing. A soft-deleted or expired link should return 410 Gone (and optionally a friendly page), not 404 — otherwise your support queue fills with "the link is broken" reports that you can't distinguish from typos.
  • Debugging checklist: php artisan route:list (is the route registered, and does anything precede it?), php artisan tinker (ShortUrl::count(), does a row exist?), check storage/logs/laravel.log for the redirect handler's exceptions, and curl with -I to see the actual status code and Location header rather than trusting the browser.
  • Extension points, in order of preference: config overrides → a subclass bound via the container if the package resolves the model from config → publishing and editing the package's own files (last resort, but legitimate for an archived package). Binding your own implementation against an interface, if one exists in src/Contracts, is the cleanest seam — look there before editing vendor/.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core