npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

prisma-extension-slug

v0.1.1

Published

Prisma Client Extension for automatic slug generation with support for unique constraints, prefix/postfix, and multiple generation modes

Readme

Prisma Slug Extension

日本語ドキュメント (Japanese Documentation)

Prisma Client Extension for automatic slug generation with support for prefix/postfix, multiple generation modes, and dependency tracking.

Features

  • 🚀 Automatic slug generation from dependent fields
  • 🔗 Prefix/Postfix support with fixed strings or field references
  • 🔄 Smart updates - regenerates only when dependent fields change
  • ✏️ Manual override - respects explicitly provided slug values
  • 📏 Length control - configurable maximum length (default: 255)
  • 🎯 Type-safe - fully typed TypeScript implementation
  • ⚙️ Flexible configuration - per-field and per-model settings

Installation

npm install prisma-extension-slug

Quick Start

1. Add /// @slug annotations to your Prisma schema

Important: Add @default("") to slug fields to make them optional in TypeScript. This allows you to create records without explicitly specifying the slug value, which will be auto-generated by the extension.

model Post {
  id    Int    @id @default(autoincrement())
  title String
  slug  String @unique @default("") /// @slug(from: ['title'])
}

model User {
  id        Int    @id @default(autoincrement())
  firstName String
  lastName  String
  username  String @unique @default("") /// @slug(from: ['firstName', 'lastName'])
}

2. Apply the extension

import { PrismaClient } from '@prisma/client';
import { slugExtension } from 'prisma-extension-slug';

// Configuration is automatically loaded from schema.prisma
const prisma = new PrismaClient().$extends(slugExtension());

3. Use it!

// Slug is automatically generated
const post = await prisma.post.create({
  data: {
    title: 'Hello World',
  },
});
console.log(post.slug); // "hello-world"

// Updates regenerate the slug
const updated = await prisma.post.update({
  where: { id: post.id },
  data: { title: 'New Title' },
});
console.log(updated.slug); // "new-title"

Configuration

Schema Annotation Syntax

Use /// @slug(options) comments on the same line as the field definition:

model Post {
  title String
  slug  String @unique /// @slug(from: ['title'])
}

Available Annotation Options

/// @slug(from: ['field1', 'field2'])
// Multiple source fields
// Input: { field1: 'Hello', field2: 'World' } → Output: "hello-world"

/// @slug(from: ['title'], prefix: 'blog-')
// Fixed string prefix
// Input: { title: 'My Post' } → Output: "blog-my-post"

/// @slug(from: ['name'], postfix: '-item')
// Fixed string postfix
// Input: { name: 'Product' } → Output: "product-item"

/// @slug(from: ['name'], prefix: 'prod-', postfix: '-item')
// Prefix and postfix combined
// Input: { name: 'Laptop' } → Output: "prod-laptop-item"

/// @slug(from: ['title'], mode: 'auto')
// Generation mode (default)
// Input: { title: 'Hello World' } → Output: "hello-world"

/// @slug(from: ['title'], mode: 'random')
// Generation mode with random suffix
// Input: { title: 'Hello World' } → Output: "hello-world-a3x9k2"

/// @slug(from: ['title'], required: true)
// Require non-empty source fields (throws error if empty)
// Input: { title: '' } → Error: "Required fields [title] for slug field are empty"

/// @slug(from: ['title'], maxLength: 20)
// Override global maxLength
// Input: { title: 'This is a very long title' } → Output: "this-is-a-very-long"

Generation Modes

mode: 'auto' (default)

  • Generates a clean slug from the source fields without any random suffix
  • Example: "hello-world" from title: "Hello World"
  • Best for human-readable URLs and when source fields are already unique

mode: 'random'

  • Appends a random 8-character suffix to the generated slug
  • Example: "hello-world-a3x9k2" from title: "Hello World"
  • Useful when:
    • Source fields may have duplicates
    • You need guaranteed uniqueness with short slugs
    • You want to obscure sequential patterns
  • Note: Collisions are still possible but rare. See Handling Unique Constraint Violations for retry logic

Complete Example

model Product {
  id       Int    @id @default(autoincrement())
  name     String
  category String
  sku      String @unique /// @slug(from: ['name'], prefix: ['category'], mode: 'random', maxLength: 50)
}

Extension Options

import { slugExtension } from 'prisma-extension-slug';

const prisma = new PrismaClient().$extends(
  slugExtension({
    schemaPath: './prisma/schema.prisma',  // Optional: custom schema path (default: auto-detect)
    maxLength: 255,                        // Optional: maximum slug length (default: 255)
  })
);

Options:

  • schemaPath: Path to your Prisma schema file (auto-detected if not specified)
  • maxLength: Maximum length for generated slugs (default: 255)

Examples

Basic Usage

