From Zero to Production: Building Scalable React Applications with TypeScript, React Router, and Authentication.

Setting Up a Production-Grade React Application: A Comprehensive Guide

In this guide, we'll walk through setting up a production-grade React application using the latest technologies and best practices. We'll cover everything from project initialization to advanced features like code splitting and state management.

Prerequisites

Before we begin, make sure you have:

  • Node.js (v18 or higher)
  • npm or yarn
  • A code editor (VS Code recommended)

Project Setup

Let's start by creating a new project using Vite:

npm create vite@latest my-enterprise-app -- --template react-ts
cd my-enterprise-app
npm install

Installing Dependencies

We'll need several packages for our production setup:

npm install @tanstack/react-query @tanstack/react-query-devtools
npm install react-router-dom
npm install zod
npm install axios
npm install @hookform/resolvers
npm install react-hook-form
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools
npm install @emotion/react @emotion/styled
npm install @mui/material @mui/icons-material

Project Structure

Let's organize our project with a clean, scalable structure:

src/
├── api/              # API related code
├── components/       # Reusable components
├── features/         # Feature-based modules
├── hooks/            # Custom hooks
├── layouts/          # Layout components
├── pages/            # Page components
├── store/            # State management
├── types/            # TypeScript types
├── utils/            # Utility functions
└── App.tsx

Configuration Files

TypeScript Configuration (tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

Vite Configuration (vite.config.ts)

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

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  build: {
    chunkSizeWarningLimit: 1600,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom', 'react-router-dom'],
          mui: ['@mui/material', '@mui/icons-material'],
        },
      },
    },
  },
})

Setting Up React Router

Create a router configuration:

// src/router/index.tsx
import { createBrowserRouter } from 'react-router-dom'
import { lazy } from 'react'
import Layout from '@/layouts/Layout'

const Home = lazy(() => import('@/pages/Home'))
const About = lazy(() => import('@/pages/About'))

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
    ],
  },
])

Protected Routes Implementation

For enterprise applications, protecting routes is crucial. Here's how to implement protected routes:

// src/components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom'
import { useAuth } from '@/hooks/useAuth'

interface ProtectedRouteProps {
  children: React.ReactNode
  requiredRoles?: string[]
}

export const ProtectedRoute = ({ children, requiredRoles }: ProtectedRouteProps) => {
  const { isAuthenticated, user, isLoading } = useAuth()
  const location = useLocation()

  if (isLoading) {
    return <div>Loading...</div> // Or your loading component
  }

  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />
  }

  if (requiredRoles && !requiredRoles.some(role => user?.roles?.includes(role))) {
    return <Navigate to="/unauthorized" replace />
  }

  return <>{children}</>
}

Create an authentication hook:

// src/context/AuthContext.tsx
import { createContext, useContext, useReducer, ReactNode, useEffect } from 'react'

interface User {
  id: string
  email: string
  roles: string[]
}

interface AuthState {
  user: User | null
  isAuthenticated: boolean
  isLoading: boolean
}

type AuthAction = 
  | { type: 'LOGIN'; payload: User }
  | { type: 'LOGOUT' }
  | { type: 'LOADING' }

const initialState: AuthState = {
  user: null,
  isAuthenticated: false,
  isLoading: true,
}

const authReducer = (state: AuthState, action: AuthAction): AuthState => {
  switch (action.type) {
    case 'LOGIN':
      return {
        user: action.payload,
        isAuthenticated: true,
        isLoading: false,
      }
    case 'LOGOUT':
      return {
        user: null,
        isAuthenticated: false,
        isLoading: false,
      }
    case 'LOADING':
      return {
        ...state,
        isLoading: true,
      }
    default:
      return state
  }
}

interface AuthContextType {
  state: AuthState
  login: (user: User) => void
  logout: () => void
}

const AuthContext = createContext<AuthContextType | null>(null)

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [state, dispatch] = useReducer(authReducer, initialState)

  useEffect(() => {
    // Check for stored user data on mount
    const storedUser = localStorage.getItem('user')
    if (storedUser) {
      dispatch({ type: 'LOGIN', payload: JSON.parse(storedUser) })
    } else {
      dispatch({ type: 'LOGOUT' })
    }
  }, [])

  const login = (user: User) => {
    localStorage.setItem('user', JSON.stringify(user))
    dispatch({ type: 'LOGIN', payload: user })
  }

  const logout = () => {
    localStorage.removeItem('user')
    dispatch({ type: 'LOGOUT' })
  }

  return (
    <AuthContext.Provider value={{ state, login, logout }}>
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = () => {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

Update the router configuration to include protected routes:

// src/router/index.tsx
import { createBrowserRouter } from 'react-router-dom'
import { lazy } from 'react'
import Layout from '@/layouts/Layout'
import { ProtectedRoute } from '@/components/ProtectedRoute'

const Home = lazy(() => import('@/pages/Home'))
const About = lazy(() => import('@/pages/About'))
const Dashboard = lazy(() => import('@/pages/Dashboard'))
const AdminPanel = lazy(() => import('@/pages/AdminPanel'))
const Login = lazy(() => import('@/pages/Login'))
const Unauthorized = lazy(() => import('@/pages/Unauthorized'))

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'login',
        element: <Login />,
      },
      {
        path: 'unauthorized',
        element: <Unauthorized />,
      },
      {
        path: 'dashboard',
        element: (
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        ),
      },
      {
        path: 'admin',
        element: (
          <ProtectedRoute requiredRoles={['admin']}>
            <AdminPanel />
          </ProtectedRoute>
        ),
      },
    ],
  },
])

