A real-world DDD
implementation project comprises the same phases as any other software development project. These phases include:
Refine and refactor the domain model based on the design and development (Continuous Integration (CI) of model concepts)
.
Repeat the above steps using the updated domain model (CI of domain implementation)
.
An agile software development methodology is a great fit here because agile methodologies focus on the delivery of business value just like DDD focuses on the alignment of software system with business model. Also, using SCRUM (for project management)
and XP (for software development purposes)
methodologies is a good combination for managing a DDD implementation project.
root
├─ src
│ ├─ app
│ ├─ constants
│ │ └─ endpoints.ts
│ ├─ modules
│ └─ user
│ ├─ domains
│ │ ├─ models
│ │ │ └─ User.ts
│ │ └─ IUserRepository.ts
│ │ └─ IUserService.ts
│ │
│ ├─ User.presentation.ts
│ ├─ User.repository.ts
│ └─ User.service.ts
|
├─ .eslintrc.json
├─ .gitignore
├─ package.json
└─ tsconfig.json
The domain layer contains the business logic of the application.
It includes the domain model (models)
and the repository, service
interface .
The domain model
encapsulates the business rules and the repository
interface defines the contract for interacting with the data source.
├─ domains
│ ├─ models
│ │ └─ User.ts
│ └─ IUserRepository.ts
│ └─ IUserService.ts
- user/domains/models/User.ts
export interface User {
email: string;
username: string;
bio?: string;
image?: string;
token: string;
}
- user/domains/IUserRepository.ts
export interface IUserRepository {
update(body: UserUpdateParams): Promise<ResponseObject<UserUpdate>>;
findByToken(): Promise<ResponseObject<UserCurrent>>;
findByEmailAndPassword(
body: UserLoginParams,
): Promise<ResponseObject<UserCurrent>>;
create(body: UserCreateParams): Promise<ResponseObject<UserCreate>>;
}
- user/domains/IUserService.ts
import { IUserRepository } from './IUserRepository';
export interface IUserService {
login: IUserRepository['findByEmailAndPassword'];
register: IUserRepository['create'];
update: IUserRepository['update'];
getCurrentUser: IUserRepository['findByToken'];
}
The infrastructure layer contains the infrastructure services (infrastructure)
and the API repository (restFull, graphQl,...)
.
The infrastructure services handle the technical concerns of the application, such as database access and network communication.
The API repository interacts
with the API and maps the data to the domain model.
└─ user
├─ ...
├─ User.repository.ts
- user/User.repository.ts
function UserRepository(): IUserRepository {
return {
findByToken: () =>
serviceHandler<UserCurrent>(() =>
request.get(endpoints.USERS.GET_USER()),
),
findByEmailAndPassword: (body: UserLoginParams) =>
serviceHandler<UserCurrent>(() =>
requestWithoutAuth.post(endpoints.USERS.POST_USERS_LOGIN(), body),
),
update: (body: UserUpdateParams) =>
serviceHandler<UserUpdate>(() =>
request.put(endpoints.USERS.PUT_USER(), body),
),
create: (body: UserCreateParams) =>
serviceHandler<UserCreate>(() =>
requestWithoutAuth.post(endpoints.USERS.POST_USERS(), body),
),
};
}
The application layer contains the application services (services)
and the data transfer objects (dtos)
.
The application services encapsulate the use-cases
of the application and the data transfer objects shape the data for the client-side.
└─ user
├─ ...
├─ User.service.ts
- user/User.service.ts
function UserService(
UserRepository: IUserRepository,
redirect?: Function,
): IUserService {
): IUserService {
return {
getCurrentUser: () => serviceHandler(UserRepository.findByToken),
login: (body: UserLoginParams) =>
serviceHandler(() => UserRepository.findByEmailAndPassword(body), {
onSuccess: (response) => {
const token = response?.data?.user?.token;
if (token) {
cookies.set('access_token', token);
}
},
}),
update: (body: UserUpdateParams) =>
serviceHandler(() => UserRepository.update(body)),
register: (body: UserCreateParams) =>
serviceHandler(() => UserRepository.create(body), {
onSuccess: (response) => {
const token = response?.data.user?.token;
if (cookies && token && redirect) {
cookies.set('access_token', token);
redirect('/');
}
return response;
},
}),
};
}
The presentation layer contains the controllers (presentation)
and the query library (reactQuery, SWR, ...)
.
The controllers handle the user interactions and delegate the work to the application layer.
For example:
the React Query
handles the state management and the data fetching for the React components.
└─ user
├─ ...
├─ User.presentation.ts
- user/User.presentation.ts
const userService = UserService(UserRepository());
export function UserPresentation() {
return {
useGetCurrentUser: () =>
useQuery({
queryKey: ['user'],
queryFn: () => userService.getCurrentUser(),
}),
useUserLogin: () => {
const router = useRouter();
const searchParams = useSearchParams();
return useMutation({
mutationFn: () => {
const rawFormData: UserLoginParams = {
user: {
email: '{{EMAIL}}',
password: '{{PASSWORD}}',
},
};
return userService.login(rawFormData);
},
onSuccess(response) {
console.log('onSuccess :>> ', response);
const nextUrl = searchParams.get('next');
router.push(nextUrl ?? '/');
},
onError(error) {
console.log('error :>> ', error);
},
});
},
}
This project utilizes npm run cli
to automate the generation of modules, app routers, and endpoints in a structured manner. It helps streamline the development process by providing templates and predefined structures for common tasks.
After run npm run cli
:
Module Name: Enter the name of the module you want to create. Generated Files:
src/modules/{moduleName}/domains/I{ModuleName}Service.ts
src/modules/{moduleName}/domains/models/{ModuleName}.ts
src/modules/{moduleName}/{ModuleName}.service.ts
src/modules/{moduleName}/{ModuleName}.presentation.ts
- Router Path: Specify the path for the app router.
- Router File Name: Provide a name for the router file.
- Module Name: Select the module to which you want to add the router.
src/app/[locale]/dashboard/{routerPath}/page.tsx
src/app/[locale]/dashboard/{routerPath}/_viewModule/{partialName}.context.tsx
src/app/[locale]/dashboard/{routerPath}/_viewModule/{partialName}.view.tsx
src/app/[locale]/dashboard/{routerPath}/_viewModule/{partialName}.vm.ts
- Endpoint Name: Enter the name of the endpoint you want to add.
The endpoint will be added to src/constants/endpoints.ts with the following structure:
{moduleName}: {
GET_{moduleName}: () => `${HOST_URL_API}/{moduleName}/`,
POST_{moduleName}: () => `${HOST_URL_API}/{moduleName}/`,
GET_{moduleName}_ID: (id: string) => `${HOST_URL_API}/{moduleName}/${id}/`,
PUT_{moduleName}_ID: (id: string) => `${HOST_URL_API}/{moduleName}/${id}/`,
DELETE_{moduleName}_ID: (id: string) => `${HOST_URL_API}/{moduleName}/${id}/`,
},
This is a Next.js project bootstrapped with create-next-app
.
First, run the development server:
# install dependencies
npm i
# run develop mode
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
Open http://localhost:3000/login with your browser to see the result.
- username: {{EMAIL}}
- password: {{PASSWORD}}
To learn more about Next.js, take a look at the following resources:
- Next.js Documentation - learn about Next.js features and API.
- Learn Next.js - an interactive Next.js tutorial.
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.