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! 😊
Top comments (18)
@jagroop2001 , You come up with fresh concepts and technologies every week. Your blogs are a great resource for me to learn from.
In my opinion, you should begin offering lessons on YouTube in order to cover more complex projects.
@works ,
Thank you so much for your kind words! I'm glad to hear that you find my blogs helpful.
The idea of offering lessons on YouTube is exciting! It would definitely allow for a deeper dive into complex projects and concepts.
I’ll definitely consider it as I continue to explore new technologies. Stay tuned for updates, and thanks again for your support!
I learned a few things from you today. Thank you for sharing your knowledge.
@twistor You're welcome! I'm glad I could help.
thanks!
@serhiyandryeyev You’re welcome! 😊
GitHub link please
@shivaprasad_g_1ea5816f7f1 , this project is not hosted on git-hub yet. For my upcoming blog I will make sure to upload on git-hub as well.
Nice work ... 👍👍👍
Thanks @usman_awan
@jagroop2001 , SerpAPI is free or we can only search jobs or something else
@john12 , It have functionalities of maps, books,shopping,lens finance,scholar and lot's of things that's available in google.
What's about hotels features. Does it have that capabilities ?
Yes @john12 , it's available.
Your can explore more by creating an account.
@jagroop2001 , thanks.
It's a great article. With the reference of this article I will try to create Hotel Search Web App with custom filter's
@john12 , Sure and let me know if you need any help from my end.
Liquid syntax error: Unknown tag 'endraw'
Sir, i am unable to view your post.
please do something
Is this post taken down ?
Some comments may only be visible to logged-in visitors. Sign in to view all comments.