In this blog, we'll walk through building a job board web app using React.js (with Vite for setup), Node.js (using Express), SerpApi to fetch job listings from Google Jobs, and Material-UI (MUI) for styling.
By the end of this tutorial, you'll have a functional job board where users can search for jobs and view results fetched from Google's job listings.
Here is the demo of this project :
Prerequisites
To follow along, you'll need:
- Basic knowledge of React.js and Node.js
- Node.js installed
- SerpApi account and API key
- Vite for project setup
- MUI for styling
1. Let's create Account on SerpAPI website :
Website Link : https://serpapi.com/
One can either create an account or login ( if account already existed)
Next, select the API Key Section on the left-side bar and choose to generate a new key, use an existing one, or create a new one.
2. Project Structure
Here is the final project structure with both the server and client looks like:
job-board/
│
├── job-board-client/ # Frontend (React + Vite)
│ ├── node_modules/
│ ├── public/
│ ├── src/
│ │ ├── components/
│ │ │ ├── SearchBar.jsx # Search bar component
│ │ │ ├── JobList.jsx # Job list display component
│ │ ├── App.jsx # Main App component
│ │ ├── main.jsx # Entry point for the React app
│ │ └── index.css # Global CSS
│ ├── .gitignore
│ ├── index.html # Main HTML file
│ ├── package.json # Dependencies and scripts
│ ├── vite.config.js # Vite configuration
│ └── README.md
│
├── job-board-server/ # Backend (Node.js + Express)
│ ├── node_modules/
│ ├── index.js # Express server entry point
│ ├── .env # Environment variables (e.g., SERP_API_KEY)
│ ├── package.json # Dependencies and scripts
│ ├── .gitignore
│ └── README.md
3. Create the root folder job-board
mkdir job-board
cd job-board/
4. Initialize the React Frontend (Vite + React.js)
Start by setting up the React project with Vite.
# Create React project using Vite
npm create vite@latest job-board-client --template react
# Navigate into the project
cd job-board-client
# Install dependencies
npm install
Install Material-UI (MUI) for styling and axios for api calling.
# MUI Core and icons for styling
npm install axios @mui/material @emotion/react @emotion/styled @mui/icons-material
5. Initialize the Node.js Backend (Express)
Next, create a backend folder and initialize an Express server.In order to create backend you must be at job-board
. One can do this by simply running this command cd ..
.
After that run,
# Create backend directory
mkdir job-board-server
cd job-board-server
# Initialize Node.js project
npm init -y
# Install Express
npm install express cors axios dotenv
6. Set up a basic Express server (index.js
) in the job-board-server directory.
In the job-board-server
folder create index.js
file and create api that return jobs.
const express = require('express');
const cors = require('cors');
const axios = require('axios');
require('dotenv').config();
const app = express();
app.use(cors());
const PORT = process.env.PORT || 5000;
// Endpoint to fetch job listings
app.get('/api/jobs', async (req, res) => {
const { query } = req.query;
try {
const serpApiUrl = `https://serpapi.com/search.json?engine=google_jobs&q=${query}&api_key=${process.env.SERP_API_KEY}`;
const response = await axios.get(serpApiUrl);
res.json(response.data.jobs_results || []);
} catch (error) {
res.status(500).json({ error: 'Error fetching jobs' });
}
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
7. Add Environment Variables
Create a .env
file in your job-board-server
directory and add your SerpApi API key.
SERP_API_KEY=your_serp_api_key_here
8. Frontend: Job Search UI (React + MUI)
In the React project, create a search component that allows users to input search terms and view job results.
- Create a
SearchBar
component insrc/components/SearchBar.jsx
.
import React, { useState } from 'react';
import { TextField, Button, CircularProgress } from '@mui/material';
const SearchBar = ({ onSearch, loading }) => {
const [query, setQuery] = useState('');
const handleSearch = () => {
if (query.trim()) {
onSearch(query);
}
};
return (
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '20px' }}>
<TextField
label="Search Jobs"
variant="outlined"
value={query}
onChange={(e) => setQuery(e.target.value)}
style={{ marginRight: '10px', width: '300px' }}
/>
<Button
variant="contained"
color="primary"
onClick={handleSearch}
disabled={loading}
>
{loading ? <CircularProgress size={24} /> : 'Search'}
</Button>
</div>
);
};
export default SearchBar;
- Create a
JobList
component insrc/components/JobList.jsx
to display job results.
import React from 'react';
import { Card, CardContent, CardActions, Typography, Button, Grid, CircularProgress, Box } from '@mui/material';
import { WorkOutline } from '@mui/icons-material';
const JobCard = ({ job }) => {
return (
<Card
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
// height: '100%',
boxShadow: '0 4px 8px #1976d2',
p: 2,
mt:3
}}>
<CardContent>
<Typography variant="h5" component="div">
{job.title}
</Typography>
<Typography sx={{ mb: 1.5 }} color="text.secondary">
{job.company_name} - {job.location}
</Typography>
<Typography variant="body2">
{job.description.slice(0, 150)}... {/* Preview part of description */}
</Typography>
</CardContent>
<CardActions>
<Button
sx={{
backgroundColor: '#1976d2',
color: '#fff',
'&:hover': {
backgroundColor: '#1565c0',
},
width: '100%',
}}
size="small" href={job.share_link} target="_blank" rel="noopener">
Apply
</Button>
</CardActions>
</Card>
);
};
const JobList = ({ jobs, loading }) => {
if (loading) {
return (
<Box display="flex" justifyContent="center" marginTop="20px">
<CircularProgress />
</Box>
);
}
if (jobs.length === 0) {
return (
<Box display="flex" justifyContent="center" alignItems="center" flexDirection="column" marginTop="20px">
<WorkOutline style={{ fontSize: 60, color: 'gray' }} />
<Typography variant="h6" color="textSecondary">
No jobs available
</Typography>
</Box>
);
}
return (
<Grid container spacing={2}>
{jobs.map((job, index) => (
<Grid item xs={12} sm={6} md={4} key={index}>
<JobCard job={job} />
</Grid>
))}
</Grid>
);
};
export default JobList;
- In the App.jsx, bring everything together.
import React, { useState } from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import SearchBar from './components/SearchBar';
import JobList from './components/JobList';
import axios from 'axios';
import { Container } from '@mui/material';
const theme = createTheme({
palette: {
primary: {
main: '#1976d2',
},
secondary: {
main: '#ff4081',
},
},
});
const App = () => {
const [jobs, setJobs] = useState([]);
const [loading, setLoading] = useState(false);
const handleSearch = async (query) => {
try {
setLoading(true);
const response = await axios.get(`http://localhost:5000/api/jobs`, {
params: { query }
});
setJobs(response.data);
setLoading(false);
} catch (error) {
console.error('Error fetching job listings:', error);
setLoading(false);
}
};
return (
<ThemeProvider theme={theme}>
<Container>
<SearchBar onSearch={handleSearch} loading={loading} />
<JobList jobs={jobs} loading={loading} />
</Container>
</ThemeProvider>
);
};
export default App;
9. Start the Servers
Make sure both the backend and frontend are running.
# In the job-board-server folder
node index.js
# In the job-board-client folder
npm run dev
🎉 You’ve now built a fully functional job board web app! The frontend is built using React.js ⚛️ and styled with Material-UI 🎨, while the backend uses Node.js 🚀 with Express to serve job listings from SerpApi 🌐.
This is a great starting point for more advanced features, such as filtering by location 📍, adding job detail pages 📄, or even allowing users to save job listings 💾.
That's all for this blog! Stay tuned for more updates and keep building amazing apps! 💻✨
Happy coding! 😊