Skip to content

Full-Stack FastAPI + React Tutorial 2025 - Build Modern Web Apps

Learn to build a complete full-stack web application using FastAPI (Python) and React (TypeScript) with modern tooling. This tutorial covers everything from project setup to API integration, using industry-standard tools and best practices.

What You'll Learn

By completing this tutorial, you'll master:

  • FastAPI backend setup with Poetry dependency management
  • React frontend development with TypeScript and Vite
  • API integration between backend and frontend
  • Modern UI development with Material-UI components
  • Development workflow with hot reloading and proxy configuration
  • Project structure following industry best practices

Prerequisites

What you need before starting:

  • Python 3.8+ and Node.js 18+ installed
  • Basic knowledge of Python and JavaScript/TypeScript
  • Command line familiarity (terminal/command prompt)
  • Code editor (VS Code recommended)

Time to complete: 45 minutes


What We're Building

You'll create a Task Management Dashboard with:

  • FastAPI backend - REST API with task CRUD operations
  • React frontend - Modern UI with Material-UI components
  • Real-time updates - Live data synchronization
  • TypeScript - Full type safety across the stack
  • Vite - Lightning-fast development experience

Final result preview: A professional dashboard where users can create, view, and manage tasks with a clean Material-UI interface connected to a FastAPI backend.


Step 1: Project Setup and Structure

Create Project Directory

mkdir fullstack-task-app
cd fullstack-task-app
fullstack-task-app/
├── backend/                 # FastAPI application
│   ├── app/
│   │   ├── main.py         # FastAPI app entry point
│   │   ├── models.py       # Data models
│   │   └── routers/        # API route handlers
│   ├── pyproject.toml      # Python dependencies
│   └── poetry.lock
├── frontend/               # React application
│   ├── src/
│   │   ├── components/     # React components
│   │   ├── services/       # API services
│   │   ├── types/          # TypeScript types
│   │   └── App.tsx         # Main app component
│   ├── package.json
│   └── vite.config.ts
└── README.md

This structure separates concerns clearly and follows modern development practices.


Step 2: FastAPI Backend Setup

Create Backend Directory

mkdir backend
cd backend

Initialize Poetry Project

# Install Poetry if not already installed
curl -sSL https://install.python-poetry.org | python3 -

# Initialize new Poetry project
poetry init --no-interaction

Install Dependencies

# Core dependencies
poetry add fastapi uvicorn[standard] pydantic

# Development dependencies  
poetry add --group dev pytest httpx

Create FastAPI Application

Create backend/app/main.py:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
import uuid
from datetime import datetime

app = FastAPI(
    title="Task Management API",
    description="A modern task management API built with FastAPI",
    version="1.0.0"
)

# Configure CORS for React frontend
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],  # Vite default port
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Data models
class TaskCreate(BaseModel):
    title: str
    description: Optional[str] = None

class Task(BaseModel):
    id: str
    title: str
    description: Optional[str] = None
    completed: bool = False
    created_at: datetime

# In-memory storage (use database in production)
tasks_db: List[Task] = []

@app.get("/")
async def root():
    return {"message": "Task Management API", "version": "1.0.0"}

@app.get("/tasks", response_model=List[Task])
async def get_tasks():
    return tasks_db

@app.post("/tasks", response_model=Task)
async def create_task(task: TaskCreate):
    new_task = Task(
        id=str(uuid.uuid4()),
        title=task.title,
        description=task.description,
        created_at=datetime.now()
    )
    tasks_db.append(new_task)
    return new_task

@app.put("/tasks/{task_id}", response_model=Task)
async def update_task(task_id: str, task_update: TaskCreate):
    for task in tasks_db:
        if task.id == task_id:
            task.title = task_update.title
            task.description = task_update.description
            return task
    return {"error": "Task not found"}

@app.delete("/tasks/{task_id}")
async def delete_task(task_id: str):
    global tasks_db
    tasks_db = [task for task in tasks_db if task.id != task_id]
    return {"message": "Task deleted"}

Test Backend Setup

# Run the FastAPI server
poetry run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

Visit http://localhost:8000/docs to see the interactive API documentation.


Step 3: React Frontend Setup

Create React App with Vite

# Navigate back to project root
cd ..

# Create React app with TypeScript
npm create vite@latest frontend -- --template react-ts
cd frontend

Install Dependencies

# Install Material-UI and dependencies
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material

# Install additional utilities
npm install axios @types/axios

# Install development dependencies
npm install --save-dev @types/node

Configure Vite for API Proxy

Update frontend/vite.config.ts:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
})

Create TypeScript Types

Create frontend/src/types/index.ts:

export interface Task {
  id: string;
  title: string;
  description?: string;
  completed: boolean;
  created_at: string;
}

export interface TaskCreate {
  title: string;
  description?: string;
}

Create API Service

Create frontend/src/services/api.ts:

import axios from 'axios';
import { Task, TaskCreate } from '../types';

