Skip to content

Chapter 12 - Refactor your application using Domain-driven design (DDD)

Your application has grown quite a lot since you started your journey. You manage slideshows, database connection, application configuration with dotenv, an API, views, etc.

All these aspects are well defined in your head but the codebase doesn't exactly reflect those domains at the moment.

In this chapter, you'll refactor your application (= changing the code without adding new features) to use Domain-driven design to help structure your application with proven practices to extend it in the future.

This will allow to have distinct domains that you can extend or re-use in other projects, helping the understanding of the codebase and ensure the Separation of concerns.

In fact, in the previous chapter, you already created the Prisma domain to keep everything related to the database in one directory.

Steps

Create the slideshows domain

Create a new slideshows directory. This directory will contain all the files related to the slideshows domain.

In a terminal, execute the following command(s).
mkdir src/slideshows

Move the current files related to the slideshows domain to their own slideshows directory.

In a terminal, execute the following command(s).
1
2
3
4
5
# Move the enums into the slideshows domain
mv src/enums src/slideshows/enums

# Move the types into the slideshows domain
mv src/types src/slideshows/types

Move and rename the slideshows service

Move the current AppService to its own directory.

In a terminal, execute the following command(s).
# Move and rename the service into the slideshows domain
mv src/app.service.ts src/slideshows/slideshows.service.ts

Update the AppService to SlideshowsService.

src/slideshows/slideshows.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateSlideshow } from './types/create-slideshow.type';
import { UpdateSlideshow } from './types/update-slideshow.type';

@Injectable()
export class SlideshowsService {
	constructor(private readonly prisma: PrismaService) {}

	async getSlideshows() {
		return await this.prisma.slideshow.findMany({
			include: {
				slides: {
					select: {
						alt: true,
						interval: true,
						src: true,
						type: true,
					},
				},
			},
		});
	}

	async getSlideshow(slideshowId: string) {
		const slideshow = await this.prisma.slideshow.findFirst({
			where: {
				id: {
					equals: slideshowId,
				},
			},
			include: {
				slides: {
					select: {
						alt: true,
						interval: true,
						src: true,
						type: true,
					},
				},
			}
		});

		return slideshow;
	}

	async createSlideshow(createSlideshow: CreateSlideshow) {
		const newSlideshow = await this.prisma.slideshow.create({
			data: {
				slides: {
					create: createSlideshow.slides,
				},
			},
			include: {
				slides: {
					select: {
						alt: true,
						interval: true,
						src: true,
						type: true,
					},
				},
			},
		});

		return newSlideshow;
	}

	async updateSlideshow(slideshowId: string, updateSlideshow: UpdateSlideshow) {
		const updatedSlideshow = await this.prisma.slideshow.update({
			where: {
				id: slideshowId,
			},
			data: {
				slides: {
					deleteMany: {},
					create: updateSlideshow.slides
				},
			},
			include: {
				slides: {
					select: {
						alt: true,
						interval: true,
						src: true,
						type: true,
					},
				},
			},
		})

		return updatedSlideshow;
	}

	async deleteSlideshow(slideshowId: string) {
		await this.prisma.slideshow.delete({
			where: {
				id: slideshowId,
			},
		});
	}
}

Move the Handlebars templates to the slideshows domain

Create the views directory for the slideshows domain and move the current Handlebars templates to it. The Handlebars template to display one slideshow is renamed [id].hbs now that it is moved to its own domain.

In a terminal, execute the following command(s).
1
2
3
4
5
6
7
8
# Create the `slideshows` views directory
mkdir views/slideshows

# Move the Handlebars template to display all the slideshows
mv views/index.hbs views/slideshows/index.hbs

# Move and rename the Handlebars template to display one slideshow
mv views/slideshow.hbs views/slideshows/[id].hbs

Split the current controller into an API and a Views controllers

The current AppController located in src/app.controller.ts has two aspects that seems too coupled and hard to maintain: the API endpoints and the Views rendering. You'll split these two aspects into two separate files.

src/slideshows/slideshows-api.controller.ts
import {
		Get,
		Controller,
		Param,
		Post,
		Body,
		Patch,
		Delete,
		NotFoundException,
		HttpCode,
} from '@nestjs/common';
import { SlideshowsService } from './slideshows.service';
import { CreateSlideshow } from './types/create-slideshow.type';
import { UpdateSlideshow } from './types/update-slideshow.type';

