Job Tracking Application - Next.js, TypeScript, Clerk, Prisma, React Query, PostgreSQL FullStack Project
A full-featured, production-ready job tracking application built with Next.js 14+, TypeScript, Clerk, Prisma, React Query, PostgreSQL, and modern web technologies. Jobify helps job seekers efficiently organize, track, and analyze their job search journey with a beautiful, responsive dashboard.
- Live-Demo: https://jobify-tracker.vercel.app/
- Overview
- Features
- Technology Stack
- Project Structure
- Getting Started
- Environment Variables
- Database Setup
- Running the Project
- Project Walkthrough
- Components Guide
- Server Actions & API
- Routing Structure
- Key Concepts
- Reusable Components
- Code Examples
- Keywords
- Conclusion
Jobify is a comprehensive job application tracking system that allows users to:
- Track Job Applications: Add, edit, and manage job applications with details like position, company, location, status, and employment type
- Search & Filter: Search jobs by position or company name, filter by status (pending, interview, declined)
- Analytics Dashboard: Visualize job application statistics with charts and graphs
- Export Data: Download job application history as CSV or Excel files with monthly grouping
- Secure Authentication: User-specific data with Clerk authentication
- Dark Mode: Beautiful theme switching with system preference detection
- Responsive Design: Works seamlessly on desktop, tablet, and mobile devices
- β CRUD Operations: Create, Read, Update, Delete job applications
- β Advanced Search: Search by job title or company name
- β Status Filtering: Filter by pending, interview, or declined status
- β Pagination: Efficient pagination for large job lists
- β Real-time Stats: Dashboard with pending, interview, and declined counts
- β Charts & Analytics: Visual representation of application trends over time
- β Data Export: Download reports in CSV or Excel format with statistics
- β Authentication: Secure user authentication with Clerk
- β User Isolation: Each user only sees their own job applications
- β Loading States: Skeleton loaders and loading indicators
- β Error Handling: Graceful error handling throughout the application
- β Form Validation: Client and server-side validation with Zod
- β Toast Notifications: User feedback for actions
- β Theme Support: Light, dark, and system theme modes
- β Server-Side Rendering: Fast initial page loads with Next.js SSR
- β Data Prefetching: React Query hydration for optimal performance
- β Type Safety: Full TypeScript coverage
- β Database ORM: Prisma for type-safe database queries
- β Responsive UI: Mobile-first design with Tailwind CSS
- β Component Library: shadcn/ui for consistent, accessible components
- Next.js 14.2.1 - React framework with App Router
- TypeScript 5 - Type-safe JavaScript
- React 18 - UI library
- Tailwind CSS 3.3 - Utility-first CSS framework
- shadcn/ui - High-quality component library
- Lucide React - Icon library
- React Hook Form - Form state management
- Zod - Schema validation
- Recharts - Chart library for analytics
- Next.js Server Actions - Server-side API endpoints
- Prisma 5.7 - Next-generation ORM
- PostgreSQL - Relational database
- Clerk - Authentication and user management
- TanStack Query (React Query) 5.14 - Server state management
- React Query Devtools - Development tools
- dayjs - Date manipulation and formatting
- xlsx - Excel file generation
- next-themes - Theme management
- class-variance-authority - Component variant management
job-tracking-app/
βββ app/ # Next.js App Router directory
β βββ (dashboard)/ # Route group for dashboard pages
β β βββ add-job/
β β β βββ page.tsx # Add new job page
β β βββ jobs/
β β β βββ [id]/ # Dynamic route for job details
β β β β βββ page.tsx # Edit job page
β β β βββ loading.tsx # Loading UI for jobs page
β β β βββ page.tsx # All jobs listing page
β β βββ stats/
β β β βββ loading.tsx # Loading UI for stats page
β β β βββ page.tsx # Statistics dashboard page
β β βββ layout.tsx # Dashboard layout (Sidebar + Navbar)
β βββ globals.css # Global styles
β βββ layout.tsx # Root layout with providers
β βββ page.tsx # Landing page
β βββ providers.tsx # React Query & Theme providers
βββ components/ # React components
β βββ ui/ # shadcn/ui components
β β βββ button.tsx
β β βββ card.tsx
β β βββ form.tsx
β β βββ input.tsx
β β βββ ... # Other UI components
β βββ ButtonContainer.tsx # Pagination buttons
β βββ ChartsContainer.tsx # Analytics charts
β βββ CreateJobForm.tsx # Form to create new job
β βββ DeleteJobButton.tsx # Delete job button
β βββ DownloadDropdown.tsx # CSV/Excel export dropdown
β βββ EditJobForm.tsx # Form to edit job
β βββ JobCard.tsx # Job card component
β βββ JobsList.tsx # Jobs listing component
β βββ Navbar.tsx # Top navigation bar
β βββ SearchForm.tsx # Search and filter form
β βββ Sidebar.tsx # Side navigation
β βββ StatsCard.tsx # Statistics card component
β βββ StatsContainer.tsx # Stats container
β βββ ThemeToggle.tsx # Theme switcher
βββ prisma/
β βββ schema.prisma # Database schema
β βββ seed.js # Database seeding script
βββ public/ # Static assets
β βββ logo.svg
β βββ main.svg
βββ utils/
β βββ actions.ts # Server actions
β βββ db.ts # Prisma client singleton
β βββ links.tsx # Navigation links
β βββ types.ts # TypeScript types & Zod schemas
βββ lib/
β βββ utils.ts # Utility functions
βββ middleware.ts # Next.js middleware (auth protection)
βββ next.config.js # Next.js configuration
βββ tailwind.config.ts # Tailwind CSS configuration
βββ tsconfig.json # TypeScript configuration
βββ package.json # Dependencies and scriptsBefore you begin, ensure you have the following installed:
- Node.js 18.x or higher
- npm or yarn package manager
- PostgreSQL database (local or cloud)
- Git for version control
-
Clone the repository
git clone https://github.com/your-username/18-nextjs-jobify-app.git cd 18-nextjs-jobify-app -
Install dependencies
npm install # or yarn install -
Set up environment variables (see Environment Variables section)
-
Set up the database (see Database Setup section)
-
Run database migrations
npx prisma migrate dev
-
Generate Prisma Client
npx prisma generate
-
Start the development server
npm run dev # or yarn dev -
Open your browser Navigate to http://localhost:3000
Create a .env.local file in the root directory with the following variables:
# Clerk Authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxx
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxx
# Database Connection
DATABASE_URL="postgresql://username:password@localhost:5432/jobify_db?schema=public"
DIRECT_URL="postgresql://username:password@localhost:5432/jobify_db?schema=public"Step 1: Create a Clerk Account
- Visit https://clerk.com
- Sign up for a free account
- Create a new application
Step 2: Get Your Keys
- Go to your Clerk Dashboard
- Navigate to API Keys section
- Copy the following:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY(starts withpk_test_orpk_live_)CLERK_SECRET_KEY(starts withsk_test_orsk_live_)
Step 3: Configure Authentication
- In Clerk Dashboard, go to Settings β Paths
- Set Sign-in path to:
/sign-in - Set Sign-up path to:
/sign-up - Set After sign-in URL to:
/add-job - Set After sign-up URL to:
/add-job
Option A: Local PostgreSQL
-
Install PostgreSQL
- Download from https://www.postgresql.org/download/
- Or use Homebrew:
brew install postgresql@14
-
Create Database
# Start PostgreSQL service brew services start postgresql@14 # or sudo service postgresql start # Connect to PostgreSQL psql postgres # Create database CREATE DATABASE jobify_db; # Create user (optional) CREATE USER jobify_user WITH PASSWORD 'your_password'; GRANT ALL PRIVILEGES ON DATABASE jobify_db TO jobify_user;
-
Connection String Format
DATABASE_URL="postgresql://username:password@localhost:5432/jobify_db?schema=public" DIRECT_URL="postgresql://username:password@localhost:5432/jobify_db?schema=public"
Option B: Cloud Database (Recommended for Production)
-
Supabase (Free tier available)
- Visit https://supabase.com
- Create a new project
- Go to Settings β Database
- Copy the Connection string (URI format)
-
Neon (Free tier available)
- Visit https://neon.tech
- Create a new project
- Copy the connection string from dashboard
-
Railway (Free tier available)
- Visit https://railway.app
- Create a new PostgreSQL database
- Copy the connection string
# Clerk Authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_51AbCdEfGhIjKlMnOpQrStUvWxYz1234567890
CLERK_SECRET_KEY=sk_test_51AbCdEfGhIjKlMnOpQrStUvWxYz1234567890
# Database (Local PostgreSQL)
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/jobify_db?schema=public"
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/jobify_db?schema=public"
# Optional: Node Environment
NODE_ENV=developmentβ οΈ Never commit.env.localto version control- β
The
.env.localfile is already in.gitignore - β Use different keys for development and production
- β Rotate keys if they're accidentally exposed
Prisma is already configured. The schema is located at prisma/schema.prisma.
npx prisma migrate dev --name initThis command will:
- Create a new migration file
- Apply the migration to your database
- Generate the Prisma Client
npx prisma generateIf you want to populate the database with sample data:
npx prisma db seednpx prisma studioThis opens a visual database browser at http://localhost:5555
The main model is the Job model:
model Job {
id String @id @default(uuid())
clerkId String // User ID from Clerk
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
position String // Job title
company String // Company name
location String // Job location
status String // pending, interview, declined
mode String // full-time, part-time, internship
}npm run dev- Runs on http://localhost:3000
- Hot reload enabled
- React Query Devtools available
# Build the application
npm run build
# Start production server
npm start# Run linter
npm run lint
# Generate Prisma Client
npx prisma generate
# Run database migrations
npx prisma migrate dev
# Open Prisma Studio
npx prisma studioThe landing page introduces Jobify with:
- Hero section with app description
- Call-to-action button to get started
- Responsive design with logo and illustration
Key Features:
- Server-side rendered for fast initial load
- Responsive layout (mobile-first)
- Direct link to authentication flow
When users click "Get Started":
- Clerk middleware checks authentication
- Unauthenticated users are redirected to sign-in
- After sign-in/sign-up, users are redirected to
/add-job
Protected Routes:
/add-job- Add new job application/jobs- View all jobs/jobs/[id]- Edit specific job/stats- View statistics dashboard
Features:
- Form to create new job application
- Fields: Position, Company, Location, Status, Mode
- Client and server-side validation
- Toast notification on success/error
- Automatic redirect to jobs list after creation
Form Validation:
- Uses React Hook Form for form state
- Zod schema for validation
- Real-time error messages
Features:
- Search by position or company name
- Filter by status (all, pending, interview, declined)
- Pagination for large job lists
- Download dropdown (CSV/Excel export)
- Responsive grid layout (2 columns on desktop, 1 on mobile)
Data Flow:
- Server pre-fetches initial data (React Query hydration)
- Client-side React Query manages state
- URL search params sync with filters
- Real-time updates on search/filter changes
Features:
- Pre-populated form with existing job data
- Same validation as create form
- Update job details
- Delete job functionality
- Server-side data prefetching
Features:
- Three stat cards: Pending, Interview, Declined
- Charts showing application trends (last 6 months)
- Real-time data from database
- Loading states with skeleton loaders
Location: components/JobsList.tsx
Purpose: Displays paginated list of job applications with search and filter capabilities.
Key Features:
- React Query for data fetching
- URL-based search parameters
- Pagination support
- Download functionality integration
Usage:
import JobsList from "@/components/JobsList";
function JobsPage() {
return <JobsList />;
}Props: None (uses URL search params)
How it Works:
- Reads search params from URL (
search,jobStatus,page) - Uses React Query to fetch data with
getAllJobsAction - Displays jobs in responsive grid
- Shows pagination if more than 1 page
- Handles loading and empty states
Location: components/SearchForm.tsx
Purpose: Provides search and filter functionality for jobs.
Key Features:
- Search by position or company
- Filter by job status
- URL-based state management
- Form submission handling
Usage:
import SearchForm from "@/components/SearchForm";
function JobsPage() {
return (
<>
<SearchForm />
<JobsList />
</>
);
}How it Works:
- Reads current search params from URL
- Pre-fills form with current values
- On submit, updates URL with new params
- React Query automatically refetches with new params
Location: components/DownloadDropdown.tsx
Purpose: Allows users to download job application data as CSV or Excel files.
Key Features:
- CSV export with proper formatting
- Excel export with merged cells
- Statistics summary in exported files
- Monthly grouping with visual gaps
- Serial numbering for easy reference
Usage:
import DownloadDropdown from "@/components/DownloadDropdown";
function JobsPage() {
return (
<div>
<h2>Jobs Found</h2>
<DownloadDropdown />
</div>
);
}Export Format:
- CSV: Simple text format, easy to open in any spreadsheet app
- Excel: Proper XLSX format with merged header cells
File Structure:
Job Application History
Total Applied: X, Declined: Y, Interview: Z, Pending: W Report Generated: DD-MMM-YYYY HH:mm
No., Applied Date, Job Title, Company Name, Job Location, Role, Status
1, 15-Oct-2025, "Software Engineer", "Tech Corp", "San Francisco", Full Time, Pending
...
Location: components/StatsContainer.tsx
Purpose: Displays job application statistics in card format.
Key Features:
- Three stat cards (Pending, Interview, Declined)
- Loading states with skeleton cards
- Real-time data from database
- Responsive grid layout
Usage:
import StatsContainer from "@/components/StatsContainer";
function StatsPage() {
return (
<>
<StatsContainer />
<ChartsContainer />
</>
);
}Location: components/ChartsContainer.tsx
Purpose: Visualizes job application trends over time.
Key Features:
- Area chart showing applications per month
- Last 6 months of data
- Responsive chart design
- Loading states
Location: components/CreateJobForm.tsx
Purpose: Form to create new job applications.
Key Features:
- React Hook Form integration
- Zod validation
- Server action submission
- Toast notifications
- Automatic redirect on success
Form Fields:
- Position (required, min 2 characters)
- Company (required, min 2 characters)
- Location (required, min 2 characters)
- Status (enum: pending, interview, declined)
- Mode (enum: full-time, part-time, internship)
Location: components/EditJobForm.tsx
Purpose: Form to edit existing job applications.
Key Features:
- Pre-populated with existing data
- Same validation as create form
- Update server action
- Delete functionality
- Optimistic updates
All UI components are located in components/ui/ and are built with:
- Radix UI primitives for accessibility
- Tailwind CSS for styling
- class-variance-authority for variants
Available Components:
Button- Various button styles and sizesCard- Container componentForm- Form wrapper with validationInput- Text input fieldSelect- Dropdown selectDropdownMenu- Dropdown menu componentToast- Notification systemSkeleton- Loading placeholder- And more...
Next.js Server Actions allow you to write server-side code that can be called directly from client components. All server actions are in utils/actions.ts.
Purpose: Create a new job application.
Signature:
createJobAction(values: CreateAndEditJobType): Promise<JobType | null>Usage:
const result = await createJobAction({
position: "Software Engineer",
company: "Tech Corp",
location: "San Francisco",
status: "pending",
mode: "full-time",
});What it Does:
- Authenticates user
- Validates input with Zod schema
- Creates job record in database
- Associates job with user's Clerk ID
- Returns created job or null on error
Purpose: Fetch paginated list of jobs with optional search and filter.
Signature:
getAllJobsAction({
search?: string,
jobStatus?: string,
page?: number,
limit?: number
}): Promise<{
jobs: JobType[],
count: number,
page: number,
totalPages: number
}>Usage:
const result = await getAllJobsAction({
search: "engineer",
jobStatus: "pending",
page: 1,
limit: 10,
});Features:
- User-specific data (only returns user's jobs)
- Search by position or company
- Filter by status
- Pagination support
- Returns total count for pagination
Purpose: Fetch a single job by ID.
Signature:
getSingleJobAction(id: string): Promise<JobType | null>Usage:
const job = await getSingleJobAction("job-id-123");Security:
- Only returns job if it belongs to authenticated user
- Redirects to
/jobsif job not found or unauthorized
Purpose: Update an existing job application.
Signature:
updateJobAction(
id: string,
values: CreateAndEditJobType
): Promise<JobType | null>Usage:
const updated = await updateJobAction("job-id-123", {
position: "Senior Engineer",
company: "Tech Corp",
location: "Remote",
status: "interview",
mode: "full-time",
});Purpose: Delete a job application.
Signature:
deleteJobAction(id: string): Promise<JobType | null>Usage:
const deleted = await deleteJobAction("job-id-123");Security:
- Only deletes jobs belonging to authenticated user
- Compound where clause ensures user ownership
Purpose: Get statistics grouped by job status.
Signature:
getStatsAction(): Promise<{
pending: number,
interview: number,
declined: number
}>Usage:
const stats = await getStatsAction();
// Returns: { pending: 5, interview: 3, declined: 2 }How it Works:
- Uses Prisma
groupByto count jobs by status - Returns counts for each status type
- Defaults to 0 if no jobs of that status exist
Purpose: Get job application data for charts (last 6 months).
Signature:
getChartsDataAction(): Promise<Array<{ date: string, count: number }>>Usage:
const chartData = await getChartsDataAction();
// Returns: [{ date: "Oct 25", count: 5 }, { date: "Nov 25", count: 3 }, ...]Purpose: Get all jobs for export (no pagination).
Signature:
getAllJobsForDownloadAction(): Promise<JobType[]>Usage:
const allJobs = await getAllJobsForDownloadAction();Note: Used by download functionality to export complete job history.
This project uses Next.js 14 App Router with the following structure:
app/
βββ page.tsx # Landing page (/)
βββ layout.tsx # Root layout
βββ providers.tsx # Global providers
βββ (dashboard)/ # Route group (doesn't affect URL)
β βββ layout.tsx # Dashboard layout
β βββ add-job/
β β βββ page.tsx # /add-job
β βββ jobs/
β β βββ page.tsx # /jobs
β β βββ loading.tsx # Loading UI
β β βββ [id]/
β β βββ page.tsx # /jobs/[id] (dynamic route)
β βββ stats/
β βββ page.tsx # /stats
β βββ loading.tsx # Loading UIRoutes are protected using Clerk middleware in middleware.ts:
const isProtectedRoute = createRouteMatcher([
"/add-job",
"/jobs(.*)",
"/stats",
]);How it Works:
- Middleware runs on every request
- Checks if route is protected
- Redirects to sign-in if user not authenticated
- Allows access if user is authenticated
/jobs/[id]- Dynamic route for individual job pages[id]is accessible viaparams.idin the page component
Server Components (default):
- Run on the server
- Can directly access database
- No JavaScript sent to client
- Faster initial page load
Client Components ("use client"):
- Run in the browser
- Can use React hooks
- Interactive features
- State management
Example:
// Server Component (default)
async function JobsPage() {
const jobs = await getAllJobsAction({});
return <JobsList initialJobs={jobs} />;
}
// Client Component
("use client");
function JobsList({ initialJobs }) {
const [jobs, setJobs] = useState(initialJobs);
// ... interactive logic
}Purpose: Server state management and data fetching.
Key Features:
- Automatic caching
- Background refetching
- Loading and error states
- Optimistic updates
Usage Pattern:
const { data, isPending, error } = useQuery({
queryKey: ["jobs", search, status, page],
queryFn: () => getAllJobsAction({ search, status, page }),
});Query Keys:
- Unique identifiers for cached data
- Changing key triggers new fetch
- Example:
['jobs', 'engineer', 'pending', 1]
Hydration:
- Server pre-fetches data
- Data is "hydrated" into client cache
- No loading spinner on initial load
Purpose: Server-side functions called from client components.
Benefits:
- No API routes needed
- Type-safe
- Automatic serialization
- Built-in error handling
Usage:
"use client";
import { createJobAction } from "@/utils/actions";
function Form() {
const handleSubmit = async (data) => {
const result = await createJobAction(data);
// Handle result
};
}Purpose: Type-safe database access.
Key Features:
- TypeScript types generated from schema
- Type-safe queries
- Migrations
- Database introspection
Usage:
// Query
const jobs = await prisma.job.findMany({
where: { clerkId: userId },
orderBy: { createdAt: 'desc' },
});
// Create
const job = await prisma.job.create({
data: { position: "Engineer", company: "Tech Corp", ... },
});Purpose: User authentication and session management.
Features:
- Pre-built UI components
- Social login options
- Session management
- User metadata
Usage:
import { auth } from "@clerk/nextjs/server";
const { userId } = auth();
if (!userId) redirect("/");Location: components/ui/button.tsx
Reusability:
- Copy
components/ui/button.tsx - Copy
lib/utils.ts(forcnfunction) - Install dependencies:
class-variance-authority,clsx,tailwind-merge
Usage:
import { Button } from '@/components/ui/button';
<Button variant="default" size="sm">Click me</Button>
<Button variant="outline" size="lg">Outline</Button>
<Button variant="destructive">Delete</Button>Location: components/ui/form.tsx, components/ui/input.tsx
Reusability:
- Copy form components from
components/ui/ - Install:
react-hook-form,@hookform/resolvers,zod - Use with React Hook Form and Zod
Usage:
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
name: z.string().min(2),
});
function MyForm() {
const form = useForm({
resolver: zodResolver(schema),
});
return (
<Form {...form}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
</Form>
);
}Reusability:
- Can be adapted for any data export needs
- Change
getAllJobsForDownloadActionto your data fetching function - Modify CSV/Excel generation logic for your data structure
Adaptation Example:
// Change data source
const handleDownloadCSV = async () => {
const data = await getYourDataAction(); // Your action
downloadAsCSV(data);
};
// Modify CSV generation
const downloadAsCSV = (data: YourDataType[]) => {
let csvContent = "Column1,Column2,Column3\n";
data.forEach((item) => {
csvContent += `${item.field1},${item.field2},${item.field3}\n`;
});
// ... rest of download logic
};Reusability:
- Generic card component for displaying statistics
- Can be used for any numeric data display
Usage:
import StatsCard from '@/components/StatsCard';
<StatsCard title="Total Users" value={150} />
<StatsCard title="Active Sessions" value={45} />"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createJobAction } from "@/utils/actions";
import { createAndEditJobSchema, CreateAndEditJobType } from "@/utils/types";
function CreateJobForm() {
const form = useForm<CreateAndEditJobType>({
resolver: zodResolver(createAndEditJobSchema),
});
const onSubmit = async (data: CreateAndEditJobType) => {
const result = await createJobAction(data);
if (result) {
// Success - redirect or show toast
router.push("/jobs");
} else {
// Error - show error message
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>{/* Form fields */}</form>
</Form>
);
}"use client";
import { useQuery } from "@tanstack/react-query";
import { getAllJobsAction } from "@/utils/actions";
function JobsList() {
const searchParams = useSearchParams();
const search = searchParams.get("search") || "";
const status = searchParams.get("jobStatus") || "all";
const page = Number(searchParams.get("page")) || 1;
const { data, isPending, error } = useQuery({
queryKey: ["jobs", search, status, page],
queryFn: () => getAllJobsAction({ search, jobStatus: status, page }),
});
if (isPending) return <div>Loading...</div>;
if (error) return <div>Error loading jobs</div>;
return (
<div>
{data?.jobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</div>
);
}// app/jobs/page.tsx (Server Component)
import {
QueryClient,
dehydrate,
HydrationBoundary,
} from "@tanstack/react-query";
import { getAllJobsAction } from "@/utils/actions";
import JobsList from "@/components/JobsList";
async function JobsPage() {
const queryClient = new QueryClient();
// Prefetch data on server
await queryClient.prefetchQuery({
queryKey: ["jobs", "", "all", 1],
queryFn: () => getAllJobsAction({}),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<JobsList />
</HydrationBoundary>
);
}// middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isProtectedRoute = createRouteMatcher([
"/add-job",
"/jobs(.*)",
"/stats",
]);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) {
auth().protect();
}
});// utils/actions.ts
"use server";
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
import prisma from "./db";
export async function createJobAction(values: CreateAndEditJobType) {
// Authenticate user
const { userId } = auth();
if (!userId) {
redirect("/");
}
// Validate and create
const job = await prisma.job.create({
data: {
...values,
clerkId: userId,
},
});
return job;
}Frontend Technologies:
- Next.js, React, TypeScript, Tailwind CSS, shadcn/ui, React Hook Form, Zod, Recharts, Lucide React
Backend & Database:
- Server Actions, Prisma, PostgreSQL, Clerk Authentication
State Management:
- TanStack Query (React Query), React Hooks
Development Tools:
- TypeScript, ESLint, Prisma Studio, React Query Devtools
Features:
- CRUD Operations, Search & Filter, Pagination, Data Export, Analytics, Dark Mode, Responsive Design
Architecture:
- Server-Side Rendering (SSR), Client-Side Rendering (CSR), Server Components, Client Components, Route Protection, Data Hydration
Concepts:
- Type Safety, Form Validation, Error Handling, Loading States, Optimistic Updates, Caching
-
Next.js 14 App Router
-
React Query (TanStack Query)
-
Prisma ORM
-
Clerk Authentication
-
shadcn/ui
-
TypeScript
Problem: Can't reach database server
Solutions:
- Check DATABASE_URL in
.env.local - Verify PostgreSQL is running
- Check database credentials
- Ensure database exists
Problem: Redirects not working or authentication failing
Solutions:
- Verify Clerk keys in
.env.local - Check Clerk dashboard settings
- Ensure middleware is properly configured
- Clear browser cookies and try again
Problem: PrismaClient is not defined
Solutions:
npx prisma generateProblem: Type errors or build failures
Solutions:
- Run
npm installto ensure all dependencies are installed - Run
npx prisma generateto generate Prisma Client - Check TypeScript errors:
npm run lint - Clear
.nextfolder and rebuild
Problem: Variables are undefined
Solutions:
- Ensure file is named
.env.local(not.env) - Restart development server after adding variables
- Check variable names match exactly (case-sensitive)
- Verify
NEXT_PUBLIC_prefix for client-side variables
-
Push to GitHub
git add . git commit -m "Ready for deployment" git push origin main
-
Deploy to Vercel
- Go to vercel.com
- Import your GitHub repository
- Add environment variables
- Deploy
-
Environment Variables in Vercel
- Go to Project Settings β Environment Variables
- Add all variables from
.env.local - Redeploy after adding variables
Railway:
- Connect GitHub repository
- Add environment variables
- Railway auto-detects Next.js and deploys
Netlify:
- Connect GitHub repository
- Build command:
npm run build - Publish directory:
.next - Add environment variables
Jobify is a comprehensive, production-ready job application tracker that demonstrates modern web development best practices. It showcases:
- Full-Stack Development: Server and client components working together
- Type Safety: End-to-end TypeScript coverage
- Modern Patterns: Server Actions, React Query, Prisma ORM
- User Experience: Responsive design, loading states, error handling
- Security: Authentication, user isolation, input validation
- Performance: Server-side rendering, data prefetching, caching
This project serves as an excellent learning resource for:
- Next.js 14 App Router
- TypeScript in React
- Database management with Prisma
- Authentication with Clerk
- State management with React Query
- Form handling with React Hook Form
- UI development with Tailwind CSS and shadcn/ui
Feel free to use this project repository and extend this project further!
If you have any questions or want to share your work, reach out via GitHub or my portfolio at https://arnob-mahmud.vercel.app/.
Enjoy building and learning! π
Thank you! π



