├── src ├── domain │ └── TODO.ts ├── ui │ ├── vite-env.d.ts │ ├── contexts │ │ └── carContext.ts │ ├── pages │ │ ├── Home.tsx │ │ └── Car.tsx │ ├── components │ │ ├── CarDetails.tsx │ │ └── CarList.tsx │ ├── main.tsx │ ├── routes │ │ └── router.tsx │ ├── App.css │ ├── App.tsx │ ├── hooks │ │ └── useCars.ts │ ├── index.css │ └── assets │ │ └── react.svg ├── shared │ └── interfaces │ │ └── Car.tsx ├── application │ ├── hello.ts │ └── reducers │ │ └── carsReducer.ts └── infrastructure │ └── repositories │ └── carsRepository.ts ├── public ├── ddd_layers.webp └── vite.svg ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── index.html ├── .eslintrc.cjs ├── tsconfig.json ├── package.json └── README.md /src/domain/TODO.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/ddd_layers.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juliomatcom/react-vite-typescript-ddd-hexagonal/main/public/ddd_layers.webp -------------------------------------------------------------------------------- /src/shared/interfaces/Car.tsx: -------------------------------------------------------------------------------- 1 | export interface Car { 2 | id: string 3 | name: string 4 | model?: string 5 | description?: string 6 | } 7 | -------------------------------------------------------------------------------- /src/application/hello.ts: -------------------------------------------------------------------------------- 1 | function getSalute(): string { 2 | return "Hello from application!"; 3 | } 4 | 5 | export default { 6 | getSalute, 7 | }; -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/ui/contexts/carContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { Car } from '../../shared/interfaces/Car'; 3 | 4 | const context = createContext({ 5 | loading: false, 6 | cars: [] as Car[], 7 | }) 8 | 9 | export default context -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/ui/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import CarList from "../components/CarList" 2 | 3 | function Home() { 4 | return ( 5 |
6 |

Home

7 |
8 |

Cars

9 | 10 |
11 |
12 | ) 13 | 14 | } 15 | 16 | export default Home -------------------------------------------------------------------------------- /src/ui/components/CarDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Car } from "../../shared/interfaces/Car" 2 | 3 | function CarDetails({ car }: { car: Car }) { 4 | return ( 5 |
6 |

{car.name}

7 |

{car.model}

8 |

{car.description}

9 |
10 | ) 11 | } 12 | 13 | export default CarDetails -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/ui/main.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { RouterProvider } from "react-router-dom"; 4 | 5 | import './index.css' 6 | import router from './routes/router'; 7 | 8 | 9 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 10 | // 11 | 12 | // , 13 | ) 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/ui/routes/router.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { createBrowserRouter} from "react-router-dom" 3 | import App from '../App' 4 | import Home from "../pages/Home" 5 | import Car from "../pages/Car" 6 | 7 | const router = createBrowserRouter([ 8 | { 9 | path: "/", 10 | element: , 11 | children: [ 12 | { 13 | path: "/", 14 | element: , 15 | }, 16 | { 17 | path: "/cars/:id", 18 | element: , 19 | }, 20 | ], 21 | }, 22 | ]) 23 | 24 | export default router 25 | -------------------------------------------------------------------------------- /src/ui/pages/Car.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import carContext from '../contexts/carContext' 3 | import { useParams } from 'react-router-dom' 4 | import CarDetails from '../components/CarDetails' 5 | 6 | function Cars() { 7 | const { cars } = useContext(carContext) 8 | const { id } = useParams() 9 | 10 | const car = cars.find(car => car.id === id) 11 | 12 | if (!car) return

Ups, no car found...

13 | 14 | return
15 | 16 |
17 | } 18 | 19 | export default Cars -------------------------------------------------------------------------------- /src/ui/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .car-page { 9 | background-color: bisque; 10 | } 11 | 12 | @keyframes logo-spin { 13 | from { 14 | transform: rotate(0deg); 15 | } 16 | to { 17 | transform: rotate(360deg); 18 | } 19 | } 20 | 21 | @media (prefers-reduced-motion: no-preference) { 22 | a:nth-of-type(2) .logo { 23 | animation: logo-spin infinite 20s linear; 24 | } 25 | } 26 | 27 | .card { 28 | padding: 2em; 29 | } 30 | 31 | .read-the-docs { 32 | color: #888; 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/ui/App.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom' 2 | import './App.css' 3 | import hello from '../application/hello' 4 | import CarContext from './contexts/carContext'; 5 | import useCars from './hooks/useCars'; 6 | 7 | function App() { 8 | const { carsState } = useCars(); 9 | 10 | return ( 11 | 12 | <> 13 |
14 |
15 | {hello.getSalute()} 16 |
17 |
18 | 19 |
20 |
21 | footer 22 |
23 |
24 | 25 |
26 | ) 27 | } 28 | 29 | export default App 30 | -------------------------------------------------------------------------------- /src/ui/components/CarList.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom" 2 | import { useContext } from "react" 3 | import carContext from '../contexts/carContext'; 4 | import { Car } from "../../shared/interfaces/Car" 5 | 6 | function CarList() { 7 | const { cars, loading } = useContext(carContext); 8 | 9 | if (loading) { 10 | return

Loading...

