Skip to content

Blog_api

Building a Complete Blog API with TypeORM and NestJS

Section titled “Building a Complete Blog API with TypeORM and NestJS”

This project demonstrates a complete blog API with users, posts, comments, and tags.

Blog API Architecture
================================================================================
+-------------------+ +-------------------+ +-------------------+
| Users | | Posts | | Comments |
+-------------------+ +-------------------+ +-------------------+
| id | | id | | id |
| name |<--->| authorId |<--->| postId |
| email | | title | | authorId |
| password | | content | | content |
| role | | status | | createdAt |
| createdAt | | viewCount | +-------------------+
+-------------------+ | createdAt |
+-------------------+
|
v
+-------------------+
| Tags |
+-------------------+
| id |
| name |
+-------------------+
^
|
+-------------------+
| post_tags |
+-------------------+
| postId |
| tagId |
+-------------------+
================================================================================

blog-api/
|-- src/
| |-- main.ts
| |-- app.module.ts
| |-- users/
| | |-- user.entity.ts
| | |-- user.module.ts
| | |-- user.service.ts
| | |-- user.controller.ts
| | |-- dto/
| | |-- create-user.dto.ts
| | |-- update-user.dto.ts
| |-- posts/
| | |-- post.entity.ts
| | |-- post.module.ts
| | |-- post.service.ts
| | |-- post.controller.ts
| | |-- dto/
| |-- comments/
| | |-- comment.entity.ts
| | |-- comment.module.ts
| | |-- comment.service.ts
| | |-- comment.controller.ts
| |-- tags/
| | |-- tag.entity.ts
| | |-- tag.module.ts
| | |-- tag.service.ts
| |-- auth/
| | |-- auth.module.ts
| | |-- auth.service.ts
| | |-- auth.controller.ts
| | |-- guards/
| | |-- strategies/
| |-- common/
| |-- filters/
| |-- interceptors/
| |-- decorators/
|-- test/
|-- migrations/
|-- package.json
|-- tsconfig.json
|-- .env

src/users/user.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
import { Post } from '../posts/post.entity';
import { Comment } from '../comments/comment.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 100 })
@Index()
name: string;
@Column({ unique: true })
@Index()
email: string;
@Column({ select: false })
password: string;
@Column({ default: 'user' })
role: 'user' | 'admin' | 'moderator';
@Column({ default: true })
isActive: boolean;
@Column({ type: 'text', nullable: true })
bio: string;
@Column({ nullable: true })
avatar: string;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@OneToMany(() => Comment, (comment) => comment.author)
comments: Comment[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
src/posts/post.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
OneToMany,
ManyToMany,
JoinTable,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
import { User } from '../users/user.entity';
import { Comment } from '../comments/comment.entity';
import { Tag } from '../tags/tag.entity';
export enum PostStatus {
DRAFT = 'draft',
PUBLISHED = 'published',
ARCHIVED = 'archived',
}
@Entity('posts')
@Index(['status', 'createdAt'])
@Index(['authorId', 'status'])
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 200 })
title: string;
@Column({ unique: true, length: 250 })
slug: string;
@Column({ type: 'text' })
content: string;
@Column({ type: 'text', nullable: true })
excerpt: string;
@Column({ nullable: true })
featuredImage: string;
@Column({ type: 'enum', enum: PostStatus, default: PostStatus.DRAFT })
status: PostStatus;
@Column({ default: 0 })
viewCount: number;
@Column({ default: 0 })
likeCount: number;
@Column({ default: true })
allowComments: boolean;
@Column()
authorId: number;
@ManyToOne(() => User, (user) => user.posts)
author: User;
@OneToMany(() => Comment, (comment) => comment.post)
comments: Comment[];
@ManyToMany(() => Tag, (tag) => tag.posts, { eager: true })
@JoinTable({ name: 'post_tags' })
tags: Tag[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
publishedAt: Date;
}
src/comments/comment.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
CreateDateColumn,
Index,
} from 'typeorm';
import { User } from '../users/user.entity';
import { Post } from '../posts/post.entity';
@Entity('comments')
@Index(['postId', 'createdAt'])
export class Comment {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'text' })
content: string;
@Column({ default: false })
isApproved: boolean;
@Column()
postId: number;
@Column()
authorId: number;
@ManyToOne(() => Post, (post) => post.comments, { onDelete: 'CASCADE' })
post: Post;
@ManyToOne(() => User, (user) => user.comments)
author: User;
@CreateDateColumn()
createdAt: Date;
}
src/tags/tag.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToMany,
CreateDateColumn,
Index,
} from 'typeorm';
import { Post } from '../posts/post.entity';
@Entity('tags')
@Index(['name'])
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true, length: 50 })
name: string;
@Column({ unique: true, length: 60 })
slug: string;
@ManyToMany(() => Post, (post) => post.tags)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
}