const API_BASE_URL = '/api';

const api = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

export const taskAPI = {
  // Get all tasks
  getTasks: async (): Promise<Task[]> => {
    const response = await api.get<Task[]>('/tasks');
    return response.data;
  },

  // Create new task
  createTask: async (task: TaskCreate): Promise<Task> => {
    const response = await api.post<Task>('/tasks', task);
    return response.data;
  },

  // Update task
  updateTask: async (id: string, task: TaskCreate): Promise<Task> => {
    const response = await api.put<Task>(`/tasks/${id}`, task);
    return response.data;
  },

  // Delete task
  deleteTask: async (id: string): Promise<void> => {
    await api.delete(`/tasks/${id}`);
  },
};

Step 4: Build the React Interface

Create Task Components

Create frontend/src/components/TaskList.tsx:

import React from 'react';
import {
  List,
  ListItem,
  ListItemText,
  ListItemSecondaryAction,
  IconButton,
  Paper,
  Typography,
  Box,
} from '@mui/material';
import { Delete as DeleteIcon } from '@mui/icons-material';
import { Task } from '../types';

interface TaskListProps {
  tasks: Task[];
  onDeleteTask: (id: string) => void;
}

const TaskList: React.FC<TaskListProps> = ({ tasks, onDeleteTask }) => {
  if (tasks.length === 0) {
    return (
      <Paper sx={{ p: 3, textAlign: 'center' }}>
        <Typography variant="h6" color="text.secondary">
          No tasks yet. Create your first task above!
        </Typography>
      </Paper>
    );
  }

  return (
    <Paper sx={{ mt: 2 }}>
      <List>
        {tasks.map((task) => (
          <ListItem key={task.id} divider>
            <ListItemText
              primary={task.title}
              secondary={
                <Box>
                  {task.description && (
                    <Typography variant="body2" color="text.secondary">
                      {task.description}
                    </Typography>
                  )}
                  <Typography variant="caption" color="text.secondary">
                    Created: {new Date(task.created_at).toLocaleDateString()}
                  </Typography>
                </Box>
              }
            />
            <ListItemSecondaryAction>
              <IconButton
                edge="end"
                aria-label="delete"
                onClick={() => onDeleteTask(task.id)}
                color="error"
              >
                <DeleteIcon />
              </IconButton>
            </ListItemSecondaryAction>
          </ListItem>
        ))}
      </List>
    </Paper>
  );
};

export default TaskList;

Create frontend/src/components/TaskForm.tsx:

import React, { useState } from 'react';
import {
  Paper,
  TextField,
  Button,
  Box,
  Typography,
} from '@mui/material';
import { TaskCreate } from '../types';

interface TaskFormProps {
  onCreateTask: (task: TaskCreate) => void;
  loading?: boolean;
}

const TaskForm: React.FC<TaskFormProps> = ({ onCreateTask, loading = false }) => {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (title.trim()) {
      onCreateTask({
        title: title.trim(),
        description: description.trim() || undefined,
      });
      setTitle('');
      setDescription('');
    }
  };

  return (
    <Paper sx={{ p: 3 }}>
      <Typography variant="h6" gutterBottom>
        Create New Task
      </Typography>
      <Box component="form" onSubmit={handleSubmit}>
        <TextField
          fullWidth
          label="Task Title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          margin="normal"
          required
          disabled={loading}
        />
        <TextField
          fullWidth
          label="Description (optional)"
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          margin="normal"
          multiline
          rows={3}
          disabled={loading}
        />
        <Button
          type="submit"
          variant="contained"
          fullWidth
          sx={{ mt: 2 }}
          disabled={!title.trim() || loading}
        >
          {loading ? 'Creating...' : 'Create Task'}
        </Button>
      </Box>
    </Paper>
  );
};

export default TaskForm;

Update Main App Component

Update frontend/src/App.tsx:

import React, { useState, useEffect } from 'react';
import {
  Container,
  Typography,
  Box,
  Alert,
  CircularProgress,
} from '@mui/material';
import { Task, TaskCreate } from './types';
import { taskAPI } from './services/api';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';

