Auto-generate URL slugs for NestJS with TypeORM — unique slugs, collision handling, and transliteration.
This package provides automatic URL slug generation for NestJS with TypeORM that generates unique, URL-friendly slugs from entity fields with collision handling and built-in transliteration for Arabic, Cyrillic, and accented characters.
Once installed, using it is as simple as:
@Sluggable({ from: "title" })
@Entity()
class Post {
@Column() title: string;
@Column() slug: string; // Auto-generated: "my-awesome-post"
}- Installation
- Quick Start
- Module Configuration
- Using the Decorator
- Using the Mixin
- Using the Service Directly
- Collision Handling
- Transliteration
- Update Behavior
- Multiple Source Fields
- Events
- Configuration Options
- Standalone Usage
- Testing
- Changelog
- Contributing
- Security
- Credits
- License
Install the package via npm:
npm install @nestbolt/sluggableOr via yarn:
yarn add @nestbolt/sluggableOr via pnpm:
pnpm add @nestbolt/sluggableThis package requires the following peer dependencies:
@nestjs/common ^10.0.0 || ^11.0.0
@nestjs/core ^10.0.0 || ^11.0.0
typeorm ^0.3.0
reflect-metadata ^0.1.13 || ^0.2.0
Optional:
@nestjs/event-emitter ^2.0.0 || ^3.0.0
- Register the module in your
AppModule:
import { SluggableModule } from "@nestbolt/sluggable";
@Module({
imports: [
TypeOrmModule.forRoot({
/* ... */
}),
SluggableModule.forRoot(),
],
})
export class AppModule {}- Add the decorator to your entity:
import { Sluggable } from "@nestbolt/sluggable";
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Sluggable({ from: "title" })
@Entity("posts")
export class Post {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column()
title: string;
@Column()
slug: string; // Auto-generated on insert
}- Save an entity and the slug is generated automatically:
const post = postRepo.create({ title: "My Awesome Post" });
await postRepo.save(post);
console.log(post.slug); // "my-awesome-post"SluggableModule.forRoot({
separator: "-", // Word separator (default: '-')
maxLength: 255, // Max slug length (default: 255)
lowercase: true, // Lowercase slugs (default: true)
transliterate: true, // Enable transliteration (default: true)
onUpdate: "keep", // 'keep' or 'regenerate' (default: 'keep')
suffixSeparator: "-", // Collision suffix separator (default: '-')
});SluggableModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
maxLength: config.get("SLUG_MAX_LENGTH", 100),
onUpdate: config.get("SLUG_ON_UPDATE", "keep"),
}),
});The module is registered as global — SluggableService is available everywhere without re-importing.
The @Sluggable() class decorator configures slug generation for an entity:
@Sluggable({
from: 'title', // Source field(s) — required
slugField: 'slug', // Target field (default: 'slug')
separator: '-', // Word separator override
maxLength: 100, // Max length override
onUpdate: 'keep', // 'keep' or 'regenerate'
unique: true, // Collision handling (default: true)
})The SluggableMixin() adds instance methods to your entity:
import { Sluggable, SluggableMixin } from "@nestbolt/sluggable";
import { BaseEntity } from "typeorm";
@Sluggable({ from: "title" })
@Entity()
class Post extends SluggableMixin(BaseEntity) {
/* ... */
}| Method | Returns | Description |
|---|---|---|
getSlug() |
string |
Get current slug value |
getSlugField() |
string |
Get slug column name |
findBySlug(slug) |
Promise<any | null> |
Find entity by slug |
regenerateSlug() |
Promise<string> |
Regenerate and return new slug |
Inject SluggableService for programmatic control:
| Method | Returns | Description |
|---|---|---|
generateSlug(input, overrides?) |
string |
Generate slug from text |
generateUniqueSlug(Entity, field, base, excludeId?) |
Promise<string> |
Generate unique slug with DB check |
findBySlug<T>(Entity, field, slug) |
Promise<T | null> |
Find entity by slug |
regenerateSlug(entity, fields, slugField, overrides?) |
Promise<string> |
Regenerate for existing entity |
When unique: true (default), the package queries the database for existing slugs and appends a numeric suffix:
my-post (first)
my-post-1 (second with same title)
my-post-2 (third with same title)
The suffix separator can be customized via suffixSeparator in module options.
Built-in transliteration converts non-Latin characters to ASCII:
// Arabic
sluggableService.generateSlug("مرحبا بالعالم"); // "mrhba-balalm"
// Cyrillic
sluggableService.generateSlug("Привет мир"); // "privet-mir"
// Accented Latin
sluggableService.generateSlug("Cafe Resume"); // "cafe-resume"
// German
sluggableService.generateSlug("Uber Munchen"); // "ueber-muenchen"Provide your own transliteration function:
SluggableModule.forRoot({
transliterator: (input) => myCustomTransliterate(input),
});The slugify and transliterate functions are exported for standalone use:
import { slugify, transliterate } from "@nestbolt/sluggable";
const slug = slugify(transliterate("Cafe Resume")); // "cafe-resume"Control what happens to the slug when an entity is updated:
'keep'(default) — Keeps the original slug, even if the source field changes'regenerate'— Generates a new slug when source fields change
@Sluggable({ from: 'title', onUpdate: 'regenerate' })You can set the default behavior at the module level and override per entity.
Generate slugs from multiple fields:
@Sluggable({ from: ["firstName", "lastName"] })
@Entity()
class User {
@Column() firstName: string;
@Column() lastName: string;
@Column() slug: string; // "john-doe"
}When @nestjs/event-emitter is installed, the following events are emitted:
| Event | Payload |
|---|---|
sluggable.slug-generated |
{ entity, slug, sourceFields, sourceText } |
sluggable.slug-regenerated |
{ entity, oldSlug, newSlug, sourceFields } |
| Option | Type | Default | Description |
|---|---|---|---|
separator |
string |
'-' |
Word separator |
maxLength |
number |
255 |
Maximum slug length |
lowercase |
boolean |
true |
Lowercase slugs |
transliterate |
boolean |
true |
Enable transliteration |
transliterator |
Function |
built-in | Custom transliteration function |
onUpdate |
'keep' | 'regenerate' |
'keep' |
Slug update behavior |
suffixSeparator |
string |
'-' |
Collision suffix separator |
| Option | Type | Default | Description |
|---|---|---|---|
from |
string | string[] |
required | Source field(s) |
slugField |
string |
'slug' |
Target field |
separator |
string |
module default | Word separator override |
maxLength |
number |
module default | Max length override |
onUpdate |
'keep' | 'regenerate' |
module default | Update behavior override |
unique |
boolean |
true |
Enable collision handling |
Use slugify and transliterate without the NestJS module:
import { slugify, transliterate } from "@nestbolt/sluggable";
const slug = slugify("Hello World!"); // "hello-world"
const slug2 = slugify("Hello", { separator: "_" }); // "hello"
const latin = transliterate("Привет"); // "Privet"npm testRun tests in watch mode:
npm run test:watchGenerate coverage report:
npm run test:covPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.
The MIT License (MIT). Please see License File for more information.