├── app ├── __init__.py ├── .env ├── config.py ├── schemas.py ├── models.py ├── crud.py ├── database.py └── main.py ├── frontend ├── src │ ├── vite-env.d.ts │ ├── main.tsx │ ├── components │ │ └── ContactList.tsx │ ├── App.css │ ├── assets │ │ └── react.svg │ └── App.tsx ├── tsconfig.json ├── vite.config.ts ├── tsconfig.node.json ├── tsconfig.app.json ├── index.html ├── package.json ├── eslint.config.js ├── public │ └── vite.svg ├── README.md └── package-lock.json ├── requirements.txt ├── README.md └── .gitignore /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /app/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/mydatabase -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.tsx' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from pydantic_settings import BaseSettings 3 | 4 | load_dotenv() 5 | 6 | class Settings(BaseSettings): 7 | database_url: str 8 | 9 | class Config: 10 | env_file = ".env" 11 | 12 | settings = Settings() 13 | 14 | print("Database URL from Settings:", settings.database_url) -------------------------------------------------------------------------------- /app/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | 3 | class ContactCreate(BaseModel): 4 | username: str 5 | email: EmailStr 6 | message: str 7 | 8 | class Contact(BaseModel): 9 | id: int 10 | username: str 11 | email: EmailStr 12 | message: str 13 | 14 | class Config: 15 | from_attributes = True -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | from .database import Base 3 | 4 | class Contact(Base): 5 | __tablename__ = "contacts" 6 | 7 | id = Column(Integer, primary_key=True, index=True) 8 | username = Column(String, index=True) 9 | email = Column(String, unique=True, index=True) 10 | message = Column(String) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.4.0 3 | click==8.1.7 4 | dnspython==2.6.1 5 | email_validator==2.2.0 6 | fastapi==0.112.1 7 | h11==0.14.0 8 | idna==3.7 9 | psycopg2==2.9.9 10 | psycopg2-binary==2.9.9 11 | pydantic==2.8.2 12 | pydantic-settings==2.4.0 13 | pydantic_core==2.20.1 14 | python-dotenv==1.0.1 15 | sniffio==1.3.1 16 | SQLAlchemy==2.0.32 17 | starlette==0.38.2 18 | typing_extensions==4.12.2 19 | uvicorn==0.30.6 20 | -------------------------------------------------------------------------------- /app/crud.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from . import models, schemas 3 | 4 | def create_contact(db: Session, contact: schemas.ContactCreate): 5 | db_contact = models.Contact(**contact.dict()) 6 | db.add(db_contact) 7 | db.commit() 8 | db.refresh(db_contact) 9 | return db_contact 10 | 11 | def get_contacts(db: Session, skip: int = 0, limit: int = 10): 12 | return db.query(models.Contact).offset(skip).limit(limit).all() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Simple To Do App

2 | 3 |

project-image

4 | 5 |

with React(TSX) | FastAPI | PostgreSQL

6 | 7 | 8 | 9 |

💻 Built with

