Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mongoose post.save failing on heroku, works on localhost

I'm trying to simply post to my MongoDB Atlas db via node,express,mongoose and heroku. A Postman POST request, Raw JSON with body:

{
    "title": "heroku post",
    "description": "post me plsssss"
}

works on localhost with this exact code, but when uploaded via heroku the try/catch block fails at post.save() as the response is the error.

{
    "error": "there's an error",
    "message": {}
}

But the error is empty and I'm not sure how to debug it. I've put in mongoose.set('debug', true); in app.js and i've modified my package.json: "start": "node app.js DEBUG=mquery", but those have made no extra output that I am seeing. Is there any other way to know why the post.save() is throwing an error, some logs that I am not utilising.. and how can I see those logs? Or if you just know what the issue is?

App.js

//use dotenv to store secret keys
require("dotenv").config();
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const cors = require('cors');

//connect to DB
mongoose.connect("mongodb+srv://grushevskiy:[email protected]/cluster-rest?retryWrites=true&w=majority", { useNewUrlParser: true, useUnifiedTopology: true, dbName: "cluster-rest" }, () => {
  console.log('connected to DB!')
})

mongoose.set('debug', true);

const db = mongoose.connection;

db.once('open', () => {
    console.log('connection opened')
});


//import routes for middleware
const postsRoute = require('./routes/posts');

//midleware routes
app.use('/posts', postsRoute)

//ROUTES
app.get('/', (req,res) => {
  res.send('we are on home')
})


//MIDDLEWARES
//cors
app.use(cors());
//decode url special characters
app.use(express.urlencoded({ extended: true }));
//parse json POSTs
app.use(express.json());


//How do we start listening to the server
app.listen( process.env.PORT || 3000);

posts.js

const express = require ('express')
const router = express.Router();
const Post = require('../models/Post')

//SUBMIT A POST
router.post('/', async (req,res) => {
  const post = new Post({
    title: req.body.title,
    description: req.body.description
  });

  console.log(post)

  try {
    const savedPost = await post.save();
    res.json(savedPost);
    console.log(savedPost)
  } catch (err) {
    res.json({ error: "there's an error", message: err, })
  }
})

module.exports = router;

Post.js Model

const mongoose = require('mongoose')
const PostSchema = mongoose.Schema({
  title: {
    type: String,
    required: true
  },
  description: {
    type: String,
    required: true
  },
  date: {
    type: Date,
    default: Date.now
  }
})
module.exports = mongoose.model('Post', PostSchema)

When I type heroku logs --tail there are no errors, also initially, the 'connected to DB!' message comes in a bit late.. I'm wondering if maybe this is an issue with async/await? My package.json:

{
  "name": "22-npmexpressrestapi",
  "version": "1.0.0",
  "engines": {
    "node": "14.15.3",
    "npm": "6.14.9"
  },
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node app.js DEBUG=mquery",
    "start:dev": "node app.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "express": "^4.17.1",
    "nodemon": "^2.0.7"
  },
  "dependencies": {
    "dotenv": "^8.2.0",
    "express": "4.17.1",
    "cors": "^2.8.5",
    "mongoose": "^5.11.11"
  }
}
like image 416
AGrush Avatar asked Nov 19 '25 23:11

AGrush


2 Answers

After reading your question carefully I have a couple of suggestions that I think you might want to try. First, let me explain myself:

In my experience, when one learns how to build REST APIs with Node.js, using mongoose to communicate with a MongoDB cluster, this is what the sequence of events of their index.js file looks like:

  1. Execute a function that uses environment variables to establish a connection with the database cluster. This function runs only once and fills de mongoose instance that the app is going to use with whatever models that have been designed for it.
  2. Set up the app by requiring the appropriate package, defining whatever middleware it's going to require, and calling to all the routes that have been defined for it.
  3. After the app has incorporated everything it's going to need in order to run properly, app.listen() is invoked, a valid port is provided and... voila! You've got your server running.

When the app and the databsae are simple enough, this works like a charm. I have built several services using these very same steps and brought them to production without noticing any miscommunication between mongoose and the app. But, if you check the App.js code you provided, you'll realize that there is a difference in your case: you only connect to your Mongo cluster after you set up your app, its middleware, and its routes. If any of those depend on the mongoose instance or the models to run, there is a good chance that by the time Heroku's compiler gets to the point where your routes need to connect to your database, it simply hasn't implemented that connection (meaning hasn't run the mongoose.connect() part) jet.