const App: React.FC = () => {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [loading, setLoading] = useState(true);
  const [creating, setCreating] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Load tasks on component mount
  useEffect(() => {
    loadTasks();
  }, []);

  const loadTasks = async () => {
    try {
      setLoading(true);
      const fetchedTasks = await taskAPI.getTasks();
      setTasks(fetchedTasks);
      setError(null);
    } catch (err) {
      setError('Failed to load tasks. Make sure the backend is running.');
      console.error('Error loading tasks:', err);
    } finally {
      setLoading(false);
    }
  };

  const handleCreateTask = async (taskData: TaskCreate) => {
    try {
      setCreating(true);
      const newTask = await taskAPI.createTask(taskData);
      setTasks([...tasks, newTask]);
      setError(null);
    } catch (err) {
      setError('Failed to create task. Please try again.');
      console.error('Error creating task:', err);
    } finally {
      setCreating(false);
    }
  };

  const handleDeleteTask = async (taskId: string) => {
    try {
      await taskAPI.deleteTask(taskId);
      setTasks(tasks.filter(task => task.id !== taskId));
      setError(null);
    } catch (err) {
      setError('Failed to delete task. Please try again.');
      console.error('Error deleting task:', err);
    }
  };

  if (loading) {
    return (
      <Container maxWidth="md" sx={{ mt: 4, textAlign: 'center' }}>
        <CircularProgress />
        <Typography variant="h6" sx={{ mt: 2 }}>
          Loading tasks...
        </Typography>
      </Container>
    );
  }

  return (
    <Container maxWidth="md" sx={{ mt: 4, mb: 4 }}>
      <Box sx={{ mb: 4 }}>
        <Typography variant="h3" component="h1" gutterBottom>
          Task Management Dashboard
        </Typography>
        <Typography variant="subtitle1" color="text.secondary">
          Full-stack application built with FastAPI and React
        </Typography>
      </Box>

      {error && (
        <Alert severity="error" sx={{ mb: 2 }}>
          {error}
        </Alert>
      )}

      <TaskForm onCreateTask={handleCreateTask} loading={creating} />

      <TaskList tasks={tasks} onDeleteTask={handleDeleteTask} />
    </Container>
  );
};

export default App;

Step 5: Run the Full-Stack Application

Start Both Servers

Terminal 1 - Backend:

cd backend
poetry run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

Terminal 2 - Frontend:

cd frontend
npm run dev

Test the Application

  1. Open your browser to http://localhost:5173
  2. Create tasks using the form
  3. View tasks in the list below
  4. Delete tasks using the delete button
  5. Check API docs at http://localhost:8000/docs

Step 6: Production Considerations

Environment Variables

Create frontend/.env:

VITE_API_URL=http://localhost:8000

Update the API service to use environment variables:

const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';

Build for Production

# Build frontend
cd frontend
npm run build

# Backend is production-ready with uvicorn
cd ../backend
poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000

Troubleshooting

Common Issues & Solutions

CORS Errors: - Ensure CORS middleware is configured in FastAPI - Check that frontend URL matches the allowed origins

API Connection Failed: - Verify both servers are running - Check that ports 8000 (backend) and 5173 (frontend) are available - Confirm Vite proxy configuration is correct

Type Errors: - Ensure TypeScript types match your API responses - Check that all imports are correct

Dependencies Issues:

# Clear node modules and reinstall
rm -rf node_modules package-lock.json
npm install

# Update Poetry dependencies
poetry update


What You've Accomplished

Congratulations! You've built a complete full-stack application featuring:

  • Modern FastAPI backend with automatic API documentation
  • React frontend with TypeScript and Material-UI
  • Real-time data synchronization between frontend and backend
  • Professional project structure following industry standards
  • Development workflow with hot reloading and proxy configuration
  • Production-ready setup with environment configuration

Next Steps

Immediate Enhancements:

  1. Add database integration - Replace in-memory storage with PostgreSQL
  2. Implement authentication - Add user registration and login
  3. Add task editing - Allow users to modify existing tasks
  4. Include task status - Add completed/pending states
  5. Add search and filtering - Enhance task management capabilities

Advanced Features:

  1. Real-time updates with WebSockets
  2. File upload functionality for task attachments
  3. Email notifications for task reminders
  4. API rate limiting and security enhancements
  5. Automated testing for both frontend and backend

Deployment Options:

  • Frontend: Vercel, Netlify, or AWS S3
  • Backend: Railway, Heroku, or AWS EC2
  • Database: PostgreSQL on Railway or AWS RDS

Pro Tips for Full-Stack Development

Development Best Practices:

  1. Use TypeScript everywhere - Catch errors early and improve code quality
  2. Implement proper error handling - Graceful failures improve user experience
  3. Add loading states - Keep users informed during async operations
  4. Follow REST conventions - Consistent API design improves maintainability
  5. Use environment variables - Keep configuration separate from code

Performance Optimization:

  1. Implement caching - Reduce API calls with smart caching strategies
  2. Add request debouncing - Prevent excessive API calls from user input
  3. Optimize bundle size - Use dynamic imports and code splitting
  4. Database indexing - Improve query performance with proper indexes

Need a Production-Ready Foundation?

While this tutorial provides a solid foundation, building a production-ready application requires additional features like authentication, database integration, testing, and deployment automation.

Our AI Velocity boilerplate includes everything from this tutorial plus:

  • Advanced authentication system with JWT and OAuth
  • PostgreSQL database integration with migrations
  • Comprehensive testing suite
  • CI/CD pipeline setup
  • Production deployment scripts
  • AI-powered development tools

Ready to build amazing full-stack applications? You now have the foundation to create modern, scalable web applications with FastAPI and React. Start building your next project with confidence!