How to Create Your Own Job Board Web App Using React.js, Node.js, SerpApi, and MUI

Jagroop Singh - Sep 29 - - Dev Community

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 :

demo1

demo2

demo3

demo4

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/

SerpAPI Website

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.

Getting API key

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
Enter fullscreen mode Exit fullscreen mode

3. Create the root folder job-board

mkdir job-board
cd job-board/

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Step1

Step 2

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

3

4

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}`);
});

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.

  1. Create a SearchBar component in src/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;

Enter fullscreen mode Exit fullscreen mode
  1. Create a JobList component in src/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;

Enter fullscreen mode Exit fullscreen mode
  1. 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;

Enter fullscreen mode Exit fullscreen mode

9. Start the Servers

Make sure both the backend and frontend are running.

# In the job-board-server folder
node index.js

Enter fullscreen mode Exit fullscreen mode
# In the job-board-client folder
npm run dev

Enter fullscreen mode Exit fullscreen mode

🎉 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! 😊

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .