On this page

Final project: Bookstore REST API

25 min read TextCh. 5 — Production

Course project: Bookstore REST API

You have reached the end of the NestJS Complete course. In this final lesson you will consolidate everything you have learned by reviewing the complete architecture of the Bookstore REST API and adding the remaining features to make it production-ready.

What you have built

Over the previous 15 lessons, you built a NestJS application with:

Lesson Feature
1-2 Project scaffold, module system
3-4 Controllers, services, dependency injection
5-6 Validation, guards, JWT auth
7-9 TypeORM entities, relations, repositories
10 JWT authentication with refresh tokens
11 Interceptors, middleware, exception filters
12 Real-time notifications via WebSockets
13 Environment-based configuration
14 Swagger/OpenAPI documentation
15 Unit and integration testing

Complete project structure

bookstore-api/
  src/
    auth/
      dto/
        login.dto.ts
        register.dto.ts
        refresh-token.dto.ts
      decorators/
        current-user.decorator.ts
        public.decorator.ts
        roles.decorator.ts
      guards/
        auth.guard.ts
        roles.guard.ts
        ws-auth.guard.ts
      strategies/
        jwt.strategy.ts
      user.entity.ts
      auth.controller.ts
      auth.module.ts
      auth.service.ts
    books/
      dto/
        create-book.dto.ts
        update-book.dto.ts
        book-response.dto.ts
        paginated-books.dto.ts
      book.entity.ts
      books.controller.ts
      books.controller.spec.ts
      books.module.ts
      books.service.ts
      books.service.spec.ts
    authors/
      author.entity.ts
      authors.controller.ts
      authors.module.ts
      authors.service.ts
    categories/
      category.entity.ts
      categories.controller.ts
      categories.module.ts
      categories.service.ts
    orders/
      dto/
        create-order.dto.ts
      entities/
        order.entity.ts
        order-item.entity.ts
      orders.controller.ts
      orders.module.ts
      orders.service.ts
    notifications/
      notifications.gateway.ts
      notifications.module.ts
    shared/
      filters/
        http-exception.filter.ts
      interceptors/
        logging.interceptor.ts
        transform.interceptor.ts
      shared.module.ts
    config/
      app.config.ts
      database.config.ts
      jwt.config.ts
    migrations/
      001-CreateAuthorsTable.ts
      002-CreateBooksTable.ts
      003-CreateCategoriesTable.ts
      004-CreateBookCategoriesJunction.ts
      005-CreateUsersTable.ts
      006-CreateOrdersTable.ts
    app.module.ts
    main.ts
  test/
    books.e2e-spec.ts
    auth.e2e-spec.ts
  datasource.ts
  nest-cli.json
  .env.development
  .env.production

The AppModule

The root module wires everything together:

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [appConfig, databaseConfig, jwtConfig],
      validationSchema: envValidationSchema,
    }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.getOrThrow('database.host'),
        port: config.getOrThrow('database.port'),
        username: config.getOrThrow('database.username'),
        password: config.getOrThrow('database.password'),
        database: config.getOrThrow('database.name'),
        autoLoadEntities: true,
        synchronize: false,
        migrationsRun: true,
        migrations: [__dirname + '/migrations/*.js'],
      }),
    }),
    ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),
    EventEmitterModule.forRoot({ wildcard: true }),
    BooksModule,
    AuthorsModule,
    CategoriesModule,
    AuthModule,
    OrdersModule,
    NotificationsModule,
    SharedModule,
  ],
})
export class AppModule {}

Global providers in main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const config = app.get(ConfigService);

  app.setGlobalPrefix(config.get('app.apiPrefix', 'api'));

  app.enableCors({
    origin: config.get<string>('app.corsOrigin', '*'),
    credentials: true,
  });

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      transformOptions: { enableImplicitConversion: true },
    }),
  );

  app.useGlobalInterceptors(
    new ClassSerializerInterceptor(app.get(Reflector)),
    new LoggingInterceptor(),
    new TransformInterceptor(),
  );

  app.useGlobalFilters(new AllExceptionsFilter());

  // Swagger only in non-production
  if (config.get('app.env') !== 'production') {
    const swaggerConfig = new DocumentBuilder()
      .setTitle('Bookstore API')
      .setVersion('1.0')
      .addBearerAuth()
      .build();
    const document = SwaggerModule.createDocument(app, swaggerConfig);
    SwaggerModule.setup('api/docs', app, document, {
      swaggerOptions: { persistAuthorization: true },
    });
  }

  // Enable graceful shutdown hooks
  app.enableShutdownHooks();

  const port = config.get<number>('app.port', 3000);
  await app.listen(port);
  console.log(`Bookstore API running on port ${port}`);
}