11 | } 12 | if (!cars?.length) { 13 | return

Ups, no cars found...

14 | } 15 | 16 | return ( 17 |
    18 | {cars.map((car: Car) => ( 19 |
  • 20 | 21 | {car.name} 22 | 23 |
  • 24 | ))} 25 |
26 | ) 27 | } 28 | 29 | export default CarList -------------------------------------------------------------------------------- /src/ui/hooks/useCars.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useReducer } from "react"; 2 | import carsReducer from "../../application/reducers/carsReducer"; 3 | import carsRepository from "../../infrastructure/repositories/carsRepository"; 4 | 5 | function useCars() { 6 | const [carsState, dispatch] = useReducer(carsReducer, { cars: [], loading: false}); 7 | 8 | useEffect(() => { 9 | async function getCars() { 10 | dispatch({ type: "SET_LOADING", payload: true }) 11 | const cars = await carsRepository.getAll() 12 | dispatch({ type: "ADD_CARS", payload: cars }) 13 | dispatch({ type: "SET_LOADING", payload: false }) 14 | } 15 | getCars() 16 | }, []) 17 | 18 | return { carsState, dispatch }; 19 | } 20 | 21 | export default useCars; -------------------------------------------------------------------------------- /src/infrastructure/repositories/carsRepository.ts: -------------------------------------------------------------------------------- 1 | import { Car } from "../../shared/interfaces/Car" 2 | 3 | const cars = [ 4 | { 5 | id: '1', 6 | name: 'Fusca', 7 | model: 'VW', 8 | description: 'Fusca 1972', 9 | }, 10 | { 11 | id: '2', 12 | name: 'Gol', 13 | model: 'VW', 14 | description: 'Gol 2002', 15 | }, 16 | { 17 | id: '3', 18 | name: 'Uno', 19 | model: 'Fiat', 20 | description: 'Uno 2000', 21 | }, 22 | ] 23 | 24 | const carsRepository = { 25 | getAll: () : Promise => new Promise((resolve) => setTimeout(() => resolve(cars), 2000)), 26 | getById: (id: string): Promise => new Promise((resolve) => setTimeout(() => resolve(cars.find((car) => car.id === id)), 2000)) 27 | } 28 | 29 | export default carsRepository -------------------------------------------------------------------------------- /src/application/reducers/carsReducer.ts: -------------------------------------------------------------------------------- 1 | import { Car } from "../../shared/interfaces/Car"; 2 | 3 | const initialState = { 4 | loading: false, 5 | cars: [] as Car[], 6 | } 7 | 8 | interface actionType { 9 | type: string, 10 | payload: any 11 | } 12 | 13 | function carsReducer(state = initialState, action: actionType) { 14 | switch (action.type) { 15 | case 'ADD_CARS': 16 | return { 17 | ...state, 18 | cars: [...state.cars, ...action.payload], 19 | }; 20 | case 'REMOVE_CAR': 21 | return { 22 | ...state, 23 | cars: state.cars.filter((car) => car.id !== action.payload), 24 | }; 25 | case 'SET_LOADING': 26 | return { 27 | ...state, 28 | loading: action.payload, 29 | }; 30 | default: 31 | return state; 32 | } 33 | } 34 | 35 | export default carsReducer; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-router-dom": "^6.11.2" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.0.37", 19 | "@types/react-dom": "^18.0.11", 20 | "@typescript-eslint/eslint-plugin": "^5.59.0", 21 | "@typescript-eslint/parser": "^5.59.0", 22 | "@vitejs/plugin-react-swc": "^3.0.0", 23 | "eslint": "^8.38.0", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "eslint-plugin-react-refresh": "^0.3.4", 26 | "typescript": "^5.0.2", 27 | "vite": "^4.3.9" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Vite Typescript with DDD & Hexagonal Architecture patterns 2 | This is a React, [vite](https://vitejs.dev/) and typescript project with **DDD and Hexagonal Architecture patterns**. 3 | 4 | The goal of this architecture patterns in front-end is to **decouple different layers** like the domain, application and infrastructure from the UI. 5 | 6 | - The **domain layer** contains the business logic and rules that define the application's behavior. It is the layer that contains the entities, value objects, domain services, etc. 7 | - The **application layer** contains the application's use cases such as services, commands, queries, etc. Usually is the layer that connects the UI with the domain. 8 | - The **infrastructure layer** contains the implementation of the application's interfaces such as http repositories, external services, etc. 9 | - The **UI layer** contains the UI and the framework (React) with all it's dependencies like the application's components, hooks, etc. This layer is the only one that depends on React. 10 | 11 | Dependencies between layers are **inward-facing**, meaning that higher-level layers can only depend on lower-level layers: 12 | 13 | ![](./public/ddd_layers.webp) 14 | 15 | *[Image © hibit.dev](https://www.hibit.dev/posts/15/domain-driven-design-layers)* 16 | 17 | The benefits will be a highly scalable and maintainable application with a **clear separation of concerns** in which you can change layers without affecting the others. 18 | 19 | Note: You probably want to adapt or add others patterns that fit your needs better. This is just an starting point and improve it is encouraged. 20 | 21 | ## What is this not 22 | - A complete project with all the patterns you'll need 23 | - A showcase of OOP, dependency injection (DI) or inversion of control (IoC) 24 | - A showcase of how unit test the different layers 25 | 26 | ## What is this 27 | - A showcase of how to structure and scale React applications using DDD and Hexagonal Architecture patterns 28 | - A showcase of how to use context and useReducer to manage react state 29 | - A showcase of how to use React Router 30 | 31 | ## How to run it 32 | 1. Clone the repository 33 | 2. Run `npm install` 34 | 3. Run `npm run dev` 35 | 36 | -------------------------------------------------------------------------------- /src/ui/assets/react.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------