Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save tshego3/03752256140ec62ad819452a5e342dad to your computer and use it in GitHub Desktop.

Select an option

Save tshego3/03752256140ec62ad819452a5e342dad to your computer and use it in GitHub Desktop.
This tutorial guides you through building a complete full-stack web application using React (frontend), Express + Prisma (backend), PostgreSQL (database), and Podman (or Docker) for containerization. All instructions are tailored for macOS.

Full-Stack React + Prisma + PostgreSQL with Podman Tutorial

Table of Contents

  1. Prerequisites
  2. Project Structure
  3. Setup Instructions
  4. Backend Development
  5. Frontend Development
  6. Podman Containerization
  7. Running the Application
  8. Running Commands in Containers
  9. Debugging with Podman
  10. Podman PostgreSQL Guide
  11. Troubleshooting
  12. Next Steps
  13. Resources

Prerequisites

Before starting, ensure you have the following installed on your macOS:

  • Node.js (v18 or higher) - Download
  • Docker Desktop - Download
  • Podman - Download
  • Code Editor (VS Code recommended) - Download
  • Terminal (built-in Terminal or iTerm2)

Verify installations:

node --version
npm --version
# docker --version
# docker-compose --version
podman --version
podman compose --version

Project Structure

Click to view project file tree and architecture

Here's the complete file tree for our application:

user-management-app/
├── backend/
│   ├── src/
│   │   ├── repositories/
│   │   │   ├── BaseRepository.ts
│   │   │   └── UserRepository.ts
│   │   ├── routes/
│   │   │   └── users.ts
│   │   ├── types/
│   │   │   └── User.ts
│   │   └── server.ts
│   ├── prisma/
│   │   └── schema.prisma
│   ├── Containerfile.dev
│   ├── Containerfile.prod
│   ├── .containerignore
│   ├── package.json
│   ├── tsconfig.json
│   └── .env
├── frontend/
│   ├── src/
│   │   ├── repositories/
│   │   │   ├── BaseRepository.ts
│   │   │   └── UserRepository.ts
│   │   ├── components/
│   │   │   ├── UserList.tsx
│   │   │   └── UserForm.tsx
│   │   ├── viewmodels/
│   │   │   └── UserManagementViewModel.ts
│   │   ├── types/
│   │   │   └── User.ts
│   │   ├── App.tsx
│   │   └── main.tsx
│   ├── Containerfile.dev
│   ├── Containerfile.prod
│   ├── .containerignore
│   ├── package.json
│   ├── tsconfig.json
│   ├── vite.config.ts
│   ├── index.html
│   └── .env
├── compose.dev.yml
├── compose.prod.yml
└── README.md

This structure follows the Repository Pattern - a design pattern that separates data access logic from business logic, making code more maintainable and testable.


Setup Instructions

Click to view initial project setup steps

Step 1: Create Project Directory

mkdir user-management-app
cd user-management-app

Step 2: Initialize Backend

mkdir -p backend/src/{repositories,routes,types}
mkdir -p backend/prisma
cd backend
npm init -y

Install backend dependencies:

npm install express cors dotenv @prisma/client
npm install -D typescript @types/node @types/express @types/cors ts-node nodemon prisma

Step 3: Initialize Frontend

cd ..
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install

Install Chakra UI and dependencies:

npm install @chakra-ui/react @emotion/react @emotion/styled framer-motion

Chakra UI is a modern React component library that provides accessible, customizable components with built-in theming support.


Backend Development

Click to view configuration and Prisma setup

0. Hot Reloading with Podman

Update backend/package.json:

{
  "scripts": {
    "dev": "nodemon --legacy-watch --exec ts-node src/app.ts",
    ...
  }
}

1. Configure TypeScript

Create backend/tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

2. Define Prisma Schema

Create backend/prisma/schema.prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  role      String   @default("user")
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Prisma is an ORM (Object-Relational Mapping) tool that provides a type-safe way to interact with your database. The schema above defines a User model that will create a corresponding table in PostgreSQL.

3. Environment Configuration

Create backend/.env:

DATABASE_URL="postgresql://devuser:devpassword@postgres:5432/main_db?schema=public"
PORT=3000

In production, never commit .env files! Add them to .gitignore.

Click to view types and repositories (Repository Pattern)

4. User Type Definition

Create backend/src/types/User.ts:

export interface User {
  id: number;
  name: string;
  email: string;
  role: 'user' | 'admin';
  createdAt?: Date;
  updatedAt?: Date;
}

5. Base Repository (Generic)

Create backend/src/repositories/BaseRepository.ts:

import { PrismaClient } from '@prisma/client';

export abstract class BaseRepository<T extends { id: number }> {
  protected prisma: PrismaClient;
  protected modelName: string;

  constructor(prisma: PrismaClient, modelName: string) {
    this.prisma = prisma;
    this.modelName = modelName;
  }

  async getAllAsync(): Promise<T[]> {
    const model = (this.prisma as any)[this.modelName];
    return await model.findMany();
  }

  async getByIdAsync(id: number): Promise<T | null> {
    const model = (this.prisma as any)[this.modelName];
    return await model.findUnique({
      where: { id }
    });
  }

  async createAsync(data: Omit<T, 'id'>): Promise<T> {
    const model = (this.prisma as any)[this.modelName];
    return await model.create({
      data
    });
  }

  async updateAsync(id: number, data: Partial<T>): Promise<T> {
    const model = (this.prisma as any)[this.modelName];
    return await model.update({
      where: { id },
      data
    });
  }

  async deleteAsync(id: number): Promise<void> {
    const model = (this.prisma as any)[this.modelName];
    await model.delete({
      where: { id }
    });
  }
}

6. User Repository (Concrete Implementation)

Create backend/src/repositories/UserRepository.ts:

import { PrismaClient } from '@prisma/client';
import { BaseRepository } from './BaseRepository';
import { User } from '../types/User';

export class UserRepository extends BaseRepository<User> {
  constructor(prisma: PrismaClient) {
    super(prisma, 'user');
  }

  async findByEmailAsync(email: string): Promise<User | null> {
    return await this.prisma.user.findUnique({
      where: { email }
    });
  }

  async findByRoleAsync(role: 'user' | 'admin'): Promise<User[]> {
    return await this.prisma.user.findMany({
      where: { role }
    });
  }