Create a login page with authentication:

// src/pages/Login.tsx
import { useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { useAuth } from '@/context/AuthContext'
import { loginSchema } from '@/utils/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import api from '@/api/axios'

export default function Login() {
  const navigate = useNavigate()
  const location = useLocation()
  const { login } = useAuth()
  const [error, setError] = useState('')

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(loginSchema),
  })

  const onSubmit = async (data) => {
    try {
      const response = await api.post('/auth/login', data)
      login(response.data.user)
      const from = location.state?.from?.pathname || '/dashboard'
      navigate(from, { replace: true })
    } catch (err) {
      setError('Invalid credentials')
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Email</label>
        <input {...register('email')} />
        {errors.email && <span>{errors.email.message}</span>}
      </div>
      <div>
        <label>Password</label>
        <input type="password" {...register('password')} />
        {errors.password && <span>{errors.password.message}</span>}
      </div>
      {error && <div>{error}</div>}
      <button type="submit">Login</button>
    </form>
  )
}

This implementation provides:

  1. Route protection based on authentication status
  2. Role-based access control
  3. Persistent authentication state
  4. Protected route redirection
  5. Login form with validation
  6. Secure token handling

Remember to:

  • Store sensitive data (like tokens) securely
  • Implement proper session management
  • Add CSRF protection
  • Use HTTPS in production
  • Implement proper error handling
  • Add rate limiting for login attempts

State Management with React Query

Set up React Query for server state management:

// src/providers/QueryProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      retry: 1,
    },
  },
})

export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

API Setup with Axios

Create a base API configuration:

// src/api/axios.ts
import axios from 'axios'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
})

// Request interceptor
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => Promise.reject(error)
)

// Response interceptor
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Handle unauthorized access
    }
    return Promise.reject(error)
  }
)

export default api

Form Validation with Zod

Create reusable validation schemas:

// src/utils/validation.ts
import { z } from 'zod'

export const loginSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
})

export type LoginFormData = z.infer<typeof loginSchema>

Main Application Setup

Finally, let's set up the main application:

// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { QueryProvider } from '@/providers/QueryProvider'
import { AuthProvider } from '@/context/AuthContext'
import { router } from '@/router'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <QueryProvider>
      <AuthProvider>
        <RouterProvider router={router} />
      </AuthProvider>
    </QueryProvider>
  </React.StrictMode>
)

Best Practices and Tips

  1. Code Splitting: Use React.lazy() and Suspense for route-based code splitting
  2. Error Boundaries: Implement error boundaries at the route level
  3. Performance Monitoring: Set up performance monitoring with tools like Sentry
  4. Testing: Implement unit and integration tests using Jest and React Testing Library
  5. CI/CD: Set up continuous integration and deployment pipelines
  6. Security: Implement proper security measures including CSP headers and XSS protection

Conclusion

This setup provides a solid foundation for building enterprise-grade React applications. The combination of TypeScript, React Query, React Router, and other modern tools ensures a scalable, maintainable, and performant application.

Remember to:

  • Keep your dependencies updated
  • Follow the principle of least privilege
  • Implement proper error handling
  • Use TypeScript's strict mode
  • Follow React's best practices for performance optimization

Happy coding!

About the Author

Hi, I'm Ritesh Singh, a Full Stack Developer and Technical Architect specializing in modern web development. With extensive experience in both frontend and backend technologies, I help businesses build scalable, performant, and maintainable applications from the ground up.

How I Can Help You

If you're looking to:

  • Build a new full-stack application from scratch
  • Optimize your existing application architecture
  • Implement enterprise-grade features
  • Scale your application for growth
  • Improve performance and user experience
  • Set up proper testing and CI/CD pipelines

I can help! I specialize in:

  • Full-stack application architecture and development
  • Frontend development with React and modern frameworks
  • Backend development with Node.js and PHP (Laravel)
  • Database design and optimization
  • Microservices architecture and implementation
  • Cloud infrastructure and deployment
  • Performance tuning and optimization
  • Authentication and security implementation
  • API design and integration

Let's Work Together

Whether you have a new project idea or need help with an existing application, I'm here to help. I can:

  • Design and architect your entire application stack
  • Review your current setup and suggest improvements
  • Implement best practices and modern patterns
  • Optimize performance and scalability
  • Set up proper testing and deployment pipelines
  • Guide your team through complex technical decisions

Feel free to reach out to discuss your project needs. Let's build something great together!

Contact Me | View My Portfolio | Github