Building a Modern Web App with Django, Vite & shadcn/ui (Part 1) – Project Setup

Table of Contents

This is the first article in a series on building a modern web application using the following technologies:

The final code of this article available as a branch on https://github.com/tibrahimdev/web-app-django-vite-shadcn/tree/1-initial

The Goals

  • Frontend only (no backend involved yet)
  • A single page on /dashboard path showing a colapsible sidebar
  • Other path will return 404 page
  • Create great UI with the help of shadcn/ui components

Initialize the UI

We will start by scaffolding a new Vite project – following Scaffolding Your First Vite Project. We will use node 22 and pnpm through the entire project

pnpm create vite frontend --template react-ts .

It will print output like this:

◇  Scaffolding project in /home/user/web-app-django-vite-shadcn/frontend...
└  Done. Now run:

  cd frontend
  pnpm install
  pnpm run dev

As mentioned above, let’s move to frontend directory and run that two commands.

  VITE v6.2.2  ready in 183 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

We will have the UI accessible on http://localhost:5173.

Let’s do few things here:

  • Delete frontend/src/App.css
  • Delete frontend/src/App.tsx
  • Edit the frontend/src/main.tsx into an empty page.

Previously, it should looks like this:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Now, let’s change it into this:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
  </StrictMode>,
)

Short explanation:

  • Above code creates a basic React app and displays a webpage.
  • It imports React, ReactDOM, and a CSS file for styling.
  • createRoot(document.getElementById('root')!) finds the <div id="root"> in index.html.
  • StrictMode helps catch common mistakes in development.
  • .render(...) puts whatever content inside it.

The UI will become as simple as this empty page:

Setting Up Routing

Back to our goals, we wanted to have a /dashboard path to be rendered and other URLs to be served using 404 not found page. That goals requires a mechanism called routing. This is very basic, somewhat forgotten, but we will do this as early as possible on our project.

To do that, we will use a popular library called React Router, which state itself as A user‑obsessed, standards‑focused, multi‑strategy router you can deploy anywhere.

There are 3 modes of routing available on React Router. To help developer decide which mode should be taken, React Router has a great documentation on Picking a Mode.

On this project, we will use the Data mode, since it’s allow us to build a more customized and more controls.

React Router Installation

pnpm add react-router

Creating the Router

Start by creating an empty file on frontend/src/router.tsx. Populate it with following content:

// frontend/src/router.tsx
import { createBrowserRouter } from "react-router";

export const router = createBrowserRouter([
  {
    path: "/",
  }
]);

As stated in the documentation, we will use createBrowserRouter which takes an array of route objects that support loaders, actions, error boundaries and more. For now, we have the root path "/" with nothing else.

Let’s render the router by adding <RouterProvider> in the frontend/src/main.tsx:

// frontend/src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import { RouterProvider } from 'react-router'
import { router } from './router'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>,
)

Now, if we save it and try to navigate to http://localhost:5173, the same empty page will be rendered. But, if we try to navigate to http://localhost:5173/dashboard, it will show an error page saying Unexpected Application Error! 404 Not Found and tells use to provide ErrorBoundary or errorElement. We’ll get back to that later. For now, let’s focus on how to show /dashboard first.

Routing to /dashboard

This section shows on how render a correct page for /dashboard and get rid of the 404 not found page.

Let’s start by adding a child on our router:

import { createBrowserRouter } from "react-router";

export const router = createBrowserRouter([
  {
    path: "/",
    children: [
      {
        path: "dashboard",
      }
    ]
  }
]);

We have added a child, which consist of an array of single object, with a path equal to dashboard.

Now, if we go back to our browser, it will show an empty page instead of a 404!

Try to put something else like http://localhost:5173/users and you will get the same 404. But that’s expected. At this point, we have our router working 🎉.

Next, we will working on the user interface 🚀.

Working on the User Interface

Install shadcn/ui

Let’s continue by installing shadcn/ui. This is a beautiful UI component framework which let us to be more productive on building user interface.

shadcn/ui provides a lot of option to install on https://ui.shadcn.com/docs/installation. As we are using vite, we will just follow the instruction on https://ui.shadcn.com/docs/installation/vite.

As we already scaffold our Vite app, we can just continue with the next section on that documentation which is 2. Add Tailwind CSS.

pnpm add tailwindcss @tailwindcss/vite

The documentation says: Replace everything in src/index.css with the following:

// src/index.css
@import "tailwindcss";

Next, we move to 3. Edit tsconfig.json file. Based on shadcn/ui explanation, it says that the current version of Vite splits TypeScript configuration into three files, two of which need to be edited. Add the baseUrl and paths properties to the compilerOptions section of the tsconfig.json and tsconfig.app.json files:

// frontend/tsconfig.json
{
  "files": [],
  "references": [
    {
      "path": "./tsconfig.app.json"
    },
    {
      "path": "./tsconfig.node.json"
    }
  ],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Add this into compilerOptions section of frontend/tsconfig.app.json:

// frontend/tsconfig.app.json
{
  "compilerOptions": {
    // ...
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
    // ...
  }
}

Next, on the 5. Update vite.config.ts. We need to install @types/node then edit vite.config.ts.

pnpm add -D @types/node
// frontend/vite.config.ts
import path from "path"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})

Next, we will run the shadcn init command to setup our project:

pnpm dlx shadcn@latest init
✔ Preflight checks.
✔ Verifying framework. Found Vite.
✔ Validating Tailwind CSS config. Found v4.
✔ Validating import alias.
✔ Which color would you like to use as the base color? › Neutral
✔ Writing components.json.
✔ Checking registry.
✔ Updating src/index.css
✔ Installing dependencies.
✔ Created 1 file:
  - src/lib/utils.ts