API endpoints summary

Authentication:
  POST   /api/auth/register       → Register new user
  POST   /api/auth/login          → Login (returns JWT)
  POST   /api/auth/refresh        → Refresh access token
  POST   /api/auth/logout         → Revoke refresh token
  GET    /api/auth/me             → Get current user profile

Books:
  GET    /api/books               → List with pagination
  GET    /api/books/search        → Full-text search
  GET    /api/books/:id           → Get by ID
  POST   /api/books               → Create (admin/author)
  PATCH  /api/books/:id           → Update (admin/author)
  DELETE /api/books/:id           → Soft delete (admin)

Authors:
  GET    /api/authors             → List all authors
  GET    /api/authors/:id         → Get author with books
  POST   /api/authors             → Create (admin)
  PATCH  /api/authors/:id         → Update (admin)

Categories:
  GET    /api/categories          → List all categories
  POST   /api/categories          → Create (admin)

Orders:
  GET    /api/orders              → My orders
  POST   /api/orders              → Place an order
  GET    /api/orders/:id          → Order details

Health:
  GET    /api/health              → Health check (DB + memory)

What to build next

Now that you have a solid NestJS foundation, here are the natural next steps for the bookstore project:

File uploads — Use @nestjs/platform-express with Multer to upload book cover images, store them on AWS S3 or Cloudinary.

Caching — Add a Redis cache layer with @nestjs/cache-manager for frequently accessed endpoints like book listings.

Background jobs — Use @nestjs/bull with Redis for processing tasks asynchronously: sending order confirmation emails, resizing uploaded images, generating sales reports.

Full-text search — Integrate PostgreSQL's tsvector or Elasticsearch for more powerful book search capabilities.

Microservices — Split the monolith into microservices using NestJS's @nestjs/microservices package with TCP or RabbitMQ transport.

GraphQL — Add a GraphQL layer alongside the REST API using @nestjs/graphql and apollo-server.

Congratulations on completing the NestJS Complete course! You now have the skills to build production-grade backend APIs with TypeScript, TypeORM, authentication, real-time features, documentation, and a comprehensive test suite.

Project structure for the full bookstore
The complete bookstore project has 6 modules: BooksModule, AuthorsModule, CategoriesModule, AuthModule, OrdersModule, and SharedModule. Each follows the same pattern shown in this lesson. The AppModule imports all of them and configures TypeORM, ConfigModule, and global providers.
What to add next
After completing this project, extend it with: file uploads for book covers (Multer + S3), full-text search (PostgreSQL tsvector or Elasticsearch), a caching layer (Redis + CacheModule), background jobs (Bull queue for email sending), and a health check endpoint (/api/health) using @nestjs/terminus.
typescript
// ============================================================
// BOOKSTORE REST API — NestJS 11 + TypeORM + JWT + Swagger
// This file shows the complete structure and key integrations
// of the final bookstore project built throughout the course.
// ============================================================

// ---------- ENTITIES ----------

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToOne,
  ManyToMany,
  JoinTable,
  JoinColumn,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany,
} from 'typeorm';

@Entity('authors')
class Author {
  @PrimaryGeneratedColumn() id: number;
  @Column() firstName: string;
  @Column() lastName: string;
  @Column({ unique: true }) email: string;
  @OneToMany(() => Book, (b) => b.author) books: Book[];
  @CreateDateColumn() createdAt: Date;
}

@Entity('categories')
class Category {
  @PrimaryGeneratedColumn() id: number;
  @Column({ unique: true }) name: string;
  @Column({ unique: true }) slug: string;
  @ManyToMany(() => Book, (b) => b.categories) books: Book[];
}

@Entity('books')
class Book {
  @PrimaryGeneratedColumn() id: number;
  @Column({ length: 200 }) title: string;
  @Column({ unique: true, length: 13 }) isbn: string;
  @Column('decimal', { precision: 8, scale: 2 }) price: number;
  @Column({ default: true }) isAvailable: boolean;
  @Column({ nullable: true }) coverUrl: string | null;

  @ManyToOne(() => Author, (a) => a.books, { nullable: false })
  @JoinColumn({ name: 'author_id' })
  author: Author;

  @Column({ name: 'author_id' }) authorId: number;

