Skip to main content

JOD Board Frontend Folder Structure

Overview

This document outlines the folder structure of JOD Board's frontend application, which is built using React with React Router v7 (a framework merging Remix and React Router). Our frontend application is organized into several key directories, each serving a specific purpose in the application architecture. This structure helps maintain clean code, improve reusability, and make the codebase more maintainable.

Naming Conventions

  1. File Naming: All files should use kebab-case (e.g., form-field.jsx, job-list.jsx)
  2. Component Naming: React components should use PascalCase (e.g., FormField, JobList)
  3. Import Paths: Use absolute imports from the app directory (e.g., import { FormField } from 'components/forms/form-field')

Root Directory Structure

.
├── app/ # Main application code
├── public/ # Static assets
├── Dockerfile # Docker configuration
├── README.md # Project documentation
├── eslint.config.js # ESLint configuration
├── jsconfig.json # JavaScript configuration
├── package.json # Project dependencies
├── react-router.config.js # React Router configuration
└── vite.config.js # Vite configuration

Application Structure (app/)

The app directory contains all the main application code, organized into several key directories:

├── app
│ ├── api
│ ├── components
│ ├── domains
│ ├── layouts
│ ├── root.jsx
│ ├── routes
│ ├── routes.js
│ ├── styles
│ └── utils

API (api/)

Contains files that handle API requests using Axios. Each file typically corresponds to a specific domain or feature. These files contain functions that handle all API calls related to a specific domain, such as fetching data, creating, updating, or deleting resources.

For example, jobs-api.js contains functions for all job-related API operations:

// api/jobs-api.js
import axios from "axios";

// Create axios instance with default config
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
"Content-Type": "application/json",
},
});

export const jobsApi = {
// Fetch list of jobs with optional filters
getJobs: async (filters = {}) => {
const { data } = await api.get("/jobs", { params: filters });
return data;
},

// Fetch single job details
getJobById: async (jobId) => {
const { data } = await api.get(`/jobs/${jobId}`);
return data;
},

// Create a new job posting
createJob: async (jobData) => {
const { data } = await api.post("/jobs", jobData);
return data;
},

// Update an existing job
updateJob: async (jobId, jobData) => {
const { data } = await api.put(`/jobs/${jobId}`, jobData);
return data;
},

// Delete a job
deleteJob: async (jobId) => {
const { data } = await api.delete(`/jobs/${jobId}`);
return data;
},

// Disable/Enable a job
toggleJobStatus: async (jobId, isActive) => {
const { data } = await api.patch(`/jobs/${jobId}/status`, { isActive });
return data;
},
};

Usage in a component:

import { jobsApi } from "api/jobs-api";

// In your component
const JobList = () => {
const [jobs, setJobs] = useState([]);
const [error, setError] = useState(null);

useEffect(() => {
const fetchJobs = async () => {
try {
const data = await jobsApi.getJobs({ status: "active" });
setJobs(data);
} catch (err) {
setError(err.response?.data?.message || "Failed to fetch jobs");
}
};
fetchJobs();
}, []);

// ... rest of the component
};

Components (components/)

Reusable UI components that are not tightly coupled to specific domain logic. These components can be used across different parts of the application. They should be generic enough to be reused in different contexts.

Example of a reusable form field component that works with React Hook Form and Bootstrap:

// components/forms/form-field.jsx
import React from "react";
import PropTypes from "prop-types";
import { useFormContext } from "react-hook-form";
import { Form } from "react-bootstrap";

export const FormField = ({
label,
name,
type = "text",
required,
...props
}) => {
const {
register,
formState: { errors },
} = useFormContext();

return (
<Form.Group className="mb-3">
<Form.Label>
{label}
{required && <span className="text-danger ms-1">*</span>}
</Form.Label>
<Form.Control
type={type}
isInvalid={!!errors[name]}
{...register(name)}
{...props}
/>
<Form.Control.Feedback type="invalid">
{errors[name]?.message}
</Form.Control.Feedback>
</Form.Group>
);
};

FormField.propTypes = {
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string,
required: PropTypes.bool,
};

Usage example with React Hook Form, Zod, and Bootstrap:

