├── .editorconfig
├── .github
└── FUNDING.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── LICENSE
├── README.md
├── angular.json
├── assets
└── images
│ └── angular-clean-architecture-banner.png
├── commitlint.config.js
├── eslint.config.js
├── package.json
├── src
├── app
│ ├── core
│ │ ├── infrastructure
│ │ │ └── models
│ │ │ │ ├── payload.dto.ts
│ │ │ │ └── response.dto.ts
│ │ └── presentation
│ │ │ ├── app.component.html
│ │ │ ├── app.component.scss
│ │ │ ├── app.component.ts
│ │ │ ├── app.config.ts
│ │ │ ├── app.routes.ts
│ │ │ ├── styles.css
│ │ │ └── types
│ │ │ ├── list-state.ts
│ │ │ └── pagination-state.ts
│ └── post
│ │ ├── application
│ │ ├── types
│ │ │ ├── get-posts.payload.ts
│ │ │ └── get-posts.response.ts
│ │ └── use-cases
│ │ │ ├── find-post.use-case.ts
│ │ │ └── get-posts.use-case.ts
│ │ ├── domain
│ │ ├── entities
│ │ │ └── post.entity.ts
│ │ └── specifications
│ │ │ └── post-repository.interface.ts
│ │ ├── infrastructure
│ │ ├── implementations
│ │ │ └── post-repository.ts
│ │ └── models
│ │ │ ├── get-posts.query.ts
│ │ │ └── post.dto.ts
│ │ ├── post.module.ts
│ │ └── presentation
│ │ ├── pages
│ │ ├── post-page
│ │ │ ├── post-page.component.html
│ │ │ ├── post-page.component.scss
│ │ │ └── post-page.component.ts
│ │ └── posts-page
│ │ │ ├── posts-page.component.html
│ │ │ ├── posts-page.component.scss
│ │ │ └── posts-page.component.ts
│ │ ├── providers
│ │ ├── find-post.provider.ts
│ │ └── get-posts.provider.ts
│ │ └── types
│ │ ├── find-post-provider-state.ts
│ │ └── get-posts-provider-state.ts
├── assets
│ └── .gitkeep
├── favicon.ico
├── index.html
└── main.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [carlossalasamper]
2 | buy_me_a_coffee: carlossala95
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | .history/*
26 |
27 | # Miscellaneous
28 | /.angular/cache
29 | .sass-cache/
30 | /connect.lock
31 | /coverage
32 | /libpeerconnection.log
33 | testem.log
34 | /typings
35 |
36 | # System files
37 | .DS_Store
38 | Thumbs.db
39 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | npx --no -- commitlint --edit ${1}
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | yarn lint
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Carlos Sala Samper
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular Clean Architecture
2 |
3 |
4 |
5 |
6 | An Angular scaffold with a clean architecture that is easy to understand.
7 |
8 | ## Features
9 |
10 | - 🚀 [Angular 19](https://blog.angular.dev/meet-angular-v19-7b29dfd05b84)
11 | - 📁 Clean architecture. Layered file structure
12 | - 🛡️ TypeScript bulletproof typing
13 | - 🎨 Design System and UI: [Tailwind CSS](https://tailwindcss.com/)
14 | - 🖌️ Code format: [angular-eslint](https://github.com/angular-eslint/angular-eslint?tab=readme-ov-file#quick-start)
15 | - 🧰 State Manager: [NgRx](https://ngrx.io/)
16 | - 🐩 Git hooks: [Husky](https://www.npmjs.com/package/husky)
17 |
18 |
19 |
20 | ## 📁 Project File Structure
21 |
22 | > ⚠️ What makes the implementation of the clean architecture concept more difficult in my opinion is that since it is defined theoretically, each person implements it using different terminology or omitting/adding some layers or pieces to simplify it or continue to make it more complex.
23 |
24 | For this reason, I think it is important to emphasize the documentation that accompanies the architecture to avoid obstacles with the rest of the people who are going to work with this system.
25 |
26 | I briefly explain each of the four layers that make up clean architecture within the /src folder:
27 |
28 | ```
29 | └── /src
30 | └── /app
31 | ├── /core # Core bounded context
32 | │ └── /presentation
33 | └── /post # Post bounded context
34 | ├── /domain
35 | ├── /application
36 | ├── /infrastructure
37 | └── /presentation
38 | ```
39 |
40 | ### Domain
41 |
42 | This layer contains all the enterprise business rules: entities, specifications...
43 |
44 | ### Application
45 |
46 | This layer contains the use cases of the bounded context.
47 |
48 | ### Infrastructure
49 |
50 | This layer contains the technical details (implementation) of the domain layer and third parties integrations.
51 |
52 | ### Presentation
53 |
54 | This layer contains the React source code: views and controllers (Mobx controllers).
55 |
56 | ### Referencesw
57 |
58 | - https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
59 | - https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/
60 |
61 |
62 |
63 | ## Development server
64 |
65 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
66 |
67 | ## Code scaffolding
68 |
69 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
70 |
71 | ## Build
72 |
73 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
74 |
75 | ## Running unit tests
76 |
77 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
78 |
79 | ## Running end-to-end tests
80 |
81 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
82 |
83 | ## Further help
84 |
85 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
86 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-clean-architecture": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "css"
11 | }
12 | },
13 | "root": "",
14 | "sourceRoot": "src",
15 | "prefix": "app",
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:application",
19 | "options": {
20 | "outputPath": "dist/angular-clean-architecture",
21 | "index": "src/index.html",
22 | "browser": "src/main.ts",
23 | "polyfills": [
24 | "zone.js"
25 | ],
26 | "tsConfig": "tsconfig.app.json",
27 | "inlineStyleLanguage": "css",
28 | "assets": [
29 | "src/favicon.ico",
30 | "src/assets"
31 | ],
32 | "styles": [
33 | "src/app/core/presentation/styles.css"
34 | ],
35 | "scripts": []
36 | },
37 | "configurations": {
38 | "production": {
39 | "budgets": [
40 | {
41 | "type": "initial",
42 | "maximumWarning": "500kb",
43 | "maximumError": "1mb"
44 | },
45 | {
46 | "type": "anyComponentStyle",
47 | "maximumWarning": "2kb",
48 | "maximumError": "4kb"
49 | }
50 | ],
51 | "outputHashing": "all"
52 | },
53 | "development": {
54 | "optimization": false,
55 | "extractLicenses": false,
56 | "sourceMap": true
57 | }
58 | },
59 | "defaultConfiguration": "production"
60 | },
61 | "serve": {
62 | "builder": "@angular-devkit/build-angular:dev-server",
63 | "configurations": {
64 | "production": {
65 | "buildTarget": "angular-clean-architecture:build:production"
66 | },
67 | "development": {
68 | "buildTarget": "angular-clean-architecture:build:development"
69 | }
70 | },
71 | "defaultConfiguration": "development"
72 | },
73 | "extract-i18n": {
74 | "builder": "@angular-devkit/build-angular:extract-i18n",
75 | "options": {
76 | "buildTarget": "angular-clean-architecture:build"
77 | }
78 | },
79 | "test": {
80 | "builder": "@angular-devkit/build-angular:karma",
81 | "options": {
82 | "polyfills": [
83 | "zone.js",
84 | "zone.js/testing"
85 | ],
86 | "tsConfig": "tsconfig.spec.json",
87 | "inlineStyleLanguage": "css",
88 | "assets": [
89 | "src/favicon.ico",
90 | "src/assets"
91 | ],
92 | "styles": [
93 | "src/app/core/presentation/styles.css"
94 | ],
95 | "scripts": []
96 | }
97 | },
98 | "lint": {
99 | "builder": "@angular-eslint/builder:lint",
100 | "options": {
101 | "lintFilePatterns": [
102 | "src/**/*.ts",
103 | "src/**/*.html"
104 | ]
105 | }
106 | }
107 | }
108 | }
109 | },
110 | "cli": {
111 | "schematicCollections": [
112 | "angular-eslint"
113 | ]
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/assets/images/angular-clean-architecture-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carlossalasamper/angular-clean-architecture/90d6880ad56c86763d08207b2ba7591d10ab19c6/assets/images/angular-clean-architecture-banner.png
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["@commitlint/config-conventional"],
3 | };
4 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const eslint = require("@eslint/js");
3 | const tseslint = require("typescript-eslint");
4 | const angular = require("angular-eslint");
5 |
6 | module.exports = tseslint.config(
7 | {
8 | files: ["**/*.ts"],
9 | extends: [
10 | eslint.configs.recommended,
11 | ...tseslint.configs.recommended,
12 | ...tseslint.configs.stylistic,
13 | ...angular.configs.tsRecommended,
14 | ],
15 | processor: angular.processInlineTemplates,
16 | rules: {
17 | "@angular-eslint/directive-selector": [
18 | "error",
19 | {
20 | type: "attribute",
21 | prefix: "app",
22 | style: "camelCase",
23 | },
24 | ],
25 | "@angular-eslint/component-selector": [
26 | "error",
27 | {
28 | type: "element",
29 | prefix: "app",
30 | style: "kebab-case",
31 | },
32 | ],
33 | },
34 | },
35 | {
36 | files: ["**/*.html"],
37 | extends: [
38 | ...angular.configs.templateRecommended,
39 | ...angular.configs.templateAccessibility,
40 | ],
41 | rules: {},
42 | }
43 | );
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-clean-architecture",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "prepare": "husky",
6 | "ng": "ng",
7 | "start": "ng serve",
8 | "build": "ng build",
9 | "watch": "ng build --watch --configuration development",
10 | "test": "ng test",
11 | "lint": "ng lint --max-warnings=0"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "^19.0.0",
16 | "@angular/common": "^19.0.0",
17 | "@angular/compiler": "^19.0.0",
18 | "@angular/core": "^19.0.0",
19 | "@angular/forms": "^19.0.0",
20 | "@angular/platform-browser": "^19.0.0",
21 | "@angular/platform-browser-dynamic": "^19.0.0",
22 | "@angular/router": "^19.0.0",
23 | "@ngrx/component-store": "^19.0.0",
24 | "@ngrx/operators": "^19.0.0",
25 | "class-transformer": "^0.5.1",
26 | "immer": "^10.1.1",
27 | "rxjs": "~7.8.0",
28 | "tslib": "^2.3.0",
29 | "zone.js": "~0.15.0"
30 | },
31 | "devDependencies": {
32 | "@angular-devkit/build-angular": "^19.0.0",
33 | "@angular/cli": "^19.0.0",
34 | "@angular/compiler-cli": "^19.0.0",
35 | "@commitlint/cli": "~18.6.1",
36 | "@commitlint/config-conventional": "~18.6.2",
37 | "@commitlint/prompt-cli": "~18.6.1",
38 | "@types/jasmine": "~5.1.0",
39 | "@types/node": "^18.0.0",
40 | "angular-eslint": "19.0.2",
41 | "autoprefixer": "^10.4.20",
42 | "commitlint": "^18.4.3",
43 | "eslint": "^9.16.0",
44 | "husky": "^9.1.7",
45 | "jasmine-core": "~5.4.0",
46 | "karma": "~6.4.4",
47 | "karma-chrome-launcher": "~3.2.0",
48 | "karma-coverage": "~2.2.1",
49 | "karma-jasmine": "~5.1.0",
50 | "karma-jasmine-html-reporter": "~2.1.0",
51 | "postcss": "^8.4.49",
52 | "tailwindcss": "^3.4.16",
53 | "typescript": "~5.5.4",
54 | "typescript-eslint": "8.18.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/core/infrastructure/models/payload.dto.ts:
--------------------------------------------------------------------------------
1 | import { instanceToPlain } from 'class-transformer';
2 |
3 | export default abstract class PayloadDto {
4 | /**
5 | * @description Maps the domain entity to the infrastructure layer model. This method is used in the constructor.
6 | * @param payload
7 | */
8 | abstract transform(payload: ApplicationType): unknown;
9 |
10 | constructor(payload: ApplicationType) {
11 | const props = this.transform(payload);
12 |
13 | Object.assign(this, props);
14 | }
15 |
16 | toPlain() {
17 | return instanceToPlain(this, { excludeExtraneousValues: true });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/core/infrastructure/models/response.dto.ts:
--------------------------------------------------------------------------------
1 | export default abstract class ResponseDto {
2 | abstract toDomain(): DomainType;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/core/presentation/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/core/presentation/app.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carlossalasamper/angular-clean-architecture/90d6880ad56c86763d08207b2ba7591d10ab19c6/src/app/core/presentation/app.component.scss
--------------------------------------------------------------------------------
/src/app/core/presentation/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { RouterOutlet } from '@angular/router';
4 |
5 | @Component({
6 | selector: 'app-root',
7 | standalone: true,
8 | imports: [CommonModule, RouterOutlet],
9 | templateUrl: './app.component.html',
10 | styleUrl: './app.component.scss'
11 | })
12 | export class AppComponent {
13 | title = 'angular-clean-architecture';
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/core/presentation/app.config.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationConfig } from '@angular/core';
2 | import { provideRouter } from '@angular/router';
3 |
4 | import { routes } from './app.routes';
5 | import { provideHttpClient } from '@angular/common/http';
6 |
7 | export const appConfig: ApplicationConfig = {
8 | providers: [provideRouter(routes), provideHttpClient()],
9 | };
10 |
--------------------------------------------------------------------------------
/src/app/core/presentation/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 | import { PostsPageComponent } from '../../post/presentation/pages/posts-page/posts-page.component';
3 | import { PostPageComponent } from '../../post/presentation/pages/post-page/post-page.component';
4 |
5 | export const routes: Routes = [
6 | {
7 | path: '',
8 | component: PostsPageComponent,
9 | },
10 | {
11 | path: 'posts/:id',
12 | component: PostPageComponent,
13 | },
14 | ];
15 |
--------------------------------------------------------------------------------
/src/app/core/presentation/styles.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/app/core/presentation/types/list-state.ts:
--------------------------------------------------------------------------------
1 | import PaginationState from './pagination-state';
2 |
3 | export default interface ListState<
4 | ResultItemType,
5 | FiltersType = Record
6 | > {
7 | isLoading: boolean;
8 | results: ResultItemType[];
9 | count: number;
10 | filters: FiltersType;
11 | pagination: PaginationState;
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/core/presentation/types/pagination-state.ts:
--------------------------------------------------------------------------------
1 | export default interface PaginationState {
2 | page: number;
3 | pageSize: number;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/post/application/types/get-posts.payload.ts:
--------------------------------------------------------------------------------
1 | export default interface GetPostsPayload {
2 | page: number;
3 | pageSize: number;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/post/application/types/get-posts.response.ts:
--------------------------------------------------------------------------------
1 | import PostEntity from '../../domain/entities/post.entity';
2 |
3 | export default interface GetPostsResponse {
4 | results: PostEntity[];
5 | count: number;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/post/application/use-cases/find-post.use-case.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@angular/core';
2 | import {
3 | IPostRepository,
4 | IPostRepositoryToken,
5 | } from '../../domain/specifications/post-repository.interface';
6 |
7 | @Injectable()
8 | export default class FindPostUseCase {
9 | constructor(
10 | @Inject(IPostRepositoryToken)
11 | private readonly postRepository: IPostRepository
12 | ) {}
13 |
14 | public execute(id: number) {
15 | return this.postRepository.find(id);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/post/application/use-cases/get-posts.use-case.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@angular/core';
2 | import {
3 | IPostRepository,
4 | IPostRepositoryToken,
5 | } from '../../domain/specifications/post-repository.interface';
6 | import GetPostsPayload from '../types/get-posts.payload';
7 |
8 | @Injectable()
9 | export default class GetPostsUseCase {
10 | constructor(
11 | @Inject(IPostRepositoryToken)
12 | private readonly postRepository: IPostRepository
13 | ) {}
14 |
15 | public execute(data: GetPostsPayload) {
16 | return this.postRepository.get(data);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/post/domain/entities/post.entity.ts:
--------------------------------------------------------------------------------
1 | export default interface PostEntity {
2 | id: number;
3 | userId: number;
4 | title: string;
5 | body: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/post/domain/specifications/post-repository.interface.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs';
2 | import GetPostsPayload from '../../application/types/get-posts.payload';
3 | import GetPostsResponse from '../../application/types/get-posts.response';
4 | import PostEntity from '../entities/post.entity';
5 | import { InjectionToken } from '@angular/core';
6 |
7 | export interface IPostRepository {
8 | find: (id: number) => Observable;
9 | get: (data: GetPostsPayload) => Observable;
10 | }
11 |
12 | export const IPostRepositoryToken = InjectionToken;
13 |
--------------------------------------------------------------------------------
/src/app/post/infrastructure/implementations/post-repository.ts:
--------------------------------------------------------------------------------
1 | import { plainToInstance } from 'class-transformer';
2 | import { Injectable } from '@angular/core';
3 | import { HttpClient } from '@angular/common/http';
4 | import { IPostRepository } from '../../domain/specifications/post-repository.interface';
5 | import GetPostsResponse from '../../application/types/get-posts.response';
6 | import PostDto from '../models/post.dto';
7 | import { map, Observable } from 'rxjs';
8 | import PostEntity from '../../domain/entities/post.entity';
9 |
10 | @Injectable()
11 | class PostRepository implements IPostRepository {
12 | private readonly baseUrl = 'https://jsonplaceholder.typicode.com/posts';
13 |
14 | constructor(private readonly httpClient: HttpClient) {}
15 |
16 | public find(id: number): Observable {
17 | return this.httpClient
18 | .get(`${this.baseUrl}/${id}`)
19 | .pipe(map((response) => plainToInstance(PostDto, response).toDomain()));
20 | }
21 |
22 | public get(): Observable {
23 | return this.httpClient.get[]>(this.baseUrl).pipe(
24 | map((response) => ({
25 | results: response.map((post) =>
26 | plainToInstance(PostDto, post).toDomain()
27 | ),
28 | count: response.length,
29 | }))
30 | );
31 | }
32 | }
33 |
34 | export default PostRepository;
35 |
--------------------------------------------------------------------------------
/src/app/post/infrastructure/models/get-posts.query.ts:
--------------------------------------------------------------------------------
1 | import { Expose } from 'class-transformer';
2 | import GetPostsPayload from '../../application/types/get-posts.payload';
3 | import PayloadDto from '../../../core/infrastructure/models/payload.dto';
4 |
5 | export default class GetPostsQuery extends PayloadDto {
6 | @Expose()
7 | page!: number;
8 |
9 | @Expose()
10 | pageSize!: number;
11 |
12 | transform(payload: GetPostsPayload) {
13 | return {
14 | page: payload.page,
15 | pageSize: payload.pageSize,
16 | };
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/post/infrastructure/models/post.dto.ts:
--------------------------------------------------------------------------------
1 | import { Expose } from 'class-transformer';
2 | import PostEntity from '../../domain/entities/post.entity';
3 | import ResponseDto from '../../../core/infrastructure/models/response.dto';
4 |
5 | export default class PostDto extends ResponseDto {
6 | @Expose()
7 | id!: number;
8 |
9 | @Expose()
10 | userId!: number;
11 |
12 | @Expose()
13 | title!: string;
14 |
15 | @Expose()
16 | body!: string;
17 |
18 | toDomain() {
19 | return {
20 | id: this.id,
21 | userId: this.userId,
22 | title: this.title,
23 | body: this.body,
24 | };
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/post/post.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { IPostRepositoryToken } from './domain/specifications/post-repository.interface';
3 | import PostRepository from './infrastructure/implementations/post-repository';
4 | import FindPostUseCase from './application/use-cases/find-post.use-case';
5 | import GetPostsUseCase from './application/use-cases/get-posts.use-case';
6 | import { CommonModule } from '@angular/common';
7 |
8 | @NgModule({
9 | imports: [CommonModule],
10 | providers: [
11 | FindPostUseCase,
12 | GetPostsUseCase,
13 | {
14 | provide: IPostRepositoryToken,
15 | useClass: PostRepository,
16 | },
17 | ],
18 | })
19 | export class PostModule {}
20 |
--------------------------------------------------------------------------------
/src/app/post/presentation/pages/post-page/post-page.component.html:
--------------------------------------------------------------------------------
1 |
2 | {{ post.title }}
3 | {{ post.body }}
4 |
5 |
--------------------------------------------------------------------------------
/src/app/post/presentation/pages/post-page/post-page.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carlossalasamper/angular-clean-architecture/90d6880ad56c86763d08207b2ba7591d10ab19c6/src/app/post/presentation/pages/post-page/post-page.component.scss
--------------------------------------------------------------------------------
/src/app/post/presentation/pages/post-page/post-page.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { PostModule } from '../../../post.module';
4 | import PostEntity from '../../../domain/entities/post.entity';
5 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6 | import { ActivatedRoute } from '@angular/router';
7 | import { FindPostProvider } from '../../providers/find-post.provider';
8 |
9 | @Component({
10 | selector: 'app-post-page',
11 | standalone: true,
12 | imports: [CommonModule, PostModule],
13 | templateUrl: './post-page.component.html',
14 | styleUrl: './post-page.component.scss',
15 | providers: [FindPostProvider],
16 | })
17 | export class PostPageComponent implements OnInit {
18 | post: PostEntity | null = null;
19 |
20 | constructor(
21 | private readonly findPostProvider: FindPostProvider,
22 | private readonly activatedRoute: ActivatedRoute
23 | ) {
24 | this.findPostProvider.post$.pipe(takeUntilDestroyed()).subscribe((post) => {
25 | this.post = post;
26 | });
27 | }
28 |
29 | ngOnInit() {
30 | const id = this.activatedRoute.snapshot.paramMap.get('id') as string;
31 |
32 | this.findPostProvider.findPost(parseInt(id));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/post/presentation/pages/posts-page/posts-page.component.html:
--------------------------------------------------------------------------------
1 |
5 |
6 | {{ post.title }}
7 | {{ post.body }}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/post/presentation/pages/posts-page/posts-page.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carlossalasamper/angular-clean-architecture/90d6880ad56c86763d08207b2ba7591d10ab19c6/src/app/post/presentation/pages/posts-page/posts-page.component.scss
--------------------------------------------------------------------------------
/src/app/post/presentation/pages/posts-page/posts-page.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { GetPostsProvider } from '../../providers/get-posts.provider';
4 | import { PostModule } from '../../../post.module';
5 | import PostEntity from '../../../domain/entities/post.entity';
6 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7 | import { RouterLink } from '@angular/router';
8 |
9 | @Component({
10 | selector: 'app-posts-page',
11 | standalone: true,
12 | imports: [CommonModule, PostModule, RouterLink],
13 | templateUrl: './posts-page.component.html',
14 | styleUrl: './posts-page.component.scss',
15 | providers: [GetPostsProvider],
16 | })
17 | export class PostsPageComponent implements OnInit {
18 | posts: PostEntity[] = [];
19 |
20 | constructor(private readonly getPostsProvider: GetPostsProvider) {
21 | this.getPostsProvider.results$
22 | .pipe(takeUntilDestroyed())
23 | .subscribe((posts) => {
24 | this.posts = posts;
25 | });
26 | }
27 |
28 | ngOnInit() {
29 | this.getPostsProvider.getPosts();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/post/presentation/providers/find-post.provider.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { ComponentStore } from '@ngrx/component-store';
3 | import { mergeMap, tap } from 'rxjs';
4 | import { produce } from 'immer';
5 | import FindPostUseCase from '../../application/use-cases/find-post.use-case';
6 | import FindPostProviderState from '../types/find-post-provider-state';
7 | import PostEntity from '../../domain/entities/post.entity';
8 | import { tapResponse } from '@ngrx/operators';
9 |
10 | @Injectable()
11 | export class FindPostProvider extends ComponentStore {
12 | readonly isLoading$ = this.select((state) => state.isLoading);
13 | readonly post$ = this.select((state) => state.post);
14 |
15 | constructor(private readonly findPostUseCase: FindPostUseCase) {
16 | super({
17 | isLoading: false,
18 | post: null,
19 | });
20 | }
21 |
22 | setIsLoading = this.updater((state, value: boolean) =>
23 | produce(state, (draft) => {
24 | draft.isLoading = value;
25 | })
26 | );
27 |
28 | setPost = this.updater((state, post: PostEntity) =>
29 | produce(state, (draft) => {
30 | draft.post = post;
31 | })
32 | );
33 |
34 | findPost = this.effect((trigger$) => {
35 | return trigger$.pipe(
36 | tap(() => {
37 | this.setIsLoading(true);
38 | }),
39 | mergeMap((id) => {
40 | return this.findPostUseCase.execute(id).pipe(
41 | tapResponse(
42 | (post) => {
43 | this.setIsLoading(false);
44 | this.setPost(post);
45 | },
46 | (error) => {
47 | this.setIsLoading(false);
48 | console.error(error);
49 | }
50 | )
51 | );
52 | })
53 | );
54 | });
55 | }
56 |
--------------------------------------------------------------------------------
/src/app/post/presentation/providers/get-posts.provider.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { ComponentStore } from '@ngrx/component-store';
3 | import { mergeMap, tap } from 'rxjs';
4 | import { produce } from 'immer';
5 | import GetPostsProviderState from '../types/get-posts-provider-state';
6 | import GetPostsUseCase from '../../application/use-cases/get-posts.use-case';
7 | import PostEntity from '../../domain/entities/post.entity';
8 | import GetPostsPayload from '../../application/types/get-posts.payload';
9 | import { tapResponse } from '@ngrx/operators';
10 |
11 | @Injectable()
12 | export class GetPostsProvider extends ComponentStore {
13 | readonly isLoading$ = this.select((state) => state.isLoading);
14 | readonly results$ = this.select((state) => state.results);
15 | readonly filters$ = this.select((state) => state.filters);
16 | readonly pagination$ = this.select((state) => state.pagination);
17 | readonly count$ = this.select((state) => state.count);
18 |
19 | constructor(private readonly getPostsUseCase: GetPostsUseCase) {
20 | super({
21 | isLoading: false,
22 | results: [],
23 | count: 0,
24 | filters: {},
25 | pagination: {
26 | page: 1,
27 | pageSize: 10,
28 | },
29 | });
30 | }
31 |
32 | setIsLoading = this.updater((state, value: boolean) =>
33 | produce(state, (draft) => {
34 | draft.isLoading = value;
35 | })
36 | );
37 |
38 | setResults = this.updater((state, results: PostEntity[]) =>
39 | produce(state, (draft) => {
40 | draft.results = results;
41 | })
42 | );
43 |
44 | setCount = this.updater((state, count: number) =>
45 | produce(state, (draft) => {
46 | draft.count = count;
47 | })
48 | );
49 |
50 | mergePagination = this.updater((state, pagination: { page: number }) =>
51 | produce(state, (draft) => {
52 | draft.pagination = { ...draft.pagination, ...pagination };
53 | })
54 | );
55 |
56 | getPosts = this.effect((trigger$) => {
57 | return trigger$.pipe(
58 | tap(() => {
59 | this.setIsLoading(true);
60 | this.mergePagination({
61 | page: 1,
62 | });
63 | }),
64 | mergeMap(() => {
65 | const { pagination } = this.get();
66 | const input: GetPostsPayload = {
67 | page: pagination.page,
68 | pageSize: pagination.pageSize,
69 | };
70 |
71 | return this.getPostsUseCase.execute(input).pipe(
72 | tapResponse(
73 | (response) => {
74 | this.setResults(response.results);
75 | this.setCount(response.count);
76 | this.setIsLoading(false);
77 | },
78 | (error) => {
79 | console.error(error);
80 | this.setIsLoading(false);
81 | }
82 | )
83 | );
84 | })
85 | );
86 | });
87 | }
88 |
--------------------------------------------------------------------------------
/src/app/post/presentation/types/find-post-provider-state.ts:
--------------------------------------------------------------------------------
1 | import PostEntity from '../../domain/entities/post.entity';
2 |
3 | export default interface FindPostProviderState {
4 | isLoading: boolean;
5 | post: PostEntity | null;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/post/presentation/types/get-posts-provider-state.ts:
--------------------------------------------------------------------------------
1 | import ListState from '../../../core/presentation/types/list-state';
2 | import PostEntity from '../../domain/entities/post.entity';
3 |
4 | type GetPostsProviderState = ListState;
5 |
6 | export default GetPostsProviderState;
7 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carlossalasamper/angular-clean-architecture/90d6880ad56c86763d08207b2ba7591d10ab19c6/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carlossalasamper/angular-clean-architecture/90d6880ad56c86763d08207b2ba7591d10ab19c6/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AngularCleanArchitecture
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 | import { AppComponent } from './app/core/presentation/app.component';
3 | import { appConfig } from './app/core/presentation/app.config';
4 |
5 | bootstrapApplication(AppComponent, appConfig).catch((err) =>
6 | console.error(err)
7 | );
8 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./src/**/*.{html,ts}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": [
9 | "src/main.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "strict": true,
6 | "noImplicitOverride": true,
7 | "noPropertyAccessFromIndexSignature": true,
8 | "noImplicitReturns": true,
9 | "noFallthroughCasesInSwitch": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "sourceMap": true,
13 | "declaration": false,
14 | "experimentalDecorators": true,
15 | "moduleResolution": "node",
16 | "importHelpers": true,
17 | "target": "ES2022",
18 | "module": "ES2022",
19 | "useDefineForClassFields": false,
20 | "lib": ["ES2022", "dom"]
21 | },
22 | "angularCompilerOptions": {
23 | "enableI18nLegacyMessageIdFormat": false,
24 | "strictInjectionParameters": true,
25 | "strictInputAccessModifiers": true,
26 | "strictTemplates": true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": [
7 | "jasmine"
8 | ]
9 | },
10 | "include": [
11 | "src/**/*.spec.ts",
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------