Routing in React with React Router and TypeScript – Part 1: Fundamentals with Vite

Learn how to set up React Router with a TypeScript React app created using Vite. We’ll build a small multi-page layout with navigation, 404 page, and a clean folder structure that will scale to real-world apps.

FSI
Full Stack Insights
7 min read...

When you build anything beyond a tiny React widget, routing becomes one of the first decisions you have to get right. URL structure, navigation, layout composition, and data loading all flow from your routing setup.

In this series, we’ll build a realistic React app using React Router and TypeScript, scaffolded with Vite. We’ll go from fundamentals to advanced patterns used in production apps.

In Part 1, we’ll focus on:

  • Creating a Vite + React + TypeScript project
  • Installing and configuring React Router v6+
  • Creating simple pages: Home, About, Contact
  • Building a shared layout with navigation
  • Adding a 404 Not Found route
  • Organizing files so we can grow this into a real-world app in later parts

By the end, you’ll have a clean base that we’ll extend in the next articles.


1. Create a Vite + React + TypeScript project

We’ll use Vite’s official template for React + TypeScript.

From a terminal, run:

# Create the project
npm create vite@latest my-react-router-app -- --template react-ts

cd my-react-router-app

# Install dependencies
npm install

Then start the dev server:

npm run dev

Open the URL printed in the terminal (usually http://localhost:5173) and confirm you see the default Vite + React + TS starter.

At this point, the relevant files are:

  • index.html – single HTML entry point
  • src/main.tsx – React entry that hydrates the app
  • src/App.tsx – main component rendered by main.tsx
  • src/assets – default logo images

We’ll now integrate React Router into this setup.


2. Install and wire up React Router

Install React Router DOM (v6+):

npm install react-router-dom

2.1. Replace App.tsx with a router-based structure

React Router v6 favors route objects and createBrowserRouter / RouterProvider (data APIs) as the core primitives. That’s also the style that scales best to real-world apps, so we’ll use it from the beginning.

Let’s refactor our entry point to use a router.

In src/main.tsx, replace the contents with:

import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

Then create a new file src/router.tsx:

import { createBrowserRouter } from "react-router-dom";
import { AppLayout } from "./routes/AppLayout";
import { HomePage } from "./routes/HomePage";
import { AboutPage } from "./routes/AboutPage";
import { ContactPage } from "./routes/ContactPage";
import { NotFoundPage } from "./routes/NotFoundPage";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <AppLayout />,
    errorElement: <NotFoundPage />,
    children: [
      { index: true, element: <HomePage /> },
      { path: "about", element: <AboutPage /> },
      { path: "contact", element: <ContactPage /> },
    ],
  },
]);

A few important points:

  • We will treat AppLayout as our app shell – the shared layout that wraps child routes.
  • We use index: true for the home route (/), which is the “default” child of the root.
  • We define simple /about and /contact routes.
  • errorElement is a place to render when something goes wrong while loading this route tree. We’ll reuse our NotFoundPage there.

We’re now missing the actual route components. Let’s create them.


3. Create pages and a shared layout

We’ll keep our route components under src/routes. This keeps them close to routing concerns and makes it easy to see what’s navigable in the app.

Create the folder:

mkdir src/routes

3.1. AppLayout – the shared shell

Create src/routes/AppLayout.tsx:

import { Outlet } from "react-router-dom";
import { MainNav } from "../shared/MainNav";

export const AppLayout: React.FC = () => {
  return (
    <div className="app-root">
      <MainNav />
      <main className="app-main">
        <Outlet />
      </main>
    </div>
  );
};

Key ideas:

  • Outlet is where the child routes (Home, About, Contact) will render.
  • MainNav is our navigation bar. We’ll create it in src/shared.

Create src/shared/MainNav.tsx:

import { NavLink } from "react-router-dom";
import "./MainNav.css";

