Skip to content

Tags Domain

The tags domain provides flexible resource tagging and categorization for TypeScript backends. Tag any resource type without coupling, using a generic junction model. It is structured in strict Clean Architecture layers with zero npm dependencies in the domain and application layers.

Terminal window
npx @backcap/cli add tags

The Tag entity is the aggregate root. It auto-generates a slug from the tag name.

import { Tag } from "./domains/tags/domain/entities/tag.entity";
const result = Tag.create({ id: crypto.randomUUID(), name: "Web Development" });
if (result.isOk()) {
const tag = result.unwrap();
console.log(tag.slug.value); // "web-development"
}
FieldTypeDescription
idstringUnique identifier (UUID)
namestringHuman-readable tag name
slugTagSlugAuto-generated kebab-case slug
createdAtDateTimestamp of creation
import { TagSlug } from "./domains/tags/domain/value-objects/tag-slug.vo";
// Create from explicit slug
const result = TagSlug.create("web-development");
// Auto-generate from name
const generated = TagSlug.fromName("Web Development");
// "web-development"

Validates: lowercase kebab-case, 1–64 characters, no leading or trailing hyphens. fromName() auto-generates a slug from any string.

Error ClassConditionMessage
TagNotFoundNo tag found for the given slugTag not found with slug: "<slug>"
TagAlreadyExistsA tag with the same slug already existsTag already exists with slug: "<slug>"
ResourceTagNotFoundResource is not taggedResource tag not found: tag "<slug>" is not associated with...
ResourceAlreadyTaggedResource is already taggedResource already tagged: tag "<slug>" is already associated with...
InvalidTagSlugSlug fails validationInvalid tag slug: "<slug>"
EventEmitted ByPayload
TagCreatedCreateTag use casetagId, slug, occurredAt

Registers a new tag with auto-generated slug. Fails if a tag with the same slug already exists.

const result = await tagsService.createTag({ name: "TypeScript" });
// Result<{ tagId: string; slug: string; createdAt: Date }, Error>

Associates a tag with any resource. Fails if the resource is already tagged.

const result = await tagsService.tagResource({
tagSlug: "typescript",
resourceId: "post-123",
resourceType: "post",
});
// Result<{ taggedAt: Date }, Error>

Removes a tag association. Fails if the association doesn’t exist.

const result = await tagsService.untagResource({
tagSlug: "typescript",
resourceId: "post-123",
resourceType: "post",
});
// Result<{ untaggedAt: Date }, Error>

Finds resources tagged with a specific tag.

const result = await tagsService.listByTag({
tagSlug: "typescript",
resourceType: "post", // optional filter
limit: 50,
offset: 0,
});
// Result<{ resources: Array<{ resourceId, resourceType, taggedAt }>; total: number }, Error>
export interface ITagRepository {
saveTag(tag: Tag): Promise<void>;
findBySlug(slug: string): Promise<Tag | undefined>;
tagResource(tagId: string, resourceId: string, resourceType: string): Promise<void>;
untagResource(tagId: string, resourceId: string, resourceType: string): Promise<void>;
findResourcesByTag(tagId: string, filters: TagResourceFilters): Promise<{ resources: Array<...>; total: number }>;
isResourceTagged(tagId: string, resourceId: string, resourceType: string): Promise<boolean>;
}
import { createTagsService, ITagsService } from "./domains/tags/contracts";
const tagsService: ITagsService = createTagsService({ tagRepository });
domains/tags/
domain/
entities/tag.entity.ts
value-objects/tag-slug.vo.ts
events/tag-created.event.ts
errors/tag-not-found.error.ts
errors/resource-tag-not-found.error.ts
errors/invalid-tag-slug.error.ts
errors/tag-already-exists.error.ts
errors/resource-already-tagged.error.ts
application/
use-cases/create-tag.use-case.ts
use-cases/tag-resource.use-case.ts
use-cases/untag-resource.use-case.ts
use-cases/list-by-tag.use-case.ts
ports/tag-repository.port.ts
dto/create-tag.dto.ts
dto/tag-resource.dto.ts
dto/untag-resource.dto.ts
dto/list-by-tag.dto.ts
contracts/
tags.contract.ts
tags.factory.ts
index.ts
shared/
result.ts