javascriptnode.jsexpressexpress-router

Express empty req.params route to the previous route


Suppose that I have the following REST endpoints

/api/companies
/api/companies/:id
/api/companies/name/:slug

The first route /api/companies POST will post a new company, while GET will fetch list of all companies.

The second route /api/companies/:id GET will fetch that specific company by its id, PUT will update it, DELETE will remove it.

The third route /api/companies/name/:slug will fetch list of companies that start with name.

My router.js file looks like this

// /api/companies
router
    .route('/')
    .post(controllers.createCompany)
    .get(controllers.getByPriority)

// /api/companies/:id
router
    .route('/:id')
    .put(controllers.updateOne)
    .delete(controllers.removeOne)
    .get(controllers.getOne)

// /api/companies/name/:slug
router
    .route('/name/:slug?')
    .get(controllers.getByName)

The problem is if there was no slug parameter in the third route, it will match with the second route which will return cast error.

For example: GET /api/companies/name/ returns the following error

CastError: Cast to ObjectId failed for value "name" (type string) at path "_id" for model "company"
    at model.Query.exec (D:\Projects\StackInfo\stack-info\backend\node_modules\mongoose\lib\query.js:4545:21)
    at D:\Projects\StackInfo\stack-info\backend\dist\utils\crud.js:91:15
    at Layer.handle [as handle_request] (D:\Projects\StackInfo\stack-info\backend\node_modules\express\lib\router\layer.js:95:5)
    at next (D:\Projects\StackInfo\stack-info\backend\node_modules\express\lib\router\route.js:137:13)
    at next (D:\Projects\StackInfo\stack-info\backend\node_modules\express\lib\router\route.js:131:14)
    at next (D:\Projects\StackInfo\stack-info\backend\node_modules\express\lib\router\route.js:131:14)
    at next (D:\Projects\StackInfo\stack-info\backend\node_modules\express\lib\router\route.js:131:14)
    at next (D:\Projects\StackInfo\stack-info\backend\node_modules\express\lib\router\route.js:131:14)
    at next (D:\Projects\StackInfo\stack-info\backend\node_modules\express\lib\router\route.js:131:14)
    at next (D:\Projects\StackInfo\stack-info\backend\node_modules\express\lib\router\route.js:131:14) {
  messageFormat: undefined,
  stringValue: '"name"',
  kind: 'ObjectId',
  value: 'name',
  path: '_id',
  reason: TypeError: Argument passed in must be a Buffer or string of 12 bytes or a string of 24 hex characters
      at new BSONTypeError (D:\Projects\StackInfo\stack-info\backend\node_modules\bson\lib\error.js:39:42)
      at new ObjectId (D:\Projects\StackInfo\stack-info\backend\node_modules\bson\lib\objectid.js:62:23)
      at castObjectId (D:\Projects\StackInfo\stack-info\backend\node_modules\mongoose\lib\cast\objectid.js:25:12)
      at ObjectId.cast (D:\Projects\StackInfo\stack-info\backend\node_modules\mongoose\lib\schema\objectid.js:245:12)
      at ObjectId.SchemaType.applySetters (D:\Projects\StackInfo\stack-info\backend\node_modules\mongoose\lib\schematype.js:1135:12)
      at ObjectId.SchemaType._castForQuery (D:\Projects\StackInfo\stack-info\backend\node_modules\mongoose\lib\schematype.js:1567:15)
      at ObjectId.SchemaType.castForQuery (D:\Projects\StackInfo\stack-info\backend\node_modules\mongoose\lib\schematype.js:1557:15)
      at ObjectId.SchemaType.castForQueryWrapper (D:\Projects\StackInfo\stack-info\backend\node_modules\mongoose\lib\schematype.js:1534:20)
      at cast (D:\Projects\StackInfo\stack-info\backend\node_modules\mongoose\lib\cast.js:336:32)
      at model.Query.Query.cast (D:\Projects\StackInfo\stack-info\backend\node_modules\mongoose\lib\query.js:4968:12),
  valueType: 'string'
}

The crud.js getOne looks like this as well

export const getOne = model => async (req, res) => {
    try {
        const id = req.params.id
        const doc = await model
            .findOne({ _id: id })
            .lean().exec() // Where the error exists
        if (!doc) {
            return res.status(404).end()
        }
        res.status(200).json({ data: doc })
    } catch (e) {
        console.error(e)
        res.status(400).end()
    }
}

I tried to add other route so that it can handle the request if no slug was passed along

// /api/companies
router
    .route('/')
    .post(controllers.createCompany)
    .get(controllers.getByPriority)

// /api/companies/:id
router
    .route('/:id')
    .put(controllers.updateOne)
    .delete(controllers.removeOne)
    .get(controllers.getOne)

// in case /api/companies/name/:slug did not match
router
.route('/name')
.get(controllers.getByPriority)
// /api/companies/name/:slug
router
    .route('/name/:slug?')
    .get(controllers.getByName)

But it still matches with the /api/companies/:id route and returns the same error. So, is there any solution for avoiding to route with /api/companies/:id when I am requesting on /api/companies/name/ even with an empty parameter?


Solution

  • Express execute code top to bottom. So you can just reverse the order of the endpoints in the code. Since /name/:slug is more specific, define it before /:id:

    router
        .route('/')
        .post(controllers.createCompany)
        .get(controllers.getByPriority)
    
    router
        .route('/name/:slug?')
        .get(controllers.getByName)
    
    router
        .route('/:id')
        .put(controllers.updateOne)
        .delete(controllers.removeOne)
        .get(controllers.getOne)