@Controller('api/slideshows') // (1)!
export class SlideshowsApiController {
		constructor(private readonly slideshowsService: SlideshowsService) { }

		@Get() // (2)!
		async getSlideshowsApi() {
				const slideshows = await this.slideshowsService.getSlideshows();

				return slideshows;
		}

		@Get('/:id') // (3)!
		async getSlideshowApi(@Param('id') id: string) {
				const slideshow = await this.slideshowsService.getSlideshow(id);

				if (!slideshow) {
						throw new NotFoundException();
				}

				return slideshow;
		}

		@Post() // (4)!
		async createSlideshowApi(@Body() createSlideshow: CreateSlideshow) {
				const newSlideshow = await this.slideshowsService.createSlideshow(createSlideshow);

				return newSlideshow;
		}

		@Patch('/:id') // (5)!
		async updateSlideshowApi(@Param('id') id: string, @Body() updateSlideshow: UpdateSlideshow) {
				try {
						const updatedSlideshow = await this.slideshowsService.updateSlideshow(id, updateSlideshow);

						return updatedSlideshow;
				} catch (error) {
						throw new NotFoundException();
				}
		}

		@Delete('/:id') // (6)!
		@HttpCode(204)
		async deleteSlideshowApi(@Param('id') id: string) {
				try {
						await this.slideshowsService.deleteSlideshow(id);
				} catch (error) {
						throw new NotFoundException();
				}
		}
}
  1. We prefix the controller with /api/slideshows so all endpoints defined in it will start will /api/slideshows.
  2. This is equal to /api/slideshows.
  3. This is equal to /api/slideshows.
  4. This is equal to /api/slideshows.
  5. This is equal to /api/slideshows.
  6. This is equal to /api/slideshows.
src/slideshows/slideshows-views.controller.ts
import {
		Get,
		Controller,
		Render,
		Param,
		Res,
} from '@nestjs/common';
import { Response } from 'express';
import { SlideshowsService } from './slideshows.service';

@Controller('/slideshows')
export class SlideshowsViewsController {
		constructor(private readonly slideshowsService: SlideshowsService) { }

		@Get()
		@Render('index')
		async getSlideshows() {
				const slideshows = await this.slideshowsService.getSlideshows();

				return { slideshows: slideshows };
		}

		@Get('/:id')
		async getSlideshow(@Res() res: Response, @Param('id') id: string) {
				const slideshow = await this.slideshowsService.getSlideshow(id);

				if (!slideshow) {
						return res.redirect('/slideshows');
				}

				return res.render(
						'slideshow',
						{ slideshow: slideshow },
				);
		}
}

Update the AppController to continue to redirect to /slideshows when accessing /.

src/app.controller.ts
1
2
3
4
5
6
7
8
import { Get, Controller, Redirect } from '@nestjs/common';

@Controller()
export class AppController {
	@Get()
	@Redirect('/slideshows')
	root() {}
}

Create the slideshows module

Create the slideshows module so it can be used in the rest of the application.

src/slideshows/slideshows.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { SlideshowsApiController } from './slideshows-api.controller';
import { SlideshowsViewsController } from './slideshows-views.controller';
import { SlideshowsService } from './slideshows.service';

@Module({
	imports: [PrismaModule],
	controllers: [SlideshowsApiController, SlideshowsViewsController],
	providers: [SlideshowsService],
})
export class SlideshowsModule {}

Update the main module

Update the main module to load the slideshows module. As the slideshows module loads everything it needs, the app module only loads what needs to be loaded.

src/app.module.ts
1
2
3
4
5
6
7
8
9
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { SlideshowsModule } from './slideshows/slideshows.module';

@Module({
	imports: [SlideshowsModule],
	controllers: [AppController],
})
export class AppModule {}

Summary

Congrats! Your application now persists all the slideshows in the database.

Prisma speeds up the development of the database thanks to its schema and the migrations.

The Prisma client generated from the schema ensures the types of your inputs are correct thanks to TypeScript.

When restarting the application, there is no more data loss.

SQLite helps to develop simple applications as only a file is required to store the database. For more robust applications, another kind of database is recommended, such as PostgreSQL. However, for development, SQLite is perfectly fine.