  async promoteToAdminAsync(id: number): Promise<User> {
    return await this.prisma.user.update({
      where: { id },
      data: { role: 'admin' }
    });
  }
}
Click to view API routes and Express server setup

7. API Routes

Create backend/src/routes/users.ts:

import { Router, Request, Response } from 'express';
import { UserRepository } from '../repositories/UserRepository';
import { PrismaClient } from '@prisma/client';

const router = Router();
const prisma = new PrismaClient();
const userRepository = new UserRepository(prisma);

// GET /api/users - Get all users
router.get('/', async (req: Request, res: Response) => {
  try {
    const users = await userRepository.getAllAsync();
    res.json(users);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch users' });
  }
});

// GET /api/users/:id - Get user by ID
router.get('/:id', async (req: Request, res: Response) => {
  try {
    const user = await userRepository.getByIdAsync(Number(req.params.id));
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch user' });
  }
});

// POST /api/users - Create new user
router.post('/', async (req: Request, res: Response) => {
  try {
    const { name, email, role } = req.body;
    const user = await userRepository.createAsync({ name, email, role: role || 'user' });
    res.status(201).json(user);
  } catch (error) {
    res.status(500).json({ error: 'Failed to create user' });
  }
});

// PUT /api/users/:id - Update user
router.put('/:id', async (req: Request, res: Response) => {
  try {
    const user = await userRepository.updateAsync(Number(req.params.id), req.body);
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: 'Failed to update user' });
  }
});

// DELETE /api/users/:id - Delete user
router.delete('/:id', async (req: Request, res: Response) => {
  try {
    await userRepository.deleteAsync(Number(req.params.id));
    res.status(204).send();
  } catch (error) {
    res.status(500).json({ error: 'Failed to delete user' });
  }
});

// POST /api/users/:id/promote - Promote user to admin
router.post('/:id/promote', async (req: Request, res: Response) => {
  try {
    const user = await userRepository.promoteToAdminAsync(Number(req.params.id));
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: 'Failed to promote user' });
  }
});

export default router;

8. Express Server Setup

Create backend/src/server.ts:

import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import userRoutes from './routes/users';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(express.json());

// Routes
app.use('/api/users', userRoutes);

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});
Click to view Prisma commands and Podman Containerfiles

9. Update Backend package.json

Add these scripts to backend/package.json:

{
  "scripts": {
    "dev": "nodemon --legacy-watch --exec ts-node src/app.ts"
    "build": "tsc",
    "start": "node dist/server.js",
    "prisma:generate": "prisma generate",
    "prisma:migrate": "prisma migrate dev",
    "prisma:deploy": "prisma migrate deploy",
    "prisma:push": "prisma db push"
  }
}

10. Database Migration

Required if backend/prisma/schema.prisma was modified.

  1. Capture Changes (Local dev): Required after editing the schema:
    npx prisma migrate dev --name <migration_name>
  2. Apply Changes (Production): Required on the production DB to apply pending migrations:
    npx prisma migrate deploy
  3. Regenerate Client: Required always after schema changes to update types:
    npx prisma generate

Key Prisma Commands:

  • npm run prisma:migrate (prisma migrate dev): Creates a new migration file from your schema changes and applies it. Use this in development.
  • npm run prisma:deploy (prisma migrate deploy): Applies all pending migrations to the database. It does not create new migration files. Use this in production or CI/CD pipelines to safely update your production database.
  • npm run prisma:push (prisma db push): Rapidly syncs your schema with the database without generating migration history. Great for prototyping, but avoid in production.

11. Podman/Docker Environment (Backend)

We use separate configurations for development and production to optimize build times and image sizes.

A. Development Containerfile (backend/Containerfile.dev)

Optimized for developer productivity with hot-reloading.

# THE "HEAVYWEIGHT": Includes everything. Best if your app needs to compile C++ modules.
# FROM node:20 AS build
# THE "LIGHTWEIGHT": Ultra-light and secure. Use this by default for 99% of web apps.
FROM node:20-alpine

WORKDIR /app

# Install dependencies for Prisma
RUN apk add --no-cache openssl libc6-compat

# Copy package files
COPY package*.json ./
COPY prisma ./prisma/

# Install dependencies
RUN npm install

# Generate Prisma Client
RUN npx prisma generate

# Copy source code
COPY . .

# Expose port
EXPOSE 3000

# Start command (Development with hot-reload)
CMD ["sh", "-c", "npm install && npx prisma db push && npm run dev"]

B. Production Containerfile (backend/Containerfile.prod)

Optimized for production with a multi-stage build.

# Stage 1: Build
FROM node:20-alpine AS builder

WORKDIR /app

RUN apk add --no-cache openssl libc6-compat

COPY package*.json ./
COPY prisma ./prisma/

RUN npm install

COPY . .

RUN npx prisma generate
RUN npm run build

# Stage 2: Runtime
FROM node:20-alpine

WORKDIR /app

RUN apk add --no-cache openssl libc6-compat

# Only install production dependencies
COPY package*.json ./
COPY prisma ./prisma/
RUN npm install --omit=dev

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma

EXPOSE 3000

CMD ["sh", "-c", "npm install && npx prisma db push && npm start"]

Create backend/.containerignore (Used by Podman as well):

node_modules
dist
.env

Frontend Development

Click to view configuration, types, and base repository

0. Hot Reloading with Podman

Update frontend/vite.config.ts:

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

// https://vite.dev/config/ 
export default defineConfig({
  plugins: [react(), tailwindcss()],
  server: {
    // Listen on 0.0.0.0
    host: true,
    // Required for Hot Module Replacement (HMR) to work in Podman on macOS
    watch: {
      usePolling: true
    }
  }
})

1. User Type Definition

Create frontend/src/types/User.ts:

export interface User {
  id: number;
  name: string;
  email: string;
  role: 'user' | 'admin';
  createdAt?: string;
  updatedAt?: string;
}

2. Base Repository (Client-Side)

Create frontend/src/repositories/BaseRepository.ts:

// Base generic repository for client-side data fetching
export abstract class BaseRepository<T extends { id: number }> {
  protected baseUrl: string = '';

  async getAllAsync(): Promise<T[]> {
    const response = await fetch(this.baseUrl);
    if (!response.ok) throw new Error(`Failed to fetch items`);
    return response.json();
  }