Success! Project initialization completed.
You may now add components.

You see, the shadcn/ui understand that we’re using Vite as the framework. We choose Neutral as the color theme. Well you can try other themes as well.

Creating the Sidebar on the Dashboard Page

Let’s get back to one our goals: A single page on /dashboard path showing a colapsible sidebar.

We will leverage the shadcn/ui component called Sidebar.

First, let’s install the component. All shadcn/ui components can be easily installed with the same command pattern.

pnpm dlx shadcn@latest add sidebar

Above command will print out few stuff like these:

✔ Checking registry.
✔ Updating src/index.css
✔ Installing dependencies.
✔ Created 8 files:
  - src/components/ui/sidebar.tsx
  - src/components/ui/button.tsx
  - src/components/ui/separator.tsx
  - src/components/ui/sheet.tsx
  - src/components/ui/tooltip.tsx
  - src/components/ui/input.tsx
  - src/hooks/use-mobile.ts
  - src/components/ui/skeleton.tsx

The command automatically creating 8 components which are the dependencies of the sidebar component. You can check the documentation on each of the component on https://ui.shadcn.com/docs/components/.

One big thing is, the command also update the frontend/src/index.css!

In the past, creating sidebar can be a long job. We’ll see on this section how productive can we be using a wonderful UI framework such as shadcn/ui.

As written in Sidebar documentation, now we need to create a file called frontend/src/app/layout.tsx with following content:

// frontend/src/app/layout.tsx
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
import { AppSidebar } from "@/components/app-sidebar"

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <AppSidebar />
      <main>
        <SidebarTrigger />
        {children}
      </main>
    </SidebarProvider>
  )
}

As you can see, it’s importing another file frontend/src/components/app-sidebar.tsx, which not exist yet. So, let’s create it:

// frontend/src/components/app-sidebar.tsx
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarHeader,
} from "@/components/ui/sidebar"

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarHeader />
      <SidebarContent>
        <SidebarGroup />
        <SidebarGroup />
      </SidebarContent>
      <SidebarFooter />
    </Sidebar>
  )
}

Now, you probably ask, why should we create those 2 files? And where’s our dashboard page?

Relax, now it’s the right time to go back to our long-awaited dashboard – with a few more things to do.

Remember our router? Last time, we defined a route to /dashboard and we get an empty page. Why? Because we haven’t define the component to render the dashboard. Let’s define then.

// frontend/src/router.tsx
import { createBrowserRouter } from "react-router";

export const router = createBrowserRouter([
  {
    path: "/",
    children: [
      {
        path: "dashboard",

        // +++ lazy loading component
        lazy: async () => ({
          Component: (await import("@/pages/dashboard")).default
        }),
      }
    ]
  }
]);

Now, the http://localhost:5173 will show you a scary-error-page saying Failed to resolve import “@/pages/dashboard” from “src/router.tsx”

That is because we haven’t create frontend/src/pages/dashboard/index.tsx. Let’s create it with following content:

// frontend/src/pages/dashboard/index.tsx
import Layout from "@/app/layout";

export default function Page() {
  return (
    <Layout>
      <div>
        <h1>My Dashboard</h1>
      </div>
    </Layout>
  )
}

The simple rule is, the index.tsx must return an exported React component.

Now, let’s head back to our browser and see!

Populate the Sidebar Menu

I believe you’ve stopped smiling now because you realized: There’s no menu on that sidebar.

Let’s add it then. Back to frontend/src/components/app-sidebar.tsx!

 1// frontend/src/components/app-sidebar.tsx
 2import {
 3  Sidebar,
 4  SidebarContent,
 5  SidebarFooter,
 6  SidebarGroup,
 7  SidebarGroupContent,
 8  SidebarGroupLabel,
 9  SidebarHeader,
10  SidebarMenu,
11  SidebarMenuButton,
12  SidebarMenuItem,
13} from "@/components/ui/sidebar"
14import { Calendar, Home, Inbox, Search, Settings } from "lucide-react"
15
16export function AppSidebar() {
17
18  const items = [
19    {
20      title: "Home",
21      url: "#",
22      icon: Home,
23    },
24    {
25      title: "Inbox",
26      url: "#",
27      icon: Inbox,
28    },
29    {
30      title: "Calendar",
31      url: "#",
32      icon: Calendar,
33    },
34    {
35      title: "Search",
36      url: "#",
37      icon: Search,
38    },
39    {
40      title: "Settings",
41      url: "#",
42      icon: Settings,
43      
44    },
45  ]
46
47  return (
48    <Sidebar>
49      <SidebarHeader />
50      <SidebarContent>
51        <SidebarGroup>
52          <SidebarGroupLabel>Application</SidebarGroupLabel>
53          <SidebarGroupContent>
54            <SidebarMenu>
55              {items.map((item) => (
56                <SidebarMenuItem key={item.title}>
57                  <SidebarMenuButton asChild>
58                    <a href={item.url}>
59                      <item.icon />
60                      <span>{item.title}</span>
61                    </a>
62                  </SidebarMenuButton>
63                </SidebarMenuItem>
64              ))}
65            </SidebarMenu>
66          </SidebarGroupContent>
67        </SidebarGroup>
68      </SidebarContent>
69      <SidebarFooter />
70    </Sidebar>
71  )
72}

See what happen now!

Wrap Up

This wraps up the first article in our series on building a modern web application. So far, we’ve:

  • ✅ Set up a frontend server.
  • ✅ Built a /dashboard page with a collapsible sidebar using shadcn/ui components.
  • ✅ Handled unknown routes with a 404 page.

In the next article, we’ll continue expanding our application by adding user authentication. Stay tuned and happy coding! 🚀