src/posts/post.service.ts
import {
Injectable,
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, SelectQueryBuilder } from 'typeorm';
import { Post, PostStatus } from './post.entity';
import { Tag } from '../tags/tag.entity';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostQueryDto } from './dto/post-query.dto';
@Injectable()
export class PostService {
constructor(
@InjectRepository(Post)
private postRepository: Repository<Post>,
@InjectRepository(Tag)
private tagRepository: Repository<Tag>,
private dataSource: DataSource,
) {}
async create(userId: number, createPostDto: CreatePostDto): Promise<Post> {
return this.dataSource.transaction(async (manager) => {
// Generate slug
const slug = this.generateSlug(createPostDto.title);
// Handle tags
const tags = await this.processTags(createPostDto.tags || [], manager);
// Create post
const post = manager.create(Post, {
...createPostDto,
slug,
authorId: userId,
tags,
status: createPostDto.status || PostStatus.DRAFT,
});
return manager.save(post);
});
}
async findAll(query: PostQueryDto) {
const qb = this.postRepository
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.tags', 'tags');
// Apply filters
this.applyFilters(qb, query);
// Apply pagination
const { page = 1, limit = 10 } = query;
const skip = (page - 1) * limit;
qb.skip(skip).take(limit);
// Apply sorting
qb.orderBy('post.createdAt', 'DESC');
const [data, total] = await qb.getManyAndCount();
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
async findOne(id: number): Promise<Post> {
const post = await this.postRepository.findOne({
where: { id },
relations: ['author', 'tags', 'comments', 'comments.author'],
});
if (!post) {
throw new NotFoundException('Post not found');
}
return post;
}
async findBySlug(slug: string): Promise<Post> {
const post = await this.postRepository
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.tags', 'tags')
.leftJoinAndSelect('post.comments', 'comments')
.leftJoinAndSelect('comments.author', 'commentAuthor')
.where('post.slug = :slug', { slug })
.andWhere('post.status = :status', { status: PostStatus.PUBLISHED })
.getOne();
if (!post) {
throw new NotFoundException('Post not found');
}
// Increment view count
await this.postRepository.increment({ id: post.id }, 'viewCount', 1);
return post;
}
async update(
id: number,
userId: number,
updatePostDto: UpdatePostDto,
): Promise<Post> {
return this.dataSource.transaction(async (manager) => {
const post = await manager.findOne(Post, {
where: { id },
relations: ['tags'],
});
if (!post) {
throw new NotFoundException('Post not found');
}
if (post.authorId !== userId) {
throw new ForbiddenException('You can only edit your own posts');
}
// Update slug if title changed
if (updatePostDto.title && updatePostDto.title !== post.title) {
updatePostDto['slug'] = this.generateSlug(updatePostDto.title);
}
// Handle tags
if (updatePostDto.tags) {
post.tags = await this.processTags(updatePostDto.tags, manager);
}
Object.assign(post, updatePostDto);
return manager.save(post);
});
}
async remove(id: number, userId: number): Promise<void> {
const post = await this.findOne(id);
if (post.authorId !== userId) {
throw new ForbiddenException('You can only delete your own posts');
}
await this.postRepository.remove(post);
}
private applyFilters(qb: SelectQueryBuilder<Post>, query: PostQueryDto) {
if (query.status) {
qb.andWhere('post.status = :status', { status: query.status });
}
if (query.authorId) {
qb.andWhere('post.authorId = :authorId', { authorId: query.authorId });
}
if (query.search) {
qb.andWhere(
'(post.title ILIKE :search OR post.content ILIKE :search)',
{ search: `%${query.search}%` },
);
}
if (query.tag) {
qb.andWhere('tags.slug = :tag', { tag: query.tag });
}
}
private generateSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
private async processTags(
tagNames: string[],
manager: any,
): Promise<Tag[]> {
const tags: Tag[] = [];
for (const name of tagNames) {
let tag = await manager.findOne(Tag, { where: { name } });
if (!tag) {
tag = manager.create(Tag, {
name,
slug: this.generateSlug(name),
});
await manager.save(tag);
}
tags.push(tag);
}
return tags;
}
}

src/posts/post.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
ParseIntPipe,
UseGuards,
Request,
} from '@nestjs/common';
import { PostService } from './post.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostQueryDto } from './dto/post-query.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { Post as PostEntity } from './post.entity';
@Controller('posts')
export class PostController {
constructor(private readonly postService: PostService) {}
@Get()
async findAll(@Query() query: PostQueryDto) {
return this.postService.findAll(query);
}
@Get('slug/:slug')
async findBySlug(@Param('slug') slug: string): Promise<PostEntity> {
return this.postService.findBySlug(slug);
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number): Promise<PostEntity> {
return this.postService.findOne(id);
}
@Post()
@UseGuards(JwtAuthGuard)
async create(
@Request() req: any,
@Body() createPostDto: CreatePostDto,
): Promise<PostEntity> {
return this.postService.create(req.user.id, createPostDto);
}
@Put(':id')
@UseGuards(JwtAuthGuard)
async update(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
@Body() updatePostDto: UpdatePostDto,
): Promise<PostEntity> {
return this.postService.update(id, req.user.id, updatePostDto);
}
@Delete(':id')
@UseGuards(JwtAuthGuard)
async remove(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
): Promise<void> {
return this.postService.remove(id, req.user.id);
}
}