export const MainNav: React.FC = () => {
  return (
    <header className="main-nav">
      <div className="main-nav__brand">My React Router App</div>
      <nav className="main-nav__links">
        <NavLink
          to="/"
          end
          className={({ isActive }) =>
            isActive ? "main-nav__link main-nav__link--active" : "main-nav__link"
          }
        >
          Home
        </NavLink>
        <NavLink
          to="/about"
          className={({ isActive }) =>
            isActive ? "main-nav__link main-nav__link--active" : "main-nav__link"
          }
        >
          About
        </NavLink>
        <NavLink
          to="/contact"
          className={({ isActive }) =>
            isActive ? "main-nav__link main-nav__link--active" : "main-nav__link"
          }
        >
          Contact
        </NavLink>
      </nav>
    </header>
  );
};

This uses NavLink to render links that know whether they’re active so we can style them.

You can add minimal styling in src/shared/MainNav.css:

.main-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1.5rem;
  background: #111827;
  color: #e5e7eb;
}

.main-nav__brand {
  font-weight: 600;
}

.main-nav__links {
  display: flex;
  gap: 1rem;
}

.main-nav__link {
  color: #e5e7eb;
  text-decoration: none;
  padding: 0.25rem 0.5rem;
  border-radius: 0.25rem;
  font-size: 0.95rem;
}

.main-nav__link--active {
  background: #2563eb;
}

And add a few base styles in src/index.css (or keep Vite’s defaults and just add):

.app-root {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.app-main {
  padding: 1.5rem;
}

3.2. Simple page components

Create src/routes/HomePage.tsx:

export const HomePage: React.FC = () => {
  return (
    <section>
      <h1>Welcome</h1>
      <p>
        This is the home page of our React Router + TypeScript application
        created with Vite.
      </p>
      <p>
        In later parts of this series, we&apos;ll evolve this into a
        realistic app with nested routes, data loading, and authentication.
      </p>
    </section>
  );
};

Create src/routes/AboutPage.tsx:

export const AboutPage: React.FC = () => {
  return (
    <section>
      <h1>About</h1>
      <p>
        This demo app is built to showcase React Router best practices with
        TypeScript and Vite.
      </p>
      <p>
        The goal is to keep the structure close to what you&apos;d see in a
        production app while still being small enough to understand.
      </p>
    </section>
  );
};

Create src/routes/ContactPage.tsx:

export const ContactPage: React.FC = () => {
  return (
    <section>
      <h1>Contact</h1>
      <p>
        In a real application, this page could contain a contact form that
        posts to your backend or integrates with a service like SendGrid.
      </p>
      <p>For now, we&apos;ll keep it simple.</p>
    </section>
  );
};

And finally, a NotFoundPage in src/routes/NotFoundPage.tsx:

import { Link } from "react-router-dom";

export const NotFoundPage: React.FC = () => {
  return (
    <section>
      <h1>404 – Page Not Found</h1>
      <p>The page you&apos;re looking for does not exist.</p>
      <p>
        <Link to="/">Go back home</Link>
      </p>
    </section>
  );
};

Now you have a simple multi-page app driven by React Router.


4. TypeScript and best practices from day one

We’ve already been using TypeScript simply by writing .tsx files. To keep things clean as this app grows, here are a few best practices we’ll stick to throughout the series:

  • Use functional components typed with React.FC (or no explicit annotation when inference is enough).
  • Keep routing-related components under src/routes and shared UI under src/shared or src/components.
  • Avoid anonymous inline components inside your route definitions. Instead, import them from their own files. This keeps routes tree readable and components testable.
  • Use NavLink for navigation UI, not Link, when you need active state styling.
  • Keep the route config close to the app entry (src/router.tsx), so it’s easy to see the app’s structure at a glance.

Later parts will introduce real-world concerns: layout nesting, data loading, mutations, and auth—but we won’t have to change this foundation. We’ll just add to it.


5. Quick recap and what’s next

At this point, you should have:

  • A Vite + React + TypeScript project
  • React Router DOM installed and wired up via RouterProvider
  • A basic route tree with /, /about, /contact
  • A shared layout and navigation bar
  • A friendly 404 page

In Part 2, we’ll:

  • Add a /dashboard section with nested routes (e.g. /dashboard/overview, /dashboard/reports)
  • Introduce layout routes for sections of the app
  • Use URL parameters and useParams for more dynamic pages
Share this article:

Related Articles

FSI

Full Stack Insights

Software Engineer

Passionate about software development, architecture, and sharing knowledge with the community.