10 | 11 | Technologies used in the project: 12 | 13 | * Python 14 | * FastAPI 15 | * PostgreSQL 16 | * React 17 | * HTML5 18 | * CSS3 19 | -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | from .config import settings 5 | 6 | SQLALCHEMY_DATABASE_URL = settings.database_url 7 | 8 | engine = create_engine(SQLALCHEMY_DATABASE_URL) 9 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 10 | Base = declarative_base() 11 | 12 | def get_db(): 13 | db = SessionLocal() 14 | try: 15 | yield db 16 | finally: 17 | db.close() 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.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 | "isolatedModules": true, 13 | "moduleDetection": "force", 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 | } 25 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React.tsx | FastAPI | PostgreSQL 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/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 -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1" 15 | }, 16 | "devDependencies": { 17 | "@eslint/js": "^9.9.0", 18 | "@types/react": "^18.3.3", 19 | "@types/react-dom": "^18.3.0", 20 | "@vitejs/plugin-react-swc": "^3.5.0", 21 | "eslint": "^9.9.0", 22 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 23 | "eslint-plugin-react-refresh": "^0.4.9", 24 | "globals": "^15.9.0", 25 | "typescript": "^5.5.3", 26 | "typescript-eslint": "^8.0.1", 27 | "vite": "^5.4.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /frontend/src/components/ContactList.tsx: -------------------------------------------------------------------------------- 1 | interface Contact { 2 | id: number; 3 | username: string; 4 | email: string; 5 | message: string; 6 | } 7 | 8 | interface ContactListProps { 9 | contacts: Contact[]; 10 | onDelete: (id: number) => void; 11 | } 12 | 13 | function ContactList({ contacts, onDelete }: ContactListProps) { 14 | return ( 15 |
16 |

Contact Lists

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {contacts.map((contact) => ( 29 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | ))} 42 | 43 |
IDNameEmailMessageDelete
{contact.id}{contact.username}{contact.email}{contact.message} 35 | onDelete(contact.id)} 37 | className="fa-solid fa-trash" 38 | > 39 |
44 |
45 | ); 46 | } 47 | 48 | export default ContactList; 49 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi.middleware.cors import CORSMiddleware 2 | from fastapi import FastAPI, Depends, HTTPException 3 | from sqlalchemy.orm import Session 4 | from . import crud, models, schemas 5 | from .database import engine, get_db 6 | 7 | models.Base.metadata.create_all(bind=engine) 8 | 9 | app = FastAPI() 10 | 11 | 12 | # CORS confg 13 | app.add_middleware( 14 | CORSMiddleware, 15 | allow_origins=["*"], 16 | allow_credentials=True, 17 | allow_methods=["*"], 18 | allow_headers=["*"], 19 | ) 20 | 21 | @app.post("/contacts/", response_model=schemas.Contact) 22 | def create_contact(contact: schemas.ContactCreate, db: Session = Depends(get_db)): 23 | return crud.create_contact(db=db, contact=contact) 24 | 25 | @app.get("/contacts/", response_model=list[schemas.Contact]) 26 | def read_contacts(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): 27 | contacts = crud.get_contacts(db, skip=skip, limit=limit) 28 | return contacts 29 | 30 | @app.delete("/contacts/{contact_id}", response_model=schemas.Contact) 31 | def delete_contact(contact_id: int, db: Session = Depends(get_db)): 32 | contact = db.query(models.Contact).filter(models.Contact.id == contact_id).first() 33 | if contact is None: 34 | raise HTTPException(status_code=404, detail="Contact not found") 35 | db.delete(contact) 36 | db.commit() 37 | return contact -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | text-decoration: none; 8 | list-style: none; 9 | font-family: "Montserrat", sans-serif; 10 | } 11 | 12 | i { 13 | cursor: pointer; 14 | color: red; 15 | } 16 | 17 | .App { 18 | width: 1400px; 19 | max-width: 95%; 20 | margin: 0 auto; 21 | text-align: center; 22 | font-size: 16px; 23 | display: flex; 24 | justify-content: space-between; 25 | align-items: flex-start; 26 | } 27 | 28 | .App h2 { 29 | margin: 20px 0px; 30 | } 31 | 32 | .contact__form label { 33 | width: 100px; 34 | text-align: right; 35 | margin-right: 10px; 36 | } 37 | 38 | .contact__form div { 39 | margin: 10px; 40 | font-size: 20px; 41 | } 42 | 43 | .contact__form input[type="text"], 44 | .contact__form input[type="email"], 45 | .contact__form textarea { 46 | width: 400px; 47 | border-radius: 8px; 48 | border: 1px solid black; 49 | padding: 10px; 50 | } 51 | 52 | .contact__form textarea { 53 | resize: none; 54 | height: 150px; 55 | } 56 | 57 | .contact__form div { 58 | display: flex; 59 | justify-content: center; 60 | align-items: top; 61 | gap: 10px; 62 | } 63 | 64 | .contact__form div.button__container { 65 | display: flex; 66 | justify-content: flex-end; 67 | } 68 | 69 | .contact__form button { 70 | padding: 10px 20px; 71 | background-color: black; 72 | color: white; 73 | border: none; 74 | border-radius: 8px; 75 | cursor: pointer; 76 | font-size: 20px; 77 | } 78 | 79 | /* Contact Table */ 80 | .contact__table { 81 | margin: 0 auto; 82 | } 83 | 84 | .contact__table th, 85 | td { 86 | border: 1px solid black; 87 | padding: 10px; 88 | } -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Django # 2 | *.log 3 | *.pot 4 | *.pyc 5 | __pycache__ 6 | db.sqlite3 7 | media 8 | 9 | # Backup files # 10 | *.bak 11 | 12 | # If you are using PyCharm # 13 | # User-specific stuff 14 | .idea/**/workspace.xml 15 | .idea/**/tasks.xml 16 | .idea/**/usage.statistics.xml 17 | .idea/**/dictionaries 18 | .idea/**/shelf 19 | 20 | # AWS User-specific 21 | .idea/**/aws.xml 22 | 23 | # Generated files 24 | .idea/**/contentModel.xml 25 | 26 | # Sensitive or high-churn files 27 | .idea/**/dataSources/ 28 | .idea/**/dataSources.ids 29 | .idea/**/dataSources.local.xml 30 | .idea/**/sqlDataSources.xml 31 | .idea/**/dynamic.xml 32 | .idea/**/uiDesigner.xml 33 | .idea/**/dbnavigator.xml 34 | 35 | # Gradle 36 | .idea/**/gradle.xml 37 | .idea/**/libraries 38 | 39 | # File-based project format 40 | *.iws 41 | 42 | # IntelliJ 43 | out/ 44 | 45 | # JIRA plugin 46 | atlassian-ide-plugin.xml 47 | 48 | # Python # 49 | *.py[cod] 50 | *$py.class 51 | 52 | # Distribution / packaging 53 | .Python build/ 54 | develop-eggs/ 55 | dist/ 56 | downloads/ 57 | eggs/ 58 | .eggs/ 59 | lib/ 60 | lib64/ 61 | parts/ 62 | sdist/ 63 | var/ 64 | wheels/ 65 | *.egg-info/ 66 | .installed.cfg 67 | *.egg 68 | *.manifest 69 | *.spec 70 | 71 | # Installer logs 72 | pip-log.txt 73 | pip-delete-this-directory.txt 74 | 75 | # Unit test / coverage reports 76 | htmlcov/ 77 | .tox/ 78 | .coverage 79 | .coverage.* 80 | .cache 81 | .pytest_cache/ 82 | nosetests.xml 83 | coverage.xml 84 | *.cover 85 | .hypothesis/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # celery 94 | celerybeat-schedule.* 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | 113 | # Sublime Text # 114 | *.tmlanguage.cache 115 | *.tmPreferences.cache 116 | *.stTheme.cache 117 | *.sublime-workspace 118 | *.sublime-project 119 | 120 | # sftp configuration file 121 | sftp-config.json 122 | 123 | # Package control specific files Package 124 | Control.last-run 125 | Control.ca-list 126 | Control.ca-bundle 127 | Control.system-ca-bundle 128 | GitHub.sublime-settings 129 | 130 | # Visual Studio Code # 131 | .vscode/* 132 | !.vscode/settings.json 133 | !.vscode/tasks.json 134 | !.vscode/launch.json 135 | !.vscode/extensions.json 136 | .history 137 | 138 | # Logs 139 | logs 140 | *.log 141 | npm-debug.log* 142 | yarn-debug.log* 143 | yarn-error.log* 144 | pnpm-debug.log* 145 | lerna-debug.log* 146 | 147 | node_modules 148 | dist 149 | dist-ssr 150 | *.local 151 | 152 | # Editor directories and files 153 | .vscode/* 154 | !.vscode/extensions.json 155 | .idea 156 | .DS_Store 157 | *.suo 158 | *.ntvs* 159 | *.njsproj 160 | *.sln 161 | *.sw? 162 | -------------------------------------------------------------------------------- /frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import "./App.css"; 3 | import ContactList from "./components/ContactList"; 4 | 5 | interface Contact { 6 | id: number; 7 | username: string; 8 | email: string; 9 | message: string; 10 | } 11 | 12 | function App() { 13 | const [contacts, setContacts] = useState([]); 14 | 15 | const fetchContacts = async () => { 16 | try { 17 | const response = await fetch("http://127.0.0.1:8000/contacts/"); 18 | if (response.ok) { 19 | const data = await response.json(); 20 | setContacts(data); 21 | } else { 22 | console.error("An error occurred while retrieving data"); 23 | } 24 | } catch (error) { 25 | console.error("An error occurred:", error); 26 | } 27 | }; 28 | 29 | const handleAddContact = async (newContact: Omit) => { 30 | try { 31 | const response = await fetch("http://127.0.0.1:8000/contacts/", { 32 | method: "POST", 33 | headers: { 34 | "Content-Type": "application/json", 35 | }, 36 | body: JSON.stringify(newContact), 37 | }); 38 | 39 | if (response.ok) { 40 | alert("Message sent successfully!"); 41 | fetchContacts(); 42 | } else { 43 | alert("Message could not be sent. Please try again."); 44 | } 45 | } catch (error) { 46 | console.error("Error:", error); 47 | alert("An error has occurred. Please try again."); 48 | } 49 | }; 50 | 51 | useEffect(() => { 52 | fetchContacts(); 53 | }, []); 54 | 55 | const handleDeleteContact = async (id: number) => { 56 | const contactToDelete = contacts.find((contact) => contact.id === id); 57 | 58 | if (!contactToDelete) { 59 | alert("Contact not found!"); 60 | return; 61 | } 62 | 63 | try { 64 | const response = await fetch(`http://127.0.0.1:8000/contacts/${id}`, { 65 | method: "DELETE", 66 | }); 67 | 68 | if (response.ok) { 69 | alert(`${contactToDelete.username} named user successfully deleted!`); 70 | fetchContacts(); 71 | } else { 72 | alert( 73 | `${contactToDelete.username} named user could not be deleted. Please try again.` 74 | ); 75 | } 76 | } catch (error) { 77 | console.error("An error occurred:", error); 78 | alert("An error occurred. Please try again."); 79 | } 80 | }; 81 | 82 | return ( 83 |
84 |
85 |

Contact Form

86 |
{ 89 | e.preventDefault(); 90 | const formData = new FormData(e.target as HTMLFormElement); 91 | const newContact = { 92 | username: formData.get("name") as string, 93 | email: formData.get("email") as string, 94 | message: formData.get("message") as string, 95 | }; 96 | handleAddContact(newContact); 97 | e.currentTarget.reset(); // clear form 98 | }} 99 | > 100 |
101 | 102 | 103 |
104 |
105 | 106 | 107 |
108 |
109 | 110 |