  @ManyToMany(() => Category, (c) => c.books, { cascade: ['insert'] })
  @JoinTable({ name: 'book_categories' })
  categories: Category[];

  @CreateDateColumn() createdAt: Date;
  @UpdateDateColumn() updatedAt: Date;
}

// ---------- DTOs ----------

import { IsString, IsNotEmpty, IsNumber, IsPositive, IsISBN, IsOptional, IsArray } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

class CreateBookDto {
  @ApiProperty({ example: 'Clean Code' })
  @IsString() @IsNotEmpty() title: string;

  @ApiProperty({ example: '9780132350884' })
  @IsISBN() isbn: string;

  @ApiProperty({ example: 39.99 })
  @IsNumber() @IsPositive() price: number;

  @ApiProperty({ example: 1 })
  @IsNumber() @IsPositive() authorId: number;

  @ApiPropertyOptional({ example: [1, 2] })
  @IsArray() @IsOptional() categoryIds?: number[];
}

// ---------- SERVICE ----------

import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, ILike } from 'typeorm';

@Injectable()
class BooksService {
  constructor(
    @InjectRepository(Book)
    private readonly booksRepo: Repository<Book>,
  ) {}

  async findAll(page = 1, limit = 10): Promise<{ data: Book[]; total: number; pages: number }> {
    const [data, total] = await this.booksRepo.findAndCount({
      relations: { author: true, categories: true },
      order: { createdAt: 'DESC' },
      skip: (page - 1) * limit,
      take: limit,
    });
    return { data, total, pages: Math.ceil(total / limit) };
  }

  async search(q: string): Promise<Book[]> {
    return this.booksRepo.find({
      where: [
        { title: ILike(`%${q}%`) },
        { author: { lastName: ILike(`%${q}%`) } },
      ],
      relations: { author: true },
      take: 20,
    });
  }

  async findOne(id: number): Promise<Book> {
    const book = await this.booksRepo.findOne({
      where: { id },
      relations: { author: true, categories: true },
    });
    if (!book) throw new NotFoundException(`Book #${id} not found`);
    return book;
  }

  async create(dto: CreateBookDto): Promise<Book> {
    const exists = await this.booksRepo.findOneBy({ isbn: dto.isbn });
    if (exists) throw new ConflictException(`ISBN ${dto.isbn} already registered`);
    const book = this.booksRepo.create(dto);
    return this.booksRepo.save(book);
  }

  async remove(id: number): Promise<void> {
    const book = await this.findOne(id);
    await this.booksRepo.softDelete(book.id);
  }
}

// ---------- CONTROLLER ----------

import {
  Controller,
  Get,
  Post,
  Delete,
  Body,
  Param,
  Query,
  ParseIntPipe,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';

@ApiTags('books')
@ApiBearerAuth('access-token')
@Controller('books')
class BooksController {
  constructor(private readonly booksService: BooksService) {}

  @Get()
  @ApiOperation({ summary: 'List all books' })
  findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
    return this.booksService.findAll(+page, +limit);
  }

  @Get('search')
  @ApiOperation({ summary: 'Search books by title or author' })
  search(@Query('q') q: string) {
    return this.booksService.search(q);
  }

  @Get(':id')
  @ApiOperation({ summary: 'Get book by ID' })
  @ApiResponse({ status: 404, description: 'Book not found' })
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.booksService.findOne(id);
  }

  @Post()
  @ApiOperation({ summary: 'Create a new book' })
  @ApiResponse({ status: 409, description: 'ISBN already exists' })
  create(@Body() dto: CreateBookDto) {
    return this.booksService.create(dto);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  @ApiOperation({ summary: 'Soft-delete a book' })
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.booksService.remove(id);
  }
}

// ---------- MODULE ----------

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([Book, Author, Category])],
  controllers: [BooksController],
  providers: [BooksService],
  exports: [BooksService],
})
class BooksModule {}

// ---------- BOOTSTRAP ----------

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(BooksModule); // Use AppModule in real project

  app.setGlobalPrefix('api');
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

  const swaggerConfig = new DocumentBuilder()
    .setTitle('Bookstore API')
    .setVersion('1.0')
    .addBearerAuth()
    .build();

  const document = SwaggerModule.createDocument(app, swaggerConfig);
  SwaggerModule.setup('api/docs', app, document);

  await app.listen(3000);
  console.log('Bookstore API running at http://localhost:3000/api');
  console.log('Swagger docs at http://localhost:3000/api/docs');
}

bootstrap();