import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { FormField } from "components/forms/form-field";
import { Form, Button, Container } from "react-bootstrap";

// Define form schema with Zod
const jobFormSchema = z.object({
title: z.string().min(1, "Job title is required"),
salary: z.number().min(0, "Salary must be a positive number"),
description: z.string().min(10, "Description must be at least 10 characters"),
});

const JobForm = () => {
const methods = useForm({
resolver: zodResolver(jobFormSchema),
});

const onSubmit = (data) => {
console.log(data);
// Handle form submission
};

return (
<Container>
<FormProvider {...methods}>
<Form onSubmit={methods.handleSubmit(onSubmit)}>
<FormField label="Job Title" name="title" required />
<FormField label="Salary" name="salary" type="number" required />
<FormField
label="Description"
name="description"
as="textarea"
rows={3}
required
/>
<Button variant="primary" type="submit">
Create Job
</Button>
</Form>
</FormProvider>
</Container>
);
};

Domains (domains/)

Contains domain-specific logic, components, and schemas. Each subdirectory represents a specific domain in the application and contains all related components, validation schemas, and business logic service.

Example of an authentication domain:

// domains/auth/login-schema.js
import { z } from "zod";

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

// domains/auth/login-form.jsx
import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { loginSchema } from "./login-schema";
import { FormField } from "components/forms/form-field";
import { Form, Button, Card } from "react-bootstrap";

export const LoginForm = ({ onSubmit }) => {
const methods = useForm({
resolver: zodResolver(loginSchema),
});

return (
<Card>
<Card.Body>
<FormProvider {...methods}>
<Form onSubmit={methods.handleSubmit(onSubmit)}>
<FormField label="Email" name="email" type="email" required />
<FormField
label="Password"
name="password"
type="password"
required
/>
<Button variant="primary" type="submit">
Login
</Button>
</Form>
</FormProvider>
</Card.Body>
</Card>
);
};

Layouts (layouts/)

Contains layout components that define the structure of different sections of the application. Layouts typically include common elements like headers, navigation, sidebars, and footers.

Example of a dashboard layout:

// layouts/dashboard/dashboard-layout.jsx
import React from "react";
import { Outlet } from "react-router-dom";
import { DashboardSidebar } from "./dashboard-sidebar";
import { AppNavbar } from "components/ui/app-navbar";
import { Container, Row, Col } from "react-bootstrap";

export const DashboardLayout = () => {
return (
<div className="dashboard-layout">
<AppNavbar />
<Container fluid>
<Row>
<Col md={3} lg={2} className="dashboard-sidebar">
<DashboardSidebar />
</Col>
<Col md={9} lg={10} className="dashboard-main">
<Outlet />
</Col>
</Row>
</Container>
</div>
);
};

Routes (routes/)

Contains page components organized by domain structure rather than URL structure. This approach aligns with our domain-driven design and provides better maintainability and code organization. The actual URL-to-file mapping is handled declaratively in routes.js, which offers several advantages:

Benefits of Domain-Based Route Organization:

  • SEO Flexibility: Clean, semantic URLs that can be optimized for search engines without being constrained by file structure
  • Maintainability: Related pages are grouped together logically, making it easier to find and modify related functionality
  • Refactoring Safety: URL changes don't require moving files around, reducing the risk of broken imports

Why Not Just Put Pages in Domain Folders?

While domain-specific pages could theoretically stored in domains folder, we maintain a separate routes folder because not all pages are tied to specific domains or backend models. Many pages serve different purposes:

  • Static/Informational Pages: about-us, privacy-policy, terms-of-service, contact-us
  • Landing Pages: home, pricing, features
  • Workflow Pages: onboarding, welcome, getting-started

These pages don't correspond to any domain model, they're more about general website stuff, helping users navigate, or explaining what we do.

Folder Structure Example:

├── routes
│ ├── about-us.jsx
│ ├── careers
│ │ └── user-profiles
│ │ ├── user-profile-edit-page.jsx
│ │ └── user-profile-page.jsx
│ ├── home-page.jsx
│ ├── identities
│ │ └── users
│ │ └── user-edit-page.jsx
├── routes.js

