Reputation: 153
So I built a simple mern app that works fine on my local environment, but when deploying to Heroku, it serves the react app fine but 404s on the API calls. I can't seems to figure out the issue. I'm sending requests using Axios. I checked the network requests and they all look good, but still come back 404. testing in postman also returned the same error.
Here's the server code... Any idea why this is failing?
const express = require('express');
const path = require('path');
const Axios = require('axios');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
require('dotenv').config();
const PORT = process.env.PORT || 8080;
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/book';
const app = express();
// Define middleware here
app.use(express.urlencoded({ extended: true }));
app.use(bodyParser.json());
// Serve up static assets (usually on heroku)
if (process.env.NODE_ENV === 'production') {
app.use(express.static('client/build'));
}
const { Schema } = mongoose;
const bookSchema = new Schema({
info: Schema.Types.Mixed,
});
const Book = mongoose.model('Book', bookSchema);
app.post('/api/search', (req, res) => {
Axios.get(
`https://www.googleapis.com/books/v1/volumes?q=${req.body.term}`
).then(books => res.json(books.data.items));
});
app.post('/api/save', (req, res) => {
const newBook = new Book({ info: req.body.book });
newBook.save(err => {
if (err) res.json(err);
res.json({ status: true });
});
});
app.post('/api/unsave', (req, res) => {
Book.findByIdAndRemove(req.body.book._id, err => {
if (err) res.json(err);
res.json({ status: true });
});
});
app.get('/api/saved', (req, res) => {
Book.find({}, (err, books) => {
if (err) res.json(err);
res.json(books);
});
});
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, './client/build/index.html'));
});
mongoose.connect(mongoUri, { useNewUrlParser: true });
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function() {
console.log('connected');
});
app.listen(PORT, () => {
console.log(`🌎 ==> API server now on port ${PORT}!`);
});
and here's my package.json
{
"name": "google-book",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev",
"start:prod": "node server.js",
"start:dev": "concurrently \"nodemon --ignore 'client/*'\" \"npm run client\"",
"client": "cd client && npm run start",
"install": "cd client && npm install",
"build": "cd client && npm run build",
"heroku-postbuild": "npm run build"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.19.2",
"body-parser": "^1.19.0",
"concurrently": "^5.1.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"mongodb": "^3.5.3",
"mongoose": "^5.9.1"
}
}
My react routes in case that would be an issue
return (
<div className="app">
<Header />
<Router>
<Route exact path='/'>
<Searchbar search={search} setSearch={setSearch} />
{!search.term ? (
<div className="message">
<p>Search for a book or whatever</p>
</div>
) : <SearchList results={search.results} />}
</Route>
<Route path='/saved'>
<h2 className="title">Saved Books</h2>
<SavedList />
</Route>
<Footer />
</Router>
</div>
);
Upvotes: 1
Views: 4627
Reputation: 8528
Final results:
Explanation:
So, even when running this locally I was getting 404's - the issue turned out to be how you were starting the app.
You only need to start the server, and not the client. It looks like you were starting the "built-in" server that comes with create-react-app
... so, your server was never actually accepting requests as your front end was running on port 3000 and your backend was running on whatever port you had set in .env
.
Due to how you have axios
sending requests (just using the current URL, which was running on the built in create-react-app
port, not your server port), it was essentially sending requests to the wrong port.
This is something I should have thought of last night, since I remembered seeing your Heroku app was using the development build of React (via the Firefox React extension) - that should have been a red flag.
I have added 2 new npm
scripts: npm run begin
and npm start
(renamed the original npm start
to npm run start:original
. npm run begin
properly builds your front end, and then starts your backend afterwards. This is ultimately what resolved the issue. I also had NODE_ENV=production
while testing locally.
I also removed npm heroku-postbuild
as it is not needed.
Code Changes:
After getting this to work, I noticed there was something wrong with your front end code - a loop was sending the request over and over - I kept seeing the below info logged to the console. So I also resolved that using the code further down (I did not delete any code, I just commented the code out, so that you can see the changes I made).
I don't know where you're using Mongo at but I tested this using Atlas - I had issues talking to the database after deploying to Heroku, so I also had to change how you were connecting to the database in server.js
. You can also view these changes below, or in the GitHub repo..
Let me know if you want me to send a pull request to your repo so you'll have the updated code and won't have to manually change anything.
Lastly, double check your environmental variables inside Heroku - make sure that they are set.
// This kept being logged to the console
...
actually hit the route
actually hit the route
actually hit the route
actually hit the route
actually hit the route
actually hit the route
...
...
// This kept going on and on and on after I searched
These are the changes I made to fix the request loop:
// App.js
function App() {
/**
* Separated your state into 2 different variables.
* Your request loop was happening due to how your
* useEffect was configured (specifically the dependency
* array)
*/
const [searchTerm, setSearchTerm] = useState();
const [searchResults, setSearchResults] = useState();
/*
const [search, setSearch] = useState({
term: '',
results: []
});
*/
useEffect(() => {
Axios.post(`/api/search`, { term: searchTerm /* search.term */ })
.then(books => {
setSearchResults(books.data);
// setSearch({...search, results: books.data})
});
}, [searchTerm]);
return (
<div className="app">
<Header />
<Router>
<Route exact path='/'>
<Searchbar /* search={search} <-- No need for this param */ setSearch={setSearchTerm} />
{!searchTerm /* search.term */ ? (
<div className="message">
<p>Search for a book or whatever</p>
</div>
) : <SearchList results={searchResults/* search.results */} />}
</Route>
<Route path='/saved'>
<h2 className="title">Saved Books</h2>
<SavedList />
</Route>
<Footer />
</Router>
</div>
);
}
// Searchbar.js
const Searchbar = ({/* search, */ setSearch}) => { // <-- No need for search param here
return (
<form action="#" method="get" className="searchbar" onSubmit={e => e.preventDefault()}>
<DebounceInput
minLength={2}
debounceTimeout={300}
type="search"
placeholder="🔎 search..."
onChange={(e) => setSearch(e.target.value)}
/>
</form>
)
}
// server.js
require('dotenv').config();
const express = require('express');
const path = require('path');
const Axios = require('axios');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const PORT = process.env.PORT || 8080;
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/book';
const app = express();
// Define middleware here
app.use(express.urlencoded({ extended: true }));
app.use(bodyParser.json());
// Serve up static assets (usually on heroku)
if (process.env.NODE_ENV === 'production') {
app.use(express.static('client/build'));
}
const { Schema } = mongoose;
const bookSchema = new Schema({
info: Schema.Types.Mixed,
});
// *** REMOVED THIS ***
// const Book = mongoose.model('Book', bookSchema);
// ==========================================================
// **********************************************************
// CHANGED THE WAY YOU CONNECT TO MONGO
// **********************************************************
// ==========================================================
/** */ mongoose.set('useCreateIndex', true);
/** */
/** */ const mongoConnection = mongoose.createConnection(mongoUri, {
/** */ useUnifiedTopology: true,
/** */ useNewUrlParser: true,
/** */ useFindAndModify: false,
/** */ });
/** */
/** */ const Book = mongoConnection.model('Book', bookSchema /*, 'COLLECTION_NAME'*/);
// ==========================================================
// **********************************************************
// END OF CHANGES
// **********************************************************
// ==========================================================
app.post('/api/search', (req, res) => {
console.log('actually hit the route');
Axios.get(
`https://www.googleapis.com/books/v1/volumes?q=${req.body.term}`
).then(books => res.json(books.data.items));
});
app.post('/api/save', (req, res) => {
const newBook = new Book({ info: req.body.book });
newBook.save(err => {
if (err) res.json(err);
res.json({ status: true });
});
});
app.post('/api/unsave', (req, res) => {
Book.findByIdAndRemove(req.body.book._id, err => {
if (err) res.json(err);
res.json({ status: true });
});
});
app.get('/api/saved', (req, res) => {
Book.find({}, (err, books) => {
if (err) res.json(err);
else res.json(books);
});
});
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, './client/build/index.html'));
});
/*
const db = mongoose.connection;
db.on('error', // console.error.bind(console, 'connection error:')
error => {
console.log("[MONGOOSE][ERROR]", error);
}
);
db.once('open', function() {
console.log('[MONGOOSE][SUCCESS] Connected to database!');
});
*/
app.listen(PORT, () => {
console.log(`🌎 ==> API server now on port ${PORT}!`);
});
Upvotes: 2
Reputation: 57
if you add a console.log in the api save does it run when hitting that url? Even though it returns a 404 it might still be running possibly.
Upvotes: -1