Schema:

model Post {
  id    Int    @id @default(autoincrement())
  title String
  slug  String @unique @default("") /// @slug(from: ['title'])
}

Code:

const prisma = new PrismaClient().$extends(slugExtension());

const post = await prisma.post.create({
  data: { title: 'My First Post' },
});
// post.slug === "my-first-post"

Multiple Source Fields

Schema:

model User {
  firstName String
  lastName  String
  username  String @unique @default("") /// @slug(from: ['firstName', 'lastName'])
}

Code:

const user = await prisma.user.create({
  data: {
    firstName: 'John',
    lastName: 'Doe',
  },
});
// user.username === "john-doe"

Fixed Prefix

Schema:

model Post {
  title String
  slug  String @unique @default("") /// @slug(from: ['title'], prefix: 'blog-')
}

Code:

const post = await prisma.post.create({
  data: { title: 'Hello' },
});
// post.slug === "blog-hello"

Prefix and Postfix Combined

Schema:

model Product {
  name String
  sku  String @unique @default("") /// @slug(from: ['name'], prefix: 'prod-', postfix: '-item')
}

Code:

const product = await prisma.product.create({
  data: {
    name: 'Laptop',
  },
});
// product.sku === "prod-laptop-item"

Manual Override

// Explicitly provided slugs are not overwritten
const post = await prisma.post.create({
  data: {
    title: 'Hello World',
    slug: 'custom-slug',
  },
});
// post.slug === "custom-slug" (not "hello-world")

Conditional Regeneration

// Slug is only regenerated when dependent fields change
const post = await prisma.post.create({
  data: { title: 'Hello' },
});
// post.slug === "hello"

// Update non-dependent field - slug stays the same
const updated1 = await prisma.post.update({
  where: { id: post.id },
  data: { content: 'New content' },
});
// updated1.slug === "hello" (unchanged)

// Update dependent field - slug is regenerated
const updated2 = await prisma.post.update({
  where: { id: post.id },
  data: { title: 'New Title' },
});
// updated2.slug === "new-title" (changed)

How It Works

The extension uses Prisma's query hooks to intercept create, update, and upsert operations:

  1. On create: Generates slug from source fields if not manually provided
  2. On update: Regenerates slug only if dependent fields changed
  3. Smart detection: Skips generation if slug is explicitly provided
  4. Length control: Automatically truncates to maximum length
  5. Empty handling: Uses random string if source fields are empty (unless required: true)

Slug Generation Rules

  • Converts to lowercase
  • Replaces spaces and underscores with hyphens
  • Removes non-alphanumeric characters (except hyphens)
  • Removes consecutive hyphens
  • Trims leading/trailing hyphens
  • Enforces maximum length (truncates and removes trailing hyphens)

Limitations

Unique Constraint Handling

Currently, the extension cannot perform automatic retry with suffix incrementation (e.g., slug-2, slug-3) due to Prisma query hook limitations. The extension relies on database-level unique constraints.

Recommendation: Define @unique constraint in your Prisma schema and let the database handle uniqueness. If a duplicate slug is generated, Prisma will throw a unique constraint violation error that you can handle in your application code.

Handling Unique Constraint Violations

When using mode: 'random' with a short maxLength, collisions may occur. Here's how to handle them:

import { Prisma } from '@prisma/client';

async function createPostWithRetry(data: { title: string }, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await prisma.post.create({ data });
    } catch (error) {
      // Check if it's a unique constraint violation
      if (
        error instanceof Prisma.PrismaClientKnownRequestError &&
        error.code === 'P2002' &&
        attempt < maxRetries - 1
      ) {
        // Retry - the extension will generate a new random suffix
        continue;
      }
      // If it's not a unique violation or max retries reached, throw the error
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}

// Usage
const post = await createPostWithRetry({ title: 'Hello World' });

This approach works because each retry triggers a new slug generation with a different random suffix.

Nested Operations

The extension processes top-level create/update operations. Nested creates (e.g., create: { ... } within a relation) will also trigger slug generation.

TypeScript Support

The extension is fully typed and exports all necessary types:

import {
  slugExtension,
  SlugExtensionConfig,
  ModelSlugConfig,
  FieldSlugConfig,
  SlugGenerationError,
  SlugConfigurationError,
} from 'prisma-extension-slug';

Error Handling

import { SlugGenerationError, SlugConfigurationError } from 'prisma-extension-slug';

try {
  const post = await prisma.post.create({
    data: { title: '' },  // Empty title with required: true
  });
} catch (error) {
  if (error instanceof SlugConfigurationError) {
    console.error('Configuration error:', error.message);
  }
}

Development

# Install dependencies
npm install

# Build the extension
npm run build

# Run example
cd example
npm install
npx prisma migrate dev
npx tsx index.ts

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Related