Simple solution

I think that Heroku is simply taking a little bit longer to run your whole code than your local machine. Also, if your local version is connecting to a locally stored MongoDB, there is a good chance things run quite quicker than in a cloud service. Your statement

the 'connected to DB!' message comes in a bit late

gives me hope that this might be the case. If that were so, then I guess that moving

mongoose.connect("mongodb+srv://xxxxx:[email protected]/cluster-rest?retryWrites=true&w=majority", { useNewUrlParser: true, useUnifiedTopology: true, dbName: "cluster-rest" }, () => {console.log('connected to DB!')});

to the second line of App.js, right after you call Dotenv, would do the trick. Your mongoose instance would go into App already connected with the database and models that App is going to use.

Asynchronous solution

Even if what I previously wrote fixed your problem, I would like to expand the explanation because, in my experience, that version of the solution wouldn't be right either. The way of doing things that I exposed at the beginning of this answer is ok... As long as you keep your MongoDB cluster and your mongoose instance simple. Not very long ago I had to implement a service that involved a much more complex database pattern, with several different databases being used by the same App, and many of its routes depending directly on those databases' models.

When I tried to use the logic I described before with such a problem, involving heavily populated databases and several models contained in the mongoose instance, I realized that it took Node far less building the App, its middleware, and its routes than connecting to mongoose and loading all the models that that very same App required for it to run. Meaning that I got my Server connected to PORT **** message before the database was actually connected. And that caused trouble.

That made me realize that the only way to do things properly was to ensure asynchronicity, forcing App to run only after all the databases and the models were loaded into the mongoose instance, and feeding it that very same instance. This way, I ended up having an index.js that looked like this:

const Dotenv = require("dotenv").config();
const { connectMongoose } = require("./server/db/mongoose");
const wrappedApp = require("./app");

connectMongoose().then((mongoose) => {
  const App = wrappedApp(mongoose);

  App.listen(process.env.PORT, () => {
    console.log(
      `Hello, I'm your server and I'm working on port ${process.env.PORT}`
    );
  });
});

A file with several functions govering the connections to my databases and modeles called mongoose.js. The function that implements all the connections in the right order looks is:

const connectMongoose = async (mongoose = require("mongoose")) => {
  try {
    // Close any previous connections
    await mongoose.disconnect();

    // Connect the Main database
    console.log(
      "--> Loading databases and models from MongoDB. Please don't use our server jet. <--"
    );
    mongoose = await connectMother(mongoose);

    // Connect all the client databases
    mongoose = await connectChildren(mongoose);

    console.log(
      "--> All databases and models loaded correctly. You can work now. <--"
    );

    return mongoose;
  } catch (e) {
    console.error(e);
  }
};

module.exports = { connectMongoose };

With this, the file app.js returns a function that needs to be fed a mongoose instance and sets up all the Express environment:

module.exports = (Mongoose) => {
  const Express = require("express"); //Enrouting
  // Other required packages go here

  // EXPRESS SETTINGS
  const App = Express();
  App.set("port", process.env.PORT || 3000); //Use port from cloud server

  // All the middleware App needs
  App.use(Flash());

  // Routing: load your routes
  
  // Return fully formed App
  return App;
};

Only using this pattern I made sure that the chain of events made sense:

  1. Connect all the databases and load their models.
  2. Only after the last database has been connected and its last model, loaded, set up the App, its middleware, and your routes.
  3. Use App.listen() to serve the App.

If your database isn't very large, then the first part of the answer might do the trick. But it's always useful to know the whole story.

like image 126
amda Avatar answered Nov 22 '25 12:11

amda


I am able to reach an empty error message with your code when i add wrong connection string.

console.log in this callback is incorrect:

mongoose.connect("mongodb+srv://xxxxx:[email protected]/cluster-rest?retryWrites=true&w=majority", { useNewUrlParser: true, useUnifiedTopology: true, dbName: "cluster-rest" }, () => {
  console.log('connected to DB!')
})

if you wanna know if you are connected to db use this:

mongoose.connect("mongodb+srv://xxxxx:[email protected]/cluster-rest?retryWrites=true&w=majority", { useNewUrlParser: true, useUnifiedTopology: true, dbName: "cluster-rest" });

const db = mongoose.connection;

db.once('open', () => {
    console.log('connection opened')
});

I noticed you are using dotenv maybe you are not adding variables to your heroku host.

like image 42
armin yahya Avatar answered Nov 22 '25 11:11

armin yahya



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!