On this page
Final project: Bookstore REST API
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.productionThe 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.
// ============================================================
// 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();
Sign in to track your progress