├── .gitignore ├── src ├── core │ ├── app │ │ ├── index.css │ │ ├── assets │ │ │ └── logo.png │ │ ├── App.vue │ │ └── store │ │ │ └── index.ts │ └── domain │ │ ├── usecase.ts │ │ └── failure.ts ├── features │ └── todo │ │ ├── domain │ │ ├── entities │ │ │ └── todo.ts │ │ ├── ports │ │ │ └── todoPort.ts │ │ ├── todoTypes.ts │ │ ├── todoFailure.ts │ │ └── usecases │ │ │ ├── listTodoUseCase.ts │ │ │ ├── deleteTodoUseCase.ts │ │ │ └── createTodoUseCase.ts │ │ ├── infrastructure │ │ ├── model │ │ │ └── todoModel.ts │ │ ├── datasource │ │ │ ├── todoDatasource.ts │ │ │ └── todoInMemoryDataSource.ts │ │ ├── todoAdapter.ts │ │ └── inversify.config.ts │ │ └── app │ │ ├── components │ │ ├── TodoBuilder.spec.ts │ │ └── TodoBuilder.vue │ │ └── store │ │ └── todoModule.ts ├── shims-vue.d.ts └── main.ts ├── public └── favicon.ico ├── postcss.config.js ├── vite.config.ts ├── jest.config.js ├── tailwind.config.js ├── index.html ├── .vscode └── launch.json ├── tsconfig.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /src/core/app/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smotastic/vue-clean-architecture/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/features/todo/domain/entities/todo.ts: -------------------------------------------------------------------------------- 1 | export default interface Todo { 2 | id?: number; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/core/app/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smotastic/vue-clean-architecture/HEAD/src/core/app/assets/logo.png -------------------------------------------------------------------------------- /src/features/todo/infrastructure/model/todoModel.ts: -------------------------------------------------------------------------------- 1 | import Todo from "../../domain/entities/todo"; 2 | 3 | export interface TodoModel extends Todo {} 4 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()] 7 | }) 8 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./core/app/App.vue"; 3 | import "./core/app/index.css"; 4 | 5 | import { store, key } from "./core/app/store"; 6 | 7 | createApp(App).use(store, key).mount("#app"); 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | globals: {}, 4 | testEnvironment: 'jsdom', 5 | transform: { 6 | "^.+\\.vue$": "vue-jest", 7 | }, 8 | moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'] 9 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /src/features/todo/domain/ports/todoPort.ts: -------------------------------------------------------------------------------- 1 | import Todo from "../entities/todo"; 2 | 3 | export default interface TodoPort { 4 | createTodo(todoName: string): Promise; 5 | 6 | listTodo(): Promise; 7 | 8 | deleteTodo(id: number): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/features/todo/infrastructure/datasource/todoDatasource.ts: -------------------------------------------------------------------------------- 1 | import { TodoModel } from "./../model/todoModel"; 2 | export interface TodoDatasource { 3 | createTodo(todoName: string): Promise; 4 | 5 | listTodo(): Promise; 6 | 7 | deleteTodo(id: number): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/features/todo/domain/todoTypes.ts: -------------------------------------------------------------------------------- 1 | const TYPES = { 2 | TodoPort: Symbol("TodoPort"), 3 | TodoDataSource: Symbol("TodoDataSource"), 4 | CreateTodoUseCase: Symbol("CreateTodoUseCase"), 5 | ListTodoUseCase: Symbol("ListTodoUseCase"), 6 | DeleteTodoUseCase: Symbol("DeleteTodoUseCase"), 7 | }; 8 | 9 | export default TYPES; 10 | -------------------------------------------------------------------------------- /src/core/domain/usecase.ts: -------------------------------------------------------------------------------- 1 | import { Either } from "purify-ts/Either"; 2 | import Failure from "./failure"; 3 | 4 | export interface UseCase { 5 | execute(command: C): Promise>; 6 | } 7 | 8 | export interface UseCaseCommand {} 9 | 10 | export class EmptyUseCaseCommand implements UseCaseCommand {} 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/core/domain/failure.ts: -------------------------------------------------------------------------------- 1 | export default interface Failure { 2 | getMessage(): string; 3 | getCode(): string; 4 | } 5 | 6 | export class AbstractFailure implements Failure { 7 | getMessage(): string { 8 | return this.message; 9 | } 10 | getCode(): string { 11 | return this.code; 12 | } 13 | 14 | public constructor( 15 | public readonly code: string, 16 | public readonly message: string 17 | ) {} 18 | } 19 | -------------------------------------------------------------------------------- /src/features/todo/domain/todoFailure.ts: -------------------------------------------------------------------------------- 1 | import { AbstractFailure } from "../../../core/domain/failure"; 2 | 3 | export default class TodoFailure extends AbstractFailure { 4 | public static readonly nameTooLong = new TodoFailure( 5 | "1", 6 | "Name should not exceed 15 characters." // per localization one should use the key here 7 | ); 8 | 9 | public static readonly emptyName = new TodoFailure( 10 | "2", 11 | "Name cannot be empty." 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "lib": ["esnext", "dom"], 12 | "types": ["vite/client", "jest"], 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 17 | } 18 | -------------------------------------------------------------------------------- /src/features/todo/app/components/TodoBuilder.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount, mount } from "@vue/test-utils"; 2 | import TodoBuilder from "./TodoBuilder.vue"; 3 | 4 | describe("Description", () => { 5 | test("renders props.msg when passed", () => { 6 | console.log("Hallo"); 7 | expect("hallo").toMatch("hallo"); 8 | // const wrapper = mount(TodoBuilder, {}); 9 | // expect(wrapper.text()).toMatch(msg); 10 | }); 11 | }); 12 | 13 | // describe("HelloWorld.vue", () => { 14 | // test("renders props.msg when passed", () => { 15 | // const msg = "new message"; 16 | // const wrapper = shallowMount(TodoBuilder, { 17 | // propsData: { msg }, 18 | // }); 19 | // expect(wrapper.text()).toMatch(msg); 20 | // }); 21 | // }); 22 | -------------------------------------------------------------------------------- /src/core/app/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /src/features/todo/infrastructure/todoAdapter.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "inversify"; 2 | import Todo from "../domain/entities/todo"; 3 | import TodoPort from "../domain/ports/todoPort"; 4 | import TYPES from "../domain/todoTypes"; 5 | import { TodoDatasource } from "./datasource/todoDatasource"; 6 | 7 | @injectable() 8 | export default class TodoAdapter implements TodoPort { 9 | public todoDataSource!: TodoDatasource; 10 | 11 | public constructor( 12 | @inject(TYPES.TodoDataSource) todoDatasource: TodoDatasource 13 | ) { 14 | this.todoDataSource = todoDatasource; 15 | } 16 | 17 | async createTodo(todoName: string): Promise { 18 | return this.todoDataSource.createTodo(todoName); 19 | } 20 | 21 | listTodo(): Promise { 22 | return this.todoDataSource.listTodo(); 23 | } 24 | 25 | async deleteTodo(id: number): Promise { 26 | return this.todoDataSource.deleteTodo(id); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/features/todo/domain/usecases/listTodoUseCase.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EmptyUseCaseCommand, 3 | UseCase, 4 | UseCaseCommand, 5 | } from "./../../../../core/domain/usecase"; 6 | 7 | import { inject, injectable } from "inversify"; 8 | import { Either, Right } from "purify-ts/Either"; 9 | import Failure from "../../../../core/domain/failure"; 10 | import Todo from "../entities/todo"; 11 | import TodoPort from "../ports/todoPort"; 12 | import TYPES from "../todoTypes"; 13 | 14 | export default interface ListTodoUseCase 15 | extends UseCase {} 16 | @injectable() 17 | export class ListTodoUseCaseImpl implements ListTodoUseCase { 18 | private _todoPort: TodoPort; 19 | 20 | public constructor(@inject(TYPES.TodoPort) _todoPort: TodoPort) { 21 | this._todoPort = _todoPort; 22 | } 23 | async execute( 24 | command: EmptyUseCaseCommand 25 | ): Promise> { 26 | const todos = await this._todoPort.listTodo(); 27 | return Right(todos); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-clean-architecture", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vue-tsc --noEmit && vite build", 7 | "serve": "vite preview", 8 | "test:unit": "jest" 9 | }, 10 | "dependencies": { 11 | "inversify": "^5.0.5", 12 | "inversify-inject-decorators": "^3.1.0", 13 | "purify-ts": "^0.16.2", 14 | "reflect-metadata": "^0.1.13", 15 | "vue": "^3.0.5", 16 | "vuex": "^4.0.0", 17 | "vuex-module-decorators": "^1.0.1" 18 | }, 19 | "devDependencies": { 20 | "@types/jest": "^26.0.23", 21 | "@vitejs/plugin-vue": "^1.2.1", 22 | "@vue/compiler-sfc": "^3.0.5", 23 | "@vue/test-utils": "^2.0.0-rc.6", 24 | "autoprefixer": "^10.2.5", 25 | "jest": "^26.6.3", 26 | "postcss": "^8.2.10", 27 | "tailwindcss": "^2.1.1", 28 | "ts-jest": "^26.5.6", 29 | "ts-loader": "^9.1.2", 30 | "typescript": "^4.1.3", 31 | "vite": "^2.1.5", 32 | "vue-jest": "^5.0.0-alpha.9", 33 | "vue-tsc": "^0.0.24" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/core/app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKey } from "vue"; 2 | import Vuex from "vuex"; 3 | import { 4 | TodoState, 5 | TodoStore, 6 | } from "../../../features/todo/app/store/todoModule"; 7 | import { createStore, useStore as baseUseStore, Store } from "vuex"; 8 | import { VuexModule, getModule } from "vuex-module-decorators"; 9 | // https://next.vuex.vuejs.org/guide/typescript-support.html#typing-store-property-in-vue-component 10 | export interface RootState { 11 | todoModule: TodoState; 12 | } 13 | 14 | // define injection key 15 | export const key: InjectionKey> = Symbol(); 16 | 17 | export const store = new Vuex.Store({ 18 | modules: { 19 | todoModule: TodoStore, 20 | }, 21 | }); 22 | 23 | function useStore() { 24 | return baseUseStore(key); 25 | } 26 | declare type ConstructorOf = { 27 | new (...args: any[]): C; 28 | }; 29 | 30 | export function useModule( 31 | moduleClass: ConstructorOf 32 | ): M { 33 | const store = useStore(); 34 | const moduleStore: M = getModule(moduleClass, store); 35 | return moduleStore; 36 | } 37 | -------------------------------------------------------------------------------- /src/features/todo/infrastructure/datasource/todoInMemoryDataSource.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "inversify"; 2 | import { TodoModel } from "../model/todoModel"; 3 | import { TodoDatasource } from "./todoDatasource"; 4 | // ListModel und DetailModel einbauen 5 | // dataSource (inMemoryAdapter?) checken 6 | @injectable() 7 | export class TodoInMemoryDataSource implements TodoDatasource { 8 | async createTodo(todoName: string): Promise { 9 | const todos: TodoModel[] = await this.listTodo(); 10 | const createdTodo: TodoModel = { 11 | name: todoName, 12 | id: todos.length + 1, 13 | }; 14 | todos.push(createdTodo); 15 | localStorage.setItem("todos", JSON.stringify(todos)); 16 | return Promise.resolve(createdTodo); 17 | } 18 | listTodo(): Promise { 19 | return Promise.resolve(JSON.parse(localStorage.getItem("todos") || "[]")); 20 | } 21 | async deleteTodo(id: number): Promise { 22 | const todos: TodoModel[] = await this.listTodo(); 23 | const newTodos = todos.filter((todo) => todo.id !== id); 24 | localStorage.setItem("todos", JSON.stringify(newTodos)); 25 | return Promise.resolve(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/features/todo/domain/usecases/deleteTodoUseCase.ts: -------------------------------------------------------------------------------- 1 | import { UseCase } from "./../../../../core/domain/usecase"; 2 | 3 | import { UseCaseCommand } from "../../../../core/domain/usecase"; 4 | 5 | import TodoPort from "../ports/todoPort"; 6 | import TYPES from "../todoTypes"; 7 | import { Either, Right } from "purify-ts/Either"; 8 | import failure from "../../../../core/domain/failure"; 9 | import { inject, injectable } from "inversify"; 10 | export default interface DeleteTodoUseCase 11 | extends UseCase {} 12 | @injectable() 13 | export class DeleteTodoUseCaseImpl implements DeleteTodoUseCase { 14 | private _todoPort: TodoPort; 15 | 16 | public constructor(@inject(TYPES.TodoPort) _todoPort: TodoPort) { 17 | this._todoPort = _todoPort; 18 | } 19 | 20 | async execute( 21 | command: DeleteTodoUseCaseCommand 22 | ): Promise> { 23 | await this._todoPort.deleteTodo(command.id); 24 | return Right(undefined); 25 | } 26 | } 27 | export class DeleteTodoUseCaseCommand implements UseCaseCommand { 28 | public constructor(private readonly _id: number) {} 29 | 30 | public get id(): number { 31 | return this._id; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/features/todo/domain/usecases/createTodoUseCase.ts: -------------------------------------------------------------------------------- 1 | import Todo from "../entities/todo"; 2 | import { UseCase, UseCaseCommand } from "../../../../core/domain/usecase"; 3 | import { Either, Left, Right } from "purify-ts/Either"; 4 | import failure from "../../../../core/domain/failure"; 5 | import TodoPort from "../ports/todoPort"; 6 | import { inject, injectable } from "inversify"; 7 | import TYPES from "../todoTypes"; 8 | import TodoFailure from "../todoFailure"; 9 | export default interface CreateTodoUseCase 10 | extends UseCase {} 11 | @injectable() 12 | export class CreateTodoUseCaseImpl implements CreateTodoUseCase { 13 | private _todoPort: TodoPort; 14 | 15 | public constructor(@inject(TYPES.TodoPort) _todoPort: TodoPort) { 16 | this._todoPort = _todoPort; 17 | } 18 | 19 | async execute( 20 | command: CreateTodoUseCaseCommand 21 | ): Promise> { 22 | if (!command.todoName) { 23 | return Left(TodoFailure.emptyName); 24 | } 25 | if (command.todoName.length > 15) { 26 | return Left(TodoFailure.nameTooLong); 27 | } 28 | const result = await this._todoPort.createTodo(command.todoName); 29 | return Right(result); 30 | } 31 | } 32 | export class CreateTodoUseCaseCommand implements UseCaseCommand { 33 | public constructor(private readonly _todoName: string) {} 34 | 35 | public get todoName(): string { 36 | return this._todoName; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/features/todo/infrastructure/inversify.config.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { TodoInMemoryDataSource } from "./datasource/todoInMemoryDataSource"; 3 | import { TodoDatasource } from "./datasource/todoDatasource"; 4 | import { ListTodoUseCaseImpl } from "./../domain/usecases/listTodoUseCase"; 5 | import { DeleteTodoUseCaseImpl } from "./../domain/usecases/deleteTodoUseCase"; 6 | import { CreateTodoUseCaseImpl } from "./../domain/usecases/createTodoUseCase"; 7 | import { Container } from "inversify"; 8 | 9 | import getDecorators from "inversify-inject-decorators"; 10 | import TodoAdapter from "./todoAdapter"; 11 | import TYPES from "../domain/todoTypes"; 12 | import DeleteTodoUseCase from "../domain/usecases/deleteTodoUseCase"; 13 | import ListTodoUseCase from "../domain/usecases/listTodoUseCase"; 14 | import CreateTodoUseCase from "../domain/usecases/createTodoUseCase"; 15 | import TodoPort from "../domain/ports/todoPort"; 16 | 17 | const container = new Container(); 18 | container.bind(TYPES.TodoPort).to(TodoAdapter); 19 | container.bind(TYPES.TodoDataSource).to(TodoInMemoryDataSource); 20 | // .toConstructor(TodoInMemoryDataSource); 21 | container 22 | .bind(TYPES.CreateTodoUseCase) 23 | .to(CreateTodoUseCaseImpl); 24 | container.bind(TYPES.ListTodoUseCase).to(ListTodoUseCaseImpl); 25 | container 26 | .bind(TYPES.DeleteTodoUseCase) 27 | .to(DeleteTodoUseCaseImpl); 28 | const { lazyInject } = getDecorators(container); 29 | export { lazyInject, container }; 30 | -------------------------------------------------------------------------------- /src/features/todo/app/store/todoModule.ts: -------------------------------------------------------------------------------- 1 | import { Action, Module, Mutation, VuexModule } from "vuex-module-decorators"; 2 | import { lazyInject } from "../../infrastructure/inversify.config"; 3 | 4 | import Todo from "../../domain/entities/todo"; 5 | 6 | import { Either, Right } from "purify-ts/Either"; 7 | import Failure from "../../../../core/domain/failure"; 8 | import ListTodoUseCase from "../../domain/usecases/listTodoUseCase"; 9 | import CreateTodoUseCase, { 10 | CreateTodoUseCaseCommand, 11 | } from "../../domain/usecases/createTodoUseCase"; 12 | import DeleteTodoUseCase, { 13 | DeleteTodoUseCaseCommand, 14 | } from "../../domain/usecases/deleteTodoUseCase"; 15 | import TYPES from "../../domain/todoTypes"; 16 | import { EmptyUseCaseCommand } from "../../../../core/domain/usecase"; 17 | export interface TodoState { 18 | todos: Todo[]; 19 | } 20 | 21 | @Module({ 22 | name: "todoModule", 23 | namespaced: true, 24 | }) 25 | export class TodoStore extends VuexModule implements TodoState { 26 | @lazyInject(TYPES.ListTodoUseCase) 27 | public listUsecase!: ListTodoUseCase; 28 | @lazyInject(TYPES.CreateTodoUseCase) 29 | public createUsecase!: CreateTodoUseCase; 30 | @lazyInject(TYPES.DeleteTodoUseCase) 31 | public deleteUsecase!: DeleteTodoUseCase; 32 | 33 | public todos: Todo[] = []; 34 | 35 | @Mutation 36 | setTodos(items: Todo[]) { 37 | this.todos = items; 38 | } 39 | 40 | @Action({ rawError: true }) 41 | async fetchTodos(): Promise> { 42 | const list = await this.listUsecase.execute(new EmptyUseCaseCommand()); 43 | return list.chain((r) => { 44 | this.setTodos(r); 45 | return Right(undefined); 46 | }); 47 | } 48 | 49 | @Action({ rawError: true }) 50 | async addTodo(todoName: string): Promise> { 51 | const createdTodo = await this.createUsecase.execute( 52 | new CreateTodoUseCaseCommand(todoName) 53 | ); 54 | 55 | return createdTodo.chain((r) => { 56 | this.todos.push(r); 57 | return Right(r); 58 | }); 59 | } 60 | 61 | @Action({ rawError: true }) 62 | async deleteTodo(id: number) { 63 | await this.deleteUsecase.execute(new DeleteTodoUseCaseCommand(id)); 64 | this.fetchTodos(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture 2 | 3 | Simple TODO App showcasing a clean architecture approach in VueJs. 4 | 5 | Technologies used: 6 | 7 | - **VueJs** with **TypeScript** 8 | - **Vite** as Buildtool 9 | - **Vuex** for state management with **Vuex Module Decorators** 10 | - **Inversify** for dependency injection 11 | - **Tailwindcss** for a pretty and simple UI 12 | - **PurifyTs** for usage of Either in Usecases 13 | 14 | This project is seperated into 3 layers. 15 | 16 | The package structure is **feature driven**. 17 | This means that for every feature, one package exists. 18 | In each feature, the 3 layers are represented as their own sub-packages. 19 | 20 | In this case, there is only one feature 'todo', which handles creating, reading, updating and deleting todos. 21 | 22 | ``` 23 | ├── core 24 | │ └── app 25 | │ ├── App.vue 26 | │ ├── components 27 | │ ├── store 28 | │ └── domain 29 | │ ├── failure.ts 30 | │ ├── usecase.ts 31 | │ └── infrastructure 32 | ├── features 33 | │ └── foofeature 34 | │ └── application 35 | │ └── domain 36 | │ ├── model 37 | │ ├── port 38 | │ ├── usecase 39 | │ └── infrastructure 40 | │ ├── entity 41 | ``` 42 | 43 | ## App 44 | 45 | The entrypoint, and most outer layer for the application. 46 | Includes all Vue specific code, such as .vue files, vuex configuration and their modules. 47 | 48 | The entrypoint in a SPA is in this case the App.vue, which resides in the **core/app** package. 49 | 50 | ### Store 51 | 52 | State-Management is handled by vuex, which in turn handles the communication to the domain, aka the usecases. 53 | Each feature is represented by its own module, therefore for the Todo Feature, a todoModule.ts exists. 54 | 55 | ## Domain 56 | 57 | The inner layer containing all services to handle business logic, and business rules. 58 | 59 | The layer in itself is seperated into **model**, **ports** and **usecases**. 60 | 61 | ### Model 62 | 63 | The representation of data retrieved in the ports, and used in the usecases. 64 | 65 | ### Port 66 | 67 | Or outer ports, are the port interfaces to communicate with the outside world (infrastructure). 68 | (Dependency inversion rule) 69 | 70 | ### Usecases 71 | 72 | Or inner ports, defining the interfaces for the business logic, and business rules. 73 | Every usecase is implemented by it's service. 74 | 75 | ## Infrastructure 76 | 77 | Framework specific code, and or implementation of the outer ports of the domain, to access the data of the "outer world". 78 | Most prominent example would be the layer to access some external server via http/s. 79 | -------------------------------------------------------------------------------- /src/features/todo/app/components/TodoBuilder.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 82 | 83 | 90 | --------------------------------------------------------------------------------