Skip to content

Blog Domain

The blog domain provides blog post management for TypeScript backends. It handles post creation, publishing lifecycle, slug generation, and listing with filtering.

Terminal window
npx @backcap/cli add blog

The Post entity is the aggregate root. It is immutable — all state changes return new instances.

import { Post } from "./domains/blog/domain/entities/post.entity";
const result = Post.create({
id: crypto.randomUUID(),
title: "Getting Started with Backcap",
content: "Blog post content here...",
authorId: "user-123",
});
if (result.isOk()) {
const post = result.unwrap();
console.log(post.slug.value); // "getting-started-with-backcap"
}
FieldTypeDescription
idstringUnique identifier (UUID)
titlestringPost title
slugSlugURL-safe kebab-case identifier (value object)
contentstringPost body content
authorIdstringReference to the author’s user ID
status"draft" | "published"Lifecycle state, defaults to "draft"
createdAtDateCreation timestamp
publishedAtDate | nullSet when the post is published

Post.create() returns Result<Post, InvalidSlug>. If no slug is provided, one is auto-generated from the title via Slug.fromTitle().

const publishResult = post.publish();
if (publishResult.isOk()) {
const { post: published, event } = publishResult.unwrap();
console.log(published.status); // "published"
console.log(event); // PostPublished { postId, slug, publishedAt }
}

Returns Result<{ post: Post; event: PostPublished }, PostAlreadyPublished>. Fails if the post is already published.

import { Slug } from "./domains/blog/domain/value-objects/slug.vo";
const result = Slug.create("my-blog-post");
// Result<Slug, InvalidSlug>
const fromTitle = Slug.fromTitle("Hello World! My Post");
// Result<Slug, InvalidSlug> — produces "hello-world-my-post"

Validates against /^[a-z0-9]+(?:-[a-z0-9]+)*$/ — lowercase alphanumeric segments joined by single hyphens.

Error ClassConditionMessage
InvalidSlugSlug fails format validationInvalid slug: "<value>". Slug must be lowercase kebab-case.
PostNotFoundNo post found for the given IDPost not found with id: "<id>"
PostAlreadyPublishedAttempt to publish an already-published postPost with id "<id>" is already published.
EventEmitted ByPayload
PostCreatedCreatePost use casepostId, authorId, occurredAt
PostPublishedPost.publish() methodpostId, slug, publishedAt, occurredAt

Creates a draft post with an auto-generated or explicit slug.

import { CreatePost } from "./domains/blog/application/use-cases/create-post.use-case";
const createPost = new CreatePost(postRepository);
const result = await createPost.execute({
title: "My First Post",
content: "Hello world!",
authorId: "user-123",
slug: "my-first-post", // optional
});
// Result<{ output: { postId, slug }; event: PostCreated }, Error>

Transitions a draft post to published state.

const result = await publishPost.execute({ postId: "post-123" });
// Result<{ output: { postId, slug, publishedAt }; event: PostPublished }, Error>

Possible failures: PostNotFound, PostAlreadyPublished

Retrieves a post by ID with full details.

const result = await getPost.execute({ postId: "post-123" });
// Result<GetPostOutput, Error>

Returns paginated posts with optional filters.

const result = await listPosts.execute({
authorId: "user-123", // optional
status: "published", // optional
});
// Result<{ posts: ListPostsOutputItem[] }, Error>
export interface IPostRepository {
findById(id: string): Promise<Post | null>;
findBySlug(slug: string): Promise<Post | null>;
findAll(filter?: { authorId?: string; status?: "draft" | "published" }): Promise<Post[]>;
save(post: Post): Promise<void>;
}
import { createBlogService, IBlogService } from "./domains/blog/contracts";
const blogService: IBlogService = createBlogService({
postRepository,
eventBus, // optional — when provided, PostCreated and PostPublished events are published automatically
});
// IBlogService interface:
// createPost(input): Promise<Result<CreatePostOutput, Error>>
// publishPost(input): Promise<Result<PublishPostOutput, Error>>
// getPost(input): Promise<Result<GetPostOutput, Error>>
// listPosts(input): Promise<Result<ListPostsOutput, Error>>

The eventBus dependency is optional. When provided, the factory automatically publishes PostCreated and PostPublished domain events after successful operations. This enables event consumers to react to blog events without manual wiring.

domains/blog/
domain/
entities/post.entity.ts
value-objects/slug.vo.ts
events/post-created.event.ts
events/post-published.event.ts
errors/invalid-slug.error.ts
errors/post-not-found.error.ts
errors/post-already-published.error.ts
application/
use-cases/create-post.use-case.ts
use-cases/publish-post.use-case.ts
use-cases/get-post.use-case.ts
use-cases/list-posts.use-case.ts
ports/post-repository.port.ts
dto/create-post.dto.ts
dto/publish-post.dto.ts
dto/get-post.dto.ts
dto/list-posts.dto.ts
contracts/
blog.contract.ts
blog.factory.ts
index.ts
shared/
result.ts