I don't know which part of the app you're asking to help to implement. Below are the backend and frontend parts.
Backend
Try it on Replit.
const express = require("express");
const { GoogleSearch } = require("google-search-results-nodejs");
const search = new GoogleSearch(process.env.API_KEY);
const app = express();
app.set('json spaces', 2)
app.use(express.json());
// Set routes
app.get("/", (req, res) => {
res.send("Make a request to /search?q=coffee&q=cake");
});
app.get("/search", (req, res) => {
// The number of queries should be limited in a real application
// but it's ommitted here in favor of simplicity
const queries = Array.from(req.query.q);
makeSearches(queries).then((results) => {
res.send(results);
});
});
// API
// Workaround to make it work with Promises
// https://github.com/serpapi/google-search-results-nodejs/issues/4
function promisifiedGetJson(params) {
return new Promise((resolve, reject) => {
try {
search.json(params, resolve);
} catch (e) {
reject(e);
}
});
}
function makeSearches(queries) {
const promises = queries.map((q) => {
const params = {
q,
location: "Austin, TX",
};
return promisifiedGetJson(params);
});
return Promise.all(promises);
};
// Start server
app.listen(3000, () => {
console.log(`Server is running on port: 3000`);
});
Sample request and response:
$ curl -s 'https://mern-serpapi-nodejs.serpapi.repl.co/search?q=stackoverflow&q=github&q=deno' | jq -r '.[].search_metadata'
{
"id": "61fad426607393485726c241",
"status": "Success",
"json_endpoint": "https://serpapi.com/searches/582d9fcdfd66a739/61fad426607393485726c241.json",
"created_at": "2022-02-02 18:57:42 UTC",
"processed_at": "2022-02-02 18:57:42 UTC",
"google_url": "https://www.google.com/search?q=stackoverflow&oq=stackoverflow&uule=w+CAIQICIdQXVzdGluLFRYLFRleGFzLFVuaXRlZCBTdGF0ZXM&sourceid=chrome&ie=UTF-8",
"raw_html_file": "https://serpapi.com/searches/582d9fcdfd66a739/61fad426607393485726c241.html",
"total_time_taken": 0.66
}
{
"id": "61fad42617f923aa08188dfc",
"status": "Success",
"json_endpoint": "https://serpapi.com/searches/667255ede060a7ae/61fad42617f923aa08188dfc.json",
"created_at": "2022-02-02 18:57:42 UTC",
"processed_at": "2022-02-02 18:57:42 UTC",
"google_url": "https://www.google.com/search?q=github&oq=github&uule=w+CAIQICIdQXVzdGluLFRYLFRleGFzLFVuaXRlZCBTdGF0ZXM&sourceid=chrome&ie=UTF-8",
"raw_html_file": "https://serpapi.com/searches/667255ede060a7ae/61fad42617f923aa08188dfc.html",
"total_time_taken": 0.96
}
{
"id": "61fad4261baebbb454181a3a",
"status": "Success",
"json_endpoint": "https://serpapi.com/searches/ae665860b250fd5f/61fad4261baebbb454181a3a.json",
"created_at": "2022-02-02 18:57:42 UTC",
"processed_at": "2022-02-02 18:57:42 UTC",
"google_url": "https://www.google.com/search?q=deno&oq=deno&uule=w+CAIQICIdQXVzdGluLFRYLFRleGFzLFVuaXRlZCBTdGF0ZXM&sourceid=chrome&ie=UTF-8",
"raw_html_file": "https://serpapi.com/searches/ae665860b250fd5f/61fad4261baebbb454181a3a.html",
"total_time_taken": 0.61
}
Explanation
The answer consists of two parts: make N requests using SerpApi and return the response.
Make N requests
At the moment, google-search-results-nodejs doesn't have a Promise API so the workaround to make it work with Promises is required. With callback API it's also possible but it requires the usage of Array#reduce.
function promisifiedGetJson(params) {
return new Promise((resolve, reject) => {
try {
search.json(params, resolve);
} catch (e) {
reject(e);
}
});
}
With the Promise API, a list of keywords will be mapped into an array of Promises.
function makeSearches(queries) {
const promises = queries.map((q) => {
const params = {
q,
location: "Austin, TX",
};
return promisifiedGetJson(params);
});
return Promise.all(promises);
};
API action (route) to return the response
It gets an array of query parameters and calls a makeSearches function.
app.get("/search", (req, res) => {
// The number of queries should be limited in a real application
// but it's ommitted here in favor of simplicity
const queries = Array.from(req.query.q);
makeSearches(queries).then((results) => {
res.send(results);
});
});
Front-end part
Here are two full-stack React projects that use an API from above.
Demo
https://user-images.githubusercontent.com/282605/155405202-50bceacb-57f6-4729-b950-ea6e5ef6c295.mp4
Both projects are deployed to Vercel. Next.js app is simpler because that's the value proposition of Next.js.
Express.js app has SSR in development mode only. It uses Vite to render React components on the server. Next.js app has SSR in dev and prod.
React component itself is the same for both cases:
// App.jsx
import React from "react";
import ReactDOM from "react-dom";
import React, { useState } from "react";
import { SearchResults } from "./SearchResults";
export function App() {
const [q, setQ] = useState("coffee, cats");
const [searchResults, setSearchResults] = useState([]);
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState(null);
function handleFormSubmit(event) {
event.preventDefault();
const params = new URLSearchParams({
q,
});
setLoading(true);
fetch(`/api/search?${params}`)
.then((res) => res.json())
.then(
(results) => {
setSearchResults(results);
setLoading(false);
setError(null);
},
(error) => {
setError(error);
}
);
}
function handleQChange(event) {
event.preventDefault();
setQ(event.target.value);
}
return (
<>
<h1>SerpApi example in MERN stack</h1>
<form action="/api/search" method="get" onSubmit={handleFormSubmit}>
<label>
<label>Queries (separated by comma):</label>
<input name="q" value={q} placeholder={q} onChange={handleQChange} />
<br />
<input
type="submit"
value={isLoading ? "Loading..." : "Search"}
disabled={isLoading} />
</label>
</form>
<br />
<SearchResults
results={searchResults}
isLoading={isLoading}
error={error} />
</>
);
}
ReactDOM.hydrate(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
SearchResults.jsx
function SearchResults({ results, isLoading, error }) {
if (isLoading)
return (
<p>
Loading...
</p>
);
if (!results || results.length === 0) {
return <p>Click "Search" ↑ to continue.</p>;
}
if (error) return <p>Error: {error}</p>;
return (
<section>
<h3>Search results ({results.length})</h3>
<ul>{results.map(r => (<li key={r.search_metadata.id}>{r.search_metadata.id}</li>))}</ul>
</section>
);
}
Disclaimer: I work at SerpApi.
This question was asked in the Github repository of google-search-results-nodejs one day before it appeared here.