File Naming Convention:

All page files must have a -page suffix in their filename to clearly distinguish them as page components.

This structure and convention keeps pages organized by their business domain while maintaining the flexibility to define clean, SEO-friendly URLs in the route configuration.

Styles (styles/)

Contains SCSS files for styling the application. The styles are organized into different files based on their purpose. We use Bootstrap as our primary CSS framework, with custom styles extending or overriding Bootstrap's default styles.

Example of style organization:

// styles/main.scss
// 1. Bootstrap Functions (so you can manipulate colors, SVGs, calc, etc)
@import "~bootstrap/scss/functions";

// 2. Bootstrap Variables Overrides
// Define custom variables (e.g. colors) before importing Bootstrap variables
// - Bootstrap will run functions like `$theme-colors` which will set the values.
@import "~styles/variables";

// 3. Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets)
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/variables-dark";

// 4. Include any default map overrides here
@import "~styles/maps";

// 5. Include remainder of required parts
@import "~bootstrap/scss/maps";
@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/utilities";

// 6. Customer Utilities
@import "~styles/utilities";
....

Utils (utils/)

Contains utility functions and helpers that are used across the application. These functions should be pure functions (same input always produces same output) and not contain any business logic.

Example of utility functions:

// utils/format.js
/**
* Format currency to local string
* @param {number} amount - The amount to format
* @param {string} currency - The currency code (default: 'SGD')
* @returns {string} Formatted currency string
*/
export const formatCurrency = (amount, currency = "SGD") => {
return new Intl.NumberFormat("en-SG", {
style: "currency",
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
};

/**
* Format date to local string
* @param {string|Date} date - The date to format
* @param {string} locale - The locale to use (default: 'en-SG')
* @returns {string} Formatted date string
*/
export const formatDate = (date, locale = "en-SG") => {
return new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(date));
};

Usage example:

import { formatCurrency, formatDate } from "utils/format";
import { isValidEmail, isValidPhone } from "utils/validation";
import { saveToStorage, getFromStorage } from "utils/storage";

// In your component
const JobCard = ({ job }) => {
return (
<Card>
<Card.Body>
<Card.Title>{job.title}</Card.Title>
<Card.Text>
Salary: {formatCurrency(job.salary)}
<br />
Posted: {formatDate(job.createdAt)}
</Card.Text>
</Card.Body>
</Card>
);
};

Route Configuration

Routes are defined in routes.js, which maps URLs to their corresponding components. The configuration uses a declarative approach:

import { index, route, layout, prefix } from "@react-router/dev/routes";

const routes = [
// Public Website Layout
layout("layouts/public/public-layout.jsx", [
index("./routes/home.jsx"),
route("about-us", "./routes/about-us.jsx"),
// Career Profile Routes
...prefix("careers", [
route("profile", "./routes/careers/user-profiles/user-profile-page.jsx"),
route(
"profile/edit",
"./routes/careers/user-profiles/user-profile-edit-page.jsx"
),
]),
...prefix("identities", [
route("edit", "./routes/identities/users/user-edit-page.jsx"),
]),
]),

// Standalone pages
route("sign-up", "./routes/sign-up.jsx"),
route("login", "./routes/login.jsx"),

// Dashboard Layout
layout("layouts/dashboard/dashboard-layout.jsx", [
...prefix("dashboard", [index("./routes/dashboard/dashboard-home.jsx")]),
]),
];

export default routes;

Best Practices

  1. Domain-Driven Design

    • Keep domain-specific logic in the domains directory
    • Each domain should be self-contained with its own components, schemas, and utilities
  2. Component Organization

    • Place reusable components in the components directory
    • Domain-specific components should go in their respective domain directories
  3. Route Organization

    • Follow the URL structure when organizing route files
    • Use nested folders for nested routes
    • Keep route files focused on page-level components
  4. API Organization

    • Group API calls by domain or feature
    • Keep API-related code separate from UI components
  5. Styling

    • Use Bootstrap as the primary CSS framework
    • Extend Bootstrap styles with custom SCSS when needed
    • Keep global styles in main.scss
    • Use variables for consistent theming