src/posts/post.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostController } from './post.controller';
import { PostService } from './post.service';
import { Post } from './post.entity';
import { Tag } from '../tags/tag.entity';
@Module({
imports: [TypeOrmModule.forFeature([Post, Tag])],
controllers: [PostController],
providers: [PostService],
exports: [PostService],
})
export class PostModule {}
src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UserModule } from './users/user.module';
import { PostModule } from './posts/post.module';
import { CommentModule } from './comments/comment.module';
import { TagModule } from './tags/tag.module';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DB_HOST', 'localhost'),
port: configService.get('DB_PORT', 5432),
username: configService.get('DB_USERNAME', 'postgres'),
password: configService.get('DB_PASSWORD', 'postgres'),
database: configService.get('DB_DATABASE', 'blog'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: configService.get('NODE_ENV') !== 'production',
logging: configService.get('DB_LOGGING') === 'true',
}),
inject: [ConfigService],
}),
UserModule,
PostModule,
CommentModule,
TagModule,
AuthModule,
],
})
export class AppModule {}

This Blog API project demonstrates:

  • Entity Relationships: One-to-Many (User-Posts), Many-to-Many (Posts-Tags)
  • Authentication: JWT-based authentication with guards
  • CRUD Operations: Full create, read, update, delete functionality
  • Query Optimization: Indexes, eager loading, query builders
  • DTOs & Validation: Input validation with class-validator
  • Error Handling: Proper exception handling

Chapter 47: E-Commerce API Project


Last Updated: February 2026