Blog_api
Chapter 46: Blog API Project
Section titled “Chapter 46: Blog API Project”Building a Complete Blog API with TypeORM and NestJS
Section titled “Building a Complete Blog API with TypeORM and NestJS”46.1 Project Overview
Section titled “46.1 Project Overview”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 | +-------------------+
================================================================================46.2 Project Structure
Section titled “46.2 Project Structure”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|-- .env46.3 Entity Definitions
Section titled “46.3 Entity Definitions”User Entity
Section titled “User Entity”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;}Post Entity
Section titled “Post Entity”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;}Comment Entity
Section titled “Comment Entity”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;}Tag Entity
Section titled “Tag Entity”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;}46.4 Service Implementation
Section titled “46.4 Service Implementation”Posts Service
Section titled “Posts Service”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; }}46.5 Controller Implementation
Section titled “46.5 Controller Implementation”Posts Controller
Section titled “Posts Controller”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); }}46.6 Module Configuration
Section titled “46.6 Module Configuration”Posts Module
Section titled “Posts Module”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 {}App Module
Section titled “App Module”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 {}46.7 Summary
Section titled “46.7 Summary”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
Next Chapter
Section titled “Next Chapter”Chapter 47: E-Commerce API Project
Last Updated: February 2026