├── 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 |
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 | 
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 |
--------------------------------------------------------------------------------