  async getByIdAsync(id: number): Promise<T> {
    const response = await fetch(`${this.baseUrl}/${id}`);
    if (!response.ok) throw new Error(`Item ${id} not found`);
    return response.json();
  }

  async createAsync(item: Omit<T, 'id'>): Promise<T> {
    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(item)
    });
    if (!response.ok) throw new Error('Failed to create item');
    return response.json();
  }

  async updateAsync(item: T): Promise<T> {
    const response = await fetch(`${this.baseUrl}/${item.id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(item)
    });
    if (!response.ok) throw new Error('Failed to update item');
    return response.json();
  }

  async deleteAsync(id: number): Promise<void> {
    const response = await fetch(`${this.baseUrl}/${id}`, {
      method: 'DELETE'
    });
    if (!response.ok) throw new Error('Failed to delete item');
  }
}
Click to view state management (Repository and ViewModel Patterns)

3. User Repository (Client-Side)

Create frontend/src/repositories/UserRepository.ts:

import { BaseRepository } from './BaseRepository';
import { User } from '../types/User';

export class UserRepository extends BaseRepository<User> {
  constructor() {
    super();
    this.baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000/api/users';
  }

  async findByEmailAsync(email: string): Promise<User | null> {
    const users = await this.getAllAsync();
    return users.find(u => u.email === email) || null;
  }

  async findByRoleAsync(role: 'user' | 'admin'): Promise<User[]> {
    const users = await this.getAllAsync();
    return users.filter(u => u.role === role);
  }

  async promoteToAdminAsync(id: number): Promise<User> {
    const response = await fetch(`${this.baseUrl}/${id}/promote`, {
      method: 'POST'
    });
    if (!response.ok) throw new Error('Failed to promote user');
    return response.json();
  }
}

4. ViewModel Pattern

Create frontend/src/viewmodels/UserManagementViewModel.ts:

import { UserRepository } from '../repositories/UserRepository';
import { User } from '../types/User';

export class UserManagementViewModel {
  private repository: UserRepository;
  private users: User[] = [];
  private isLoading: boolean = false;
  private error: string | null = null;
  private listeners: (() => void)[] = [];

  constructor() {
    this.repository = new UserRepository();
  }

  // Subscribe to state changes
  subscribe(listener: () => void): () => void {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }

  private notify(): void {
    this.listeners.forEach(listener => listener());
  }

  async initializeAsync(): Promise<void> {
    try {
      this.isLoading = true;
      this.notify();
      this.users = await this.repository.getAllAsync();
      this.error = null;
    } catch (err) {
      this.error = err instanceof Error ? err.message : 'Initialization failed';
    } finally {
      this.isLoading = false;
      this.notify();
    }
  }

  async createUserAsync(name: string, email: string): Promise<void> {
    try {
      this.isLoading = true;
      this.notify();
      const newUser = await this.repository.createAsync({
        name,
        email,
        role: 'user'
      });
      this.users.push(newUser);
      this.error = null;
    } catch (err) {
      this.error = err instanceof Error ? err.message : 'Failed to create user';
      throw err;
    } finally {
      this.isLoading = false;
      this.notify();
    }
  }

  async updateUserAsync(user: User): Promise<void> {
    try {
      this.isLoading = true;
      this.notify();
      const updated = await this.repository.updateAsync(user);
      const index = this.users.findIndex(u => u.id === user.id);
      if (index !== -1) {
        this.users[index] = updated;
      }
      this.error = null;
    } catch (err) {
      this.error = err instanceof Error ? err.message : 'Failed to update user';
      throw err;
    } finally {
      this.isLoading = false;
      this.notify();
    }
  }

  async deleteUserAsync(id: number): Promise<void> {
    try {
      this.isLoading = true;
      this.notify();
      await this.repository.deleteAsync(id);
      this.users = this.users.filter(u => u.id !== id);
      this.error = null;
    } catch (err) {
      this.error = err instanceof Error ? err.message : 'Failed to delete user';
      throw err;
    } finally {
      this.isLoading = false;
      this.notify();
    }
  }

  async promoteToAdminAsync(id: number): Promise<void> {
    const user = this.users.find(u => u.id === id);
    if (!user) throw new Error('User not found');

    user.role = 'admin';
    await this.updateUserAsync(user);
  }

  getUsers(): User[] {
    return this.users;
  }

  async getAdmins(): Promise<User[]> {
    return this.repository.findByRoleAsync('admin');
  }

  getError(): string | null {
    return this.error;
  }

  getIsLoading(): boolean {
    return this.isLoading;
  }
}
Click to view React components (Form, List, and Main App)

5. React Components

Create frontend/src/components/UserForm.tsx:

import { useState } from 'react';
import {
  Box,
  FormControl,
  FormLabel,
  Input,
  Button,
  Heading,
  VStack,
} from '@chakra-ui/react';

interface UserFormProps {
  onSubmit: (name: string, email: string) => Promise<void>;
}

export const UserForm: React.FC<UserFormProps> = ({ onSubmit }) => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    try {
      await onSubmit(name, email);
      setName('');
      setEmail('');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <Box
      as="form"
      onSubmit={handleSubmit}
      bg="white"
      p={8}
      borderRadius="xl"
      boxShadow="xl"
    >
      <VStack spacing={6} align="stretch">
        <Heading size="lg" color="purple.600">
          Add New User
        </Heading>

        <FormControl isRequired>
          <FormLabel>Name</FormLabel>
          <Input
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="John Doe"
            size="lg"
          />
        </FormControl>

        <FormControl isRequired>
          <FormLabel>Email</FormLabel>
          <Input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="john@example.com"
            size="lg"
          />
        </FormControl>

        <Button
          type="submit"
          colorScheme="purple"
          size="lg"
          isLoading={isSubmitting}
          loadingText="Adding..."
        >
          Add User
        </Button>
      </VStack>
    </Box>
  );
};

Create frontend/src/components/UserList.tsx:

import {
  Box,
  Table,
  Thead,
  Tbody,
  Tr,
  Th,
  Td,
  Button,
  Badge,
  Heading,
  Text,
  HStack,
  useDisclosure,
  AlertDialog,
  AlertDialogBody,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogContent,
  AlertDialogOverlay,
} from '@chakra-ui/react';
import { useRef, useState } from 'react';
import { User } from '../types/User';

interface UserListProps {
  users: User[];
  onDelete: (id: number) => Promise<void>;
  onPromote: (id: number) => Promise<void>;
}

export const UserList: React.FC<UserListProps> = ({ users, onDelete, onPromote }) => {
  const { isOpen, onOpen, onClose } = useDisclosure();
  const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
  const cancelRef = useRef<HTMLButtonElement>(null);

  const handleDeleteClick = (id: number) => {
    setSelectedUserId(id);
    onOpen();
  };

  const handleConfirmDelete = async () => {
    if (selectedUserId !== null) {
      await onDelete(selectedUserId);
      onClose();
      setSelectedUserId(null);
    }
  };

  return (
    <Box bg="white" p={8} borderRadius="xl" boxShadow="xl">
      <Heading size="lg" mb={6} color="purple.600">
        User Management
      </Heading>

      {users.length === 0 ? (
        <Text color="gray.500" textAlign="center" py={8} fontStyle="italic">
          No users found. Add your first user!
        </Text>
      ) : (
        <Box overflowX="auto">
          <Table variant="simple">
            <Thead>
              <Tr>
                <Th>ID</Th>
                <Th>Name</Th>
                <Th>Email</Th>
                <Th>Role</Th>
                <Th>Actions</Th>
              </Tr>
            </Thead>
            <Tbody>
              {users.map((user) => (
                <Tr key={user.id}>
                  <Td>{user.id}</Td>
                  <Td fontWeight="medium">{user.name}</Td>
                  <Td color="gray.600">{user.email}</Td>
                  <Td>
                    <Badge
                      colorScheme={user.role === 'admin' ? 'pink' : 'blue'}
                      fontSize="sm"
                      px={3}
                      py={1}
                      borderRadius="full"
                    >
                      {user.role.toUpperCase()}
                    </Badge>
                  </Td>
                  <Td>
                    <HStack spacing={2}>
                      {user.role === 'user' && (
                        <Button
                          size="sm"
                          colorScheme="green"
                          onClick={() => onPromote(user.id)}
                        >
                          Promote
                        </Button>
                      )}
                      <Button
                        size="sm"
                        colorScheme="red"
                        onClick={() => handleDeleteClick(user.id)}
                      >
                        Delete
                      </Button>
                    </HStack>
                  </Td>
                </Tr>
              ))}
            </Tbody>
          </Table>
        </Box>
      )}

      <AlertDialog
        isOpen={isOpen}
        leastDestructiveRef={cancelRef}
        onClose={onClose}
      >
        <AlertDialogOverlay>
          <AlertDialogContent>
            <AlertDialogHeader fontSize="lg" fontWeight="bold">
              Delete User
            </AlertDialogHeader>

            <AlertDialogBody>
              Are you sure you want to delete this user? This action cannot be undone.
            </AlertDialogBody>

            <AlertDialogFooter>
              <Button ref={cancelRef} onClick={onClose}>
                Cancel
              </Button>
              <Button colorScheme="red" onClick={handleConfirmDelete} ml={3}>
                Delete
              </Button>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialogOverlay>
      </AlertDialog>
    </Box>
  );
};

6. Main App Component

Create frontend/src/App.tsx:

import { useEffect } from 'react';
import {
  Box,
  Container,
  Heading,
  Text,
  VStack,
  Alert,
  AlertIcon,
  Spinner,
  Center,
  SimpleGrid,
} from '@chakra-ui/react';
import { UserManagementViewModel } from './viewmodels/UserManagementViewModel';
import { UserList } from './components/UserList';
import { UserForm } from './components/UserForm';

const viewModel = new UserManagementViewModel();

function App() {
  const [, setUpdate] = useState(0);

  useEffect(() => {
    const unsubscribe = viewModel.subscribe(() => {
      setUpdate(prev => prev + 1);
    });

    viewModel.initializeAsync();

    return unsubscribe;
  }, []);

  const handleCreateUser = async (name: string, email: string) => {
    await viewModel.createUserAsync(name, email);
  };

  const handleDeleteUser = async (id: number) => {
    await viewModel.deleteUserAsync(id);
  };

  const handlePromoteUser = async (id: number) => {
    await viewModel.promoteToAdminAsync(id);
  };

  const users = viewModel.getUsers();
  const isLoading = viewModel.getIsLoading();
  const error = viewModel.getError();

  return (
    <Box minH="100vh" bgGradient="linear(to-br, purple.600, purple.800)" py={10}>
      <Container maxW="container.xl">
        <VStack spacing={8} align="stretch">
          <Box textAlign="center" color="white">
            <Heading as="h1" size="2xl" mb={2} textShadow="2px 2px 4px rgba(0,0,0,0.2)">
              User Management System
            </Heading>
            <Text fontSize="xl" opacity={0.9}>
              Full-Stack React + Prisma + PostgreSQL
            </Text>
          </Box>

          {error && (
            <Alert status="error" borderRadius="md">
              <AlertIcon />
              {error}
            </Alert>
          )}

          {isLoading && users.length === 0 ? (
            <Center py={20}>
              <VStack>
                <Spinner size="xl" color="white" thickness="4px" />
                <Text color="white">Loading...</Text>
              </VStack>
            </Center>
          ) : (
            <SimpleGrid columns={{ base: 1, md: 3 }} spacing={8}>
              <Box gridColumn={{ md: "span 1" }}>
                <UserForm onSubmit={handleCreateUser} />
              </Box>
              <Box gridColumn={{ md: "span 2" }}>
                <UserList
                   users={users}
                   onDelete={handleDeleteUser}
                   onPromote={handlePromoteUser}
                />
              </Box>
            </SimpleGrid>
          )}
        </VStack>
      </Container>
    </Box>
  );
}

export default App;
Click to view entry point, environment, and Podman Containerfiles

7. Main Entry Point

Update frontend/src/main.tsx to include Chakra Provider:

import React from 'react';
import ReactDOM from 'react-dom/client';
import { ChakraProvider } from '@chakra-ui/react';
import App from './App';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <ChakraProvider>
      <App />
    </ChakraProvider>
  </React.StrictMode>
);

8. Environment Configuration

Create frontend/.env:

VITE_API_URL=http://localhost:3000/api/users

9. Podman/Docker Environment (Frontend)

A. Development Containerfile (frontend/Containerfile.dev)

Uses the Vite development server with hot-reloading.

# THE "HEAVYWEIGHT": Includes everything. Best if your app needs to compile C++ modules.
# FROM node:20 AS build
# THE "LIGHTWEIGHT": Ultra-light and secure. Use this by default for 99% of web apps.
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

# Expose Vite dev port
EXPOSE 5173

CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

B. Production Containerfile (frontend/Containerfile.prod)

Compiles the app and serves it via Nginx.

# Stage 1: Build
FROM node:20-alpine AS build

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

# Stage 2: Production
FROM nginx:alpine

# Copy built assets
COPY --from=build /app/dist /usr/share/nginx/html

# Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Create frontend/nginx.conf:

server {
    listen 80;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api {
        proxy_pass http://backend:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

Create frontend/.containerignore (Used by Podman as well):

node_modules
dist
.env

Podman Containerization

Click to view Podman Compose development and production configurations

Podman Compose Configurations

We use two separate compose files to distinguish between development and production environments.

A. Development Compose (compose.dev.yml)

Includes volume mounts for hot-reloading and development secrets.

services:
  # PostgreSQL
  postgres:
    # THE "HEAVYWEIGHT": Based on Debian. Best for production. Handles different languages and sorting (locales) perfectly.
    # image: postgres:17
    # THE "LIGHTWEIGHT": Based on Alpine. Fast and tiny, but text sorting might behave differently (uses musl instead of glibc).
    image: postgres:17-alpine
    container_name: postgres-db
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: main_db
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U devuser -d main_db" ]
      interval: 10s
      timeout: 5s
      retries: 5

  # Backend API
  backend:
    build:
      context: ./backend
      dockerfile: Containerfile.dev
    container_name: user-api
    environment:
      DATABASE_URL: postgresql://devuser:devpassword@postgres:5432/main_db?schema=public
      PORT: 3000
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - ./backend:/app
      - /app/node_modules

  # Frontend React App
  frontend:
    build:
      context: ./frontend
      dockerfile: Containerfile.dev
    container_name: user-frontend
    environment:
      VITE_API_BASE_URL: http://localhost:3000/api
    ports:
      - "5173:5173"
    depends_on:
      - backend
    volumes:
      - ./frontend:/app
      - /app/node_modules

volumes:
  pgdata:
    driver: local

B. Production Compose (compose.prod.yml)

Optimized for deployment, pulling secrets from environment variables.

services:
  # PostgreSQL
  postgres:
    image: postgres:17-alpine
    container_name: postgres-db-prod
    restart: always
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-produser}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-prodpassword}
      POSTGRES_DB: ${POSTGRES_DB:-prod_db}
    ports:
      - "5432:5432"
    volumes:
      - pgdata_prod:/var/lib/postgresql/data
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}" ]
      interval: 10s
      timeout: 5s
      retries: 5

  # Backend API
  backend:
    build:
      context: ./backend
      dockerfile: Containerfile.prod
    container_name: user-api-prod
    restart: always
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
      PORT: 3000
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy

  # Frontend (Served via Nginx)
  frontend:
    build:
      context: ./frontend
      dockerfile: Containerfile.prod
    container_name: user-frontend-prod
    restart: always
    ports:
      - "80:80"
    depends_on:
      - backend

volumes:
  pgdata_prod:
    driver: local

What does Podman/Docker Compose do?

  • Defines and runs multi-container applications
  • Manages networks between containers
  • Handles startup dependencies
  • Simplifies environment configuration

Running the Application

Click to view commands to start, test, and stop the application

Step 1: Start Podman Machine

Ensure your Podman machine is running (if on macOS):

podman machine start

Step 2: Build and Start Containers

From the project root directory:

For Development:

podman compose -f compose.dev.yml up --build

For Production:

podman compose -f compose.prod.yml up -d --build

The --build flag rebuilds images. After the first build, you can use just podman compose up.

Development Mode

The development configuration maps local directories to containers for hot-reloading. Changes made locally will instantly reflect in the running container.

Manual Image Builds

Backend (Dev):

podman build -t user-api-dev -f backend/Containerfile.dev ./backend

Backend (Prod):

podman build -t user-api-prod -f backend/Containerfile.prod ./backend

Frontend (Dev):

podman build -t user-frontend-dev -f frontend/Containerfile.dev ./frontend

Frontend (Prod):

podman build -t user-frontend-prod -f frontend/Containerfile.prod ./frontend

Environment Variables

The compose.dev.yml file defaults to dev credentials. For compose.prod.yml, the engine will pull values from your shell or a local .env file for:

  • POSTGRES_USER
  • POSTGRES_PASSWORD
  • POSTGRES_DB

Step 3: Access the Application

Step 4: Test the API

Using curl:

# Create a user
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe","email":"john@example.com"}'

# Get all users
curl http://localhost:3000/api/users

# Get user by ID
curl http://localhost:3000/api/users/1

# Update user
curl -X PUT http://localhost:3000/api/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"John Updated","email":"john@example.com","role":"user"}'

# Promote to admin
curl -X POST http://localhost:3000/api/users/1/promote

# Delete user
curl -X DELETE http://localhost:3000/api/users/1

Step 5: Stop the Application

# docker-compose down
podman compose down

To remove volumes (deletes database data):

# docker-compose down -v
podman compose down -v

Running Commands in Containers

Click to view guide for running commands inside containers

Sometimes you need to monitor your containers or execute commands directly inside them.

1. List Running Containers

To see which containers are currently running, use podman ps.

# List only running containers
# docker ps
podman ps

# List ALL containers (including stopped ones)
# docker ps -a
podman ps -a

2. Execute a Command in a Running Container

Use podman exec to run a command in an already running container.

# Syntax: podman exec [options] <container_name_or_id> <command>
podman exec -it <container_name> /bin/sh
  • -i, --interactive: Keep STDIN open even if not attached.
  • -t, --tty: Allocate a pseudo-TTY.
  • -it: Combining these allows you to interact with the container's shell.

3. Common Scenarios

A. Open a Shell in the Backend Container

# docker exec -it backend /bin/sh
podman exec -it backend /bin/sh

B. Open a Shell in the Frontend Container

# docker exec -it frontend /bin/sh
podman exec -it frontend /bin/sh

C. Run Prisma Commands Inside Backend If you need to manually run a Prisma command inside the container:

# docker exec -it backend npx prisma status
podman exec -it backend npx prisma status

D. Access PostgreSQL Directly Connect to the database using psql inside the database container:

# docker exec -it postgres-db psql -U devuser -d main_db
podman exec -it postgres-db psql -U devuser -d main_db

E. Installing a New Package If you've added a package to package.json on your host and need the container to install it immediately without restarting:

# docker exec -it backend npm install
podman exec -it backend npm install

4. Running One-Off Commands (podman run)

If you want to run a command in a new container that stops and removes itself after the command finishes:

# docker run --rm -it alpine echo "Hello World"
podman run --rm -it alpine echo "Hello World"

Debugging with Podman

Click to view debugging guide for Backend and Frontend

Attaching a debugger to code running inside a Podman container allows you to set breakpoints, inspect variables, and step through code just like you would locally.

1. Backend Debugging (Node.js + TypeScript)

To debug the backend, we need to enable the Node.js inspector and expose the debugging port (9229) to your host machine.

A. Update Podman/Docker Compose Configuration

Modify the backend service to expose the debug port and pass the inspector flag via environment variables:

  backend:
    ...
    ports:
      - "3000:3000"
      - "9229:9229" # Node.js Inspector port
    environment:
      - DATABASE_URL=postgresql://devuser:devpassword@postgres:5432/main_db?schema=public
      - NODE_OPTIONS=--inspect=0.0.0.0:9229

B. VS Code Launch Configuration

Create a .vscode/launch.json file in your project root to tell VS Code how to connect:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Podman: Attach to Backend",
      "type": "node",
      "request": "attach",
      "address": "localhost",
      "port": 9229,
      "localRoot": "${workspaceFolder}/backend",
      "remoteRoot": "/app",
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

C. How to Debug

  1. Start your containers: podman compose up.
  2. Open the Run and Debug sidebar in VS Code (Cmd+Shift+D).
  3. Select Podman: Attach to Backend (updated label) and press the Play button (F5).
  4. Set a breakpoint in any src/*.ts file and trigger the API!

2. Frontend Debugging (React + Vite)

Frontend code runs in the browser, so debugging is primarily done there. However, you can still bridge the experience to VS Code.

A. Browser DevTools (Recommended)

Since the frontend is running in Podman and served via Nginx or Vite:

  1. Open http://localhost:3000 in Chrome.
  2. Press F12 or Cmd+Option+I to open DevTools.
  3. Go to the Sources tab.
  4. Press Cmd+P and type the name of your component (e.g., UserList.tsx).
  5. Set breakpoints directly in the browser.

B. VS Code "Attach" to Chrome

To debug React directly inside VS Code:

  1. Add this configuration to your .vscode/launch.json:
{
  "name": "Podman: Debug Frontend (Chrome)",
  "type": "chrome",
  "request": "launch",
  "url": "http://localhost:5173",
  "webRoot": "${workspaceFolder}/frontend/src"
}
  1. Press F5 in VS Code. It will open a new Chrome instance where your breakpoints will be synced with your source code.

Note: For the best development experience with hot-reloading and debugging, ensure you are using a development stage in Podman rather than the production Nginx build shown in the standard Dockerfile.


Podman PostgreSQL Guide

Click to view Podman PostgreSQL guide (Storage, Backups, and Restore)

Podman provides a secure, rootless alternative to Docker. Using Podman for your database ensures your development environment remains clean and consistent.

1. Persistent Storage with Podman Volumes

To ensure your data survives container restarts or deletions, we use Podman Volumes. Volumes are the preferred mechanism for persisting data generated by and used by Podman containers.

2. Specialized Podman Compose

Create or update your compose.yaml (Podman uses the same format) to optimize for ARM64:

# version: '3.8'

services:
  # PostgreSQL Optimized for ARM64 device
  postgres:
    # Using the official arm64v8/postgres image or standard postgres which is multi-arch
    # THE "HEAVYWEIGHT": Based on Debian. Best for production. Handles different languages and sorting (locales) perfectly.
    # image: postgres:17
    # THE "LIGHTWEIGHT": Based on Alpine. Fast and tiny, but text sorting might behave differently (uses musl instead of glibc).
    image: postgres:17-alpine
    container_name: postgres-db
    # restart: always
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: main_db
    ports:
      - "5432:5432"
    volumes:
      # Data persistence: maps local volume to container's data directory
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U devuser -d main_db" ]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pgdata:
    driver: local

3. Initializing the Database

  1. Start Podman: Ensure the Podman machine is running (if on macOS).
  2. Pull and Start:
    # docker compose -f docker-compose.yml up -d
    # podman compose -f compose.yml up -d
    podman compose -f compose.dev.yml up postgres-db
    • -f compose.yml: Tells Podman to use this specific file instead of the default compose.yml.
    • up: Builds, (re)creates, starts, and attaches to containers for a service.
    • -d: Runs the containers in detached mode (in the background).
  3. Verify Growth:
    # docker volume ls
    podman volume ls
    # docker volume inspect pgdata
    podman volume inspect pgdata

4. Database Consumption Examples

A. Connecting with Prisma

Your .env file for Prisma should look like this:

DATABASE_URL="postgresql://devuser:devpassword@localhost:5432/main_db?schema=public"

B. Direct Access via psql (inside container)

# docker exec -it postgres-db psql -U devuser -d main_db
podman exec -it postgres-db psql -U devuser -d main_db

C. Using a Database GUI (e.g., TablePlus, DBeaver)

  • Host: localhost
  • Port: 5432
  • User: devuser
  • Password: devpassword
  • Database: main_db

D. Connecting via VS Code PostgreSQL Extension

If you use the PostgreSQL extension for VS Code, follow these steps to connect:

1. Open the PostgreSQL Panel
  • Click on the PostgreSQL icon in the Activity Bar on the left side of VS Code.
  • In the PostgreSQL explorer, click the + (plus) button to add a new connection.
2. Enter Connection Details

The extension will prompt you for details at the top of the VS Code window. Enter them as follows:

  1. Host Name: localhost
  2. PostgreSQL User: devuser (or as configured in your .env)
  3. PostgreSQL Password: devpassword (or as configured in your .env)
  4. Port: 5432
  5. Connection Method: Standard (unless you specifically configured SSL)
  6. Database Name: main_db
  7. Display Name: main_db (or any name you prefer)
3. Verify Connection
  • Once saved, your database should appear in the PostgreSQL sidebar.
  • You can expand it to see the main_db database, its schemas (likely public), and your tables.

5. Troubleshooting Performance

For Podman, performance is generally superior due to its rootless/daemonless architecture on many systems, but ensure your machine has sufficient resources allocated: podman machine inspect.

6. Database Backup & Restore (macOS)

This section covers how to backup a PostgreSQL database and restore it to another environment (e.g., migrating from staging to local development).

A. Setup: Install Client Tools (Homebrew)

If you don't have Homebrew yet, install it first via brew.sh. Then, run these commands to get the PostgreSQL utilities without the server:

# Install the library containing psql and pg_dump
brew install libpq

# Force link it so the commands are available in your terminal
echo 'export PATH="'$(brew --prefix)'/opt/libpq/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

Verify: Type psql --version. If it returns a version number, you're ready.

B. The Full Backup (Dump)

Connect to your remote/source database and create the plain-text .sql file.

pg_dump -h <source_host> -U <source_user> -d <database> -f backup.sql

Note: When you run this, it will ask for your password. If you want to avoid typing it, you can run export PGPASSWORD='your_password' right before the command, though be mindful of your terminal history security!

C. The "Find & Replace" Owner Fix

This is the most critical step. SQL dumps usually contain OWNER TO <old_user> commands. If your new environment uses a different username, the restore will fail.

Since you are on macOS, we use sed to swap the names.

# Replace 'old_owner' with the user from the original DB
# Replace 'new_owner' with your current environment's username
sed -i '' 's/OWNER TO old_owner/OWNER TO new_owner/g' backup.sql

# Optional: Also replace any specific GRANT permissions if they cause errors
sed -i '' 's/TO old_owner/TO new_owner/g' backup.sql

Tip: Not sure who the old owner was? Open backup.sql in VS Code and search for "OWNER TO". You will see a line like ALTER TABLE public.mytable OWNER TO postgres;. In this case, postgres is your "old_owner."

D. The Restore

Now, push that modified file into your target database.

Step 1: Create the target database (if it doesn't exist)

# Connect to 'postgres' (default db) to create your new one
psql -h <target_host> -U <target_user> -c "CREATE DATABASE <database>;"

Note: When you run this, it will ask for your password. If you want to avoid typing it, you can run export PGPASSWORD='your_password' right before the command, though be mindful of your terminal history security!

Step 2: Run the restore script

# -f: the file we just edited
# --set ON_ERROR_STOP=on: highly recommended so it doesn't ignore failures
psql -h <target_host> -U <target_user> -d <database> --set ON_ERROR_STOP=on -f backup.sql
# pg_restore -h <target_host> -U <target_user> -d <database> --clean --if-exists backup.dump
# Mention: update the production owner to the local db owner (see Section C)

E. Common Maintenance Commands

Command Description
podman machine start Starts the Podman virtual machine (macOS).
podman compose -f compose.dev.yml up Starts all services in the dev environment.
podman compose -f compose.dev.yml up postgres-db Starts only the PostgreSQL database service.
podman compose -f compose.dev.yml build backend --no-cache Rebuilds the backend service without using the cache.
dropdb -h localhost -U devuser "main_db"; Drops (deletes) the local development database.
createdb -h localhost -U devuser "main_db"; Creates a new local development database.
pg_dump -h <prod_host> -U <prod_user> -d <prod_db> -f prod_backup.sql; Dumps the production database to a file.
pg_dump -h localhost -U devuser -d main_db -f local_backup.sql; Dumps the local database to a file.
psql -h localhost -U devuser -d main_db -f backup.sql; Restores a database from a backup file.

F. Summary Cheat Sheet

Command Why use it?
brew install libpq Gets tools without the heavy server background process.
pg_dump ... -f file.sql Creates a human-readable text backup.
sed -i '' 's/.../.../g' The macOS way to batch-edit the "Owner" inside the file.
psql ... -f file.sql The "Player" that reads the script and rebuilds your DB.

Troubleshooting

Click to view common issues and solutions

Here is the updated documentation. I have added the "Clean Slate" method as the primary recommendation for users migrating from Docker Desktop, as it is the most reliable way to clear configuration conflicts.

Podman Compose Setup & Troubleshooting (macOS)

This guide covers the resolution for command not found errors and credential helper conflicts when transitioning from Docker Desktop to Podman.

Resolving "command not found: podman-compose"

Issue: After installing Podman Desktop, the podman-compose command is missing. Cause: Podman Desktop bundles the Podman engine but not the legacy Python script. It uses the official docker-compose binary as an external provider.

Solution: The "Podman Compose" Native Bridge

  1. Remove Conflict: Clear any broken symlinks from previous Docker installs:
sudo rm -f /usr/local/bin/docker-compose
  1. Enable in GUI: * Open Podman Desktop > Settings > Resources.
  • Find Compose and click Setup.
  1. Correct Command: Use the modern syntax (space instead of hyphen):
podman compose up

Resolving Credential Errors (The "Clean Slate" Method)

Issue: Errors like exec: "docker-credential-desktop": executable file not found. Cause: Leftover settings in ~/.docker/config.json point to non-existent Docker Desktop tools.

Solution: Reset Configuration & Re-initialize

This is the most effective way to fix "Ghost" Docker settings:

  1. Delete the old Docker config folder:
rm -rf ~/.docker
  1. Re-initialize via Podman Desktop:
  • Go back to Settings > Resources > Compose.
  • Re-run the Setup/Installation.
  • This forces Podman to create a fresh config.json specifically configured for macOS (osxkeychain).
  1. Verify:
cat ~/.docker/config.json

It should now correctly show "credsStore": "osxkeychain".

Issue: Port Already in Use

Error: Bind for 0.0.0.0:5432 failed: port is already allocated

Solution:

# Find process using the port
lsof -i :5432

# Kill the process
kill -9 <PID>

# Or change the port in compose.yaml (Podman/Docker)
<!-- # Or change the port in docker-compose.yml -->
ports:
  - "5433:5432"

Issue: Prisma Client Not Generated

Error: Cannot find module '@prisma/client'

Solution:

cd backend
npx prisma generate

Issue: Prisma Client Out of Sync (Type Errors)

Error: TypeScript errors like Property 'phoneNumber' does not exist on type 'User' (or any new field you just added) even though the field exists in schema.prisma.

Cause: The generated Prisma Client files are outdated and do not match your current schema.prisma definition. This happens when you modify the schema but haven't regenerated the client library yet.

Solution: Regenerate the Prisma Client to update the type definitions:

cd backend
npx prisma generate

Issue: Prisma Migration Error P3015 (Missing Migration File)

Error: P3015: Could not find the migration file at prisma/migrations/.../migration.sql

Cause: A migration directory exists but is empty, or the migration.sql file is missing. This often happens if a migration creation process was interrupted or failed but the directory was left behind.

Background: When you run npx prisma migrate dev, Prisma compares your schema.prisma with the current database schema. If there are changes, it:

  1. Creates a new directory in prisma/migrations/.
  2. Generates a migration.sql file inside that directory containing the SQL commands to apply the changes.
  3. Applies the SQL to the database. If this process is interrupted (e.g., Ctrl+C), the directory might be created without the SQL file, leading to this error.

Solution:

  1. Delete the empty directory: Remove the faulty migration folder from prisma/migrations.
  2. Reset and Re-apply: Run npx prisma migrate dev.
    • Note: If Prisma detects drift (schema out of sync), it may ask to reset the database.

Zero Data Loss Strategy

CRITICAL RULE: Prisma will ask for permission before resetting the database (...Do you want to continue? All data will be lost.). If you see this prompt and your data is important, type N (No).

1. Production Safety (The "Never Dev" Rule)

  • NEVER run migrate dev in production. It is designed for development and may attempt to reset the database if it detects conflicts.
  • ALWAYS use migrate deploy in production. This command applies pending migrations but will fail safely (exit code 1) instead of resetting if there is a conflict.

2. Development Workflow (Safe Mode)

  • Use Create-Only: If you are unsure about a change, run npx prisma migrate dev --create-only. This generates the SQL file without applying it, letting you inspect the SQL to ensure it handles data correctly (e.g., adding default values for new non-null columns).
  • Backup First: If you must fix a drift or apply a risky change, backup your container data first:
    podman exec -t <container_name> pg_dump -U <username> -d <dbname> > backup.sql

3. Resolving Conflicts Without Reset If migrate dev demands a reset due to "Drift detected":

  1. Say No to the reset.
  2. Introspect the actual database state: npx prisma db pull.
  3. Compare the updated schema.prisma with your previous version to see what changed in the DB manually.
  4. Decide whether to keep the manual changes (add them to your schema) or discard them (manually revert the DB changes).

Issue: Database Connection Failed

Error: Can't reach database server

Solution:

  • Ensure PostgreSQL container is healthy: podman compose ps
  • Check DATABASE_URL environment variable
  • Wait for database to be ready (health check)

Issue: CORS Errors

Error: Access to fetch at 'http://localhost:3000' has been blocked by CORS policy

Solution:

  • Ensure cors() middleware is enabled in backend
  • Check that frontend is making requests to correct URL
  • Rebuild containers: podman compose up --build

Issue: Frontend Can't Connect to Backend

Solution:

  • Verify backend is running: curl http://localhost:3000/health
  • Check VITE_API_URL in frontend/.env
  • Inspect browser console for errors
  • Verify nginx.conf proxy configuration

Issue: New Package Not Recognized (Module Not Found)

Problem: You ran npm install <package> on your Mac, but the application inside Podman fails with Error: Cannot find module '<package>'.

Cause: Depending on your compose.yaml setup, the node_modules folder inside the container may be isolated from your host's node_modules. Even if they are synced, packages installed on macOS might contain binaries that are incompatible with the Linux environment (Alpine) inside the container.

Solution:

  1. Direct Install: Run the install command inside the running container:
    podman exec -it backend npm install
  2. Restart with Build: The most reliable way is to restart the container while forcing a sync:
    podman compose up --build backend
  3. Check Dockerignore: Ensure node_modules is listed in your .containerignore (or .dockerignore) file. This prevents your host's (macOS) modules from being copied into the container during the build process, forcing the container to build its own compatible versions.

Issue: Vite Import Resolution Error (Frontend)

Error: [plugin:vite:import-analysis] Failed to resolve import "@some/package" from "src/components/SomeComponent.tsx". Does the file exist?

Cause: The package exists in your host's package.json but isn't installed in the container's node_modules. This happens because:

  • The compose.yaml uses an anonymous volume for node_modules (e.g., - /app/node_modules) to prevent overwriting with host binaries.
  • The container image was built before the dependency was added to package.json.
  • The container is running with stale cached packages.

Solution: Rebuild the container image to force a fresh npm install with the updated package.json:

# Rebuild and restart all services
podman compose -f compose.dev.yml up --build

# Or rebuild just the frontend service (rather run these commands separately)
podman compose -f compose.dev.yml down frontend && podman compose -f compose.dev.yml build frontend --no-cache && podman compose -f compose.dev.yml up

Tip: Always rebuild the container image after adding new dependencies to package.json.

Issue: Android Emulator Can't Access Localhost

Problem: When running the app in an Android emulator, it cannot connect to http://localhost:5173 or http://localhost:3000.

Cause: The Android emulator runs on its own virtual network. localhost on the emulator refers to the emulator itself, not your host machine (Mac).

Solution:

  1. Enable Host Access in Vite (frontend/vite.config.ts): Configure the dev server to listen on all network interfaces, not just localhost inside the container.

    server: {
      // Listen on 0.0.0.0
      host: true,
      // Required for Hot Module Replacement (HMR) to work in Podman on macOS
      watch: {
        usePolling: true
      }
    }
  2. Restart the Frontend Container:

    podman restart frontend
  3. Bridge Ports with ADB: Use adb reverse to map the emulator's ports to your computer's ports.

    # Map frontend port
    adb reverse tcp:5173 tcp:5173
    
    # Map backend port
    adb reverse tcp:3000 tcp:3000
  4. Verify: Open Chrome in the Android Emulator and browse to http://localhost:5173. It should now load your app.

macOS-Specific: Podman Machine Not Running

Solution:

  1. Open terminal
  2. Start the machine: podman machine start
  3. Verify with: podman ps

Next Steps

Congratulations! You've built a full-stack application. Here are some enhancements to try:

  • Authentication: Add JWT-based authentication
  • Validation: Implement Zod or Yup for data validation
  • Testing: Add Jest/Vitest unit tests
  • Pagination: Implement pagination for large datasets
  • Search: Add search functionality
  • Deployment: Deploy to AWS, Azure, or DigitalOcean

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment