npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@fundaciobit/express-redis-mongo

v1.4.4

Published

A useful collection of Express middleware wrappers for Redis and MongoDB.

Downloads

41

Readme

Express wrappers for Redis and MongoDB

A useful collection of Express middleware wrappers for Redis and MongoDB.

Middlewares

| middleware | description | |-----------------|-------------------------------------------------------------------------| | redisGet | Get the value of a key from Redis cache. | | redisSet | Set the string value of a key to Redis cache. | | redisDel | Delete a key from the Redis cache. | | mongoFind | Query documents of a MongoDB collection and optionally format results. | | mongoFindOne | Query a document of a MongoDB collection and optionally format result. | | mongoInsertOne | Insert a document into a MongoDB collection. | | mongoUpdateOne | Update a single document in a MongoDB collection. | | mongoReplaceOne | Replace a single document in a MongoDB collection. | | mongoDeleteOne | Delete a document from a MongoDB collection. | | mongoCreateIndex| Creates an index on a MongoDB collection. |

Install

npm install @fundaciobit/express-redis-mongo

Index

redisGet

Middleware wrapper for the Redis GET command. Get the value of a key from the Redis cache. Returned value is available on the response via res.locals.redisValue by default.

Parameters

  • client: (required) Redis client.
  • key: (required) Function that accepts the request object as parameter, that returns the key (string).
  • parseResults: (optional) Boolean that indicates if the extracted value from Redis must be JSON parsed. Default value: false.
  • responseProperty: (optional) String. Property name on the response object res.locals where the returned value will be stored ( res.locals[responseProperty] ). Default property: res.locals.redisValue.

Usage

const express = require('express')
const redis = require('redis')
const { redisGet } = require('@fundaciobit/express-redis-mongo')

const REDIS_DB_INDEX = 0
const client = redis.createClient({ db: REDIS_DB_INDEX })

const app = express()

app.get('/companies/island/:island',
  redisGet({
    client,
    key: (req) => req.path,
    parseResults: true
  }),
  (req, res) => {
    const { redisValue } = res.locals
    if (redisValue) return res.status(200).json(redisValue)
    res.status(404).send('Not found')
  })

app.use((err, req, res, next) => {
  if (!err.statusCode) err.statusCode = 500
  res.status(err.statusCode).send(err.toString())
})

const port = 3000
app.listen(port, () => { console.log(`Server running on port ${port}...`) })

redisSet

Middleware wrapper for the Redis SET command. Set the string value of a key.

Parameters

  • client: (required) Redis client.
  • key: (required) Function that accepts the request object as parameter, that returns the key (string).
  • value: (required) Function that accepts the request and response objects as parameters, that returns the value (string).
  • expiration: (required) Integer. Number of seconds of expiraton time for the key/value.

Usage

const express = require('express')
const bodyParser = require('body-parser')
const redis = require('redis')
const { redisSet } = require('@fundaciobit/express-redis-mongo')

const REDIS_DB_INDEX = 0
const client = redis.createClient({ db: REDIS_DB_INDEX })

const app = express()

app.use(bodyParser.json())

app.post('/users',
  redisSet({
    client,
    key: (req) => req.body.username,
    value: (req, res) => JSON.stringify({ ip: req.ip }),
    expiration: 600  // seconds
  }),
  (req, res) => {
    res.status(200).send('Data cached')
  })

app.use((err, req, res, next) => {
  if (!err.statusCode) err.statusCode = 500
  res.status(err.statusCode).send(err.toString())
})

const port = 3000
app.listen(port, () => { console.log(`Server running on port ${port}...`) })

redisDel

Middleware wrapper for the Redis DEL command. Deletes a key from the Redis cache. Redis response will be available on res.locals.redisResponse. The response will be (integer) 1 in a delete successful operation.

Parameters

  • client: (required) Redis client.
  • key: (required) Function that accepts the request object as parameter, that returns the key (string).

Usage

const express = require('express')
const redis = require('redis')
const { redisDel } = require('@fundaciobit/express-redis-mongo')

const REDIS_DB_INDEX = 0
const client = redis.createClient({ db: REDIS_DB_INDEX })

const app = express()

app.delete('/users/:username',
  redisDel({
    client,
    key: (req) => req.params.username
  }),
  (req, res) => {
    const { redisResponse } = res.locals
    if (redisResponse === 1) return res.status(200).send('Deleted Successfully')
    res.status(404).send('Not found')
  })

app.use((err, req, res, next) => {
  if (!err.statusCode) err.statusCode = 500
  res.status(err.statusCode).send(err.toString())
})

const port = 3000
app.listen(port, () => { console.log(`Server running on port ${port}...`) })

mongoFind

Middleware wrapper of the MongoDB find method to query documents of the specified database and collection. The retrieved documents are available on the response via res.locals.results by default. It also provides an optional parameter to format results.

Parameters

  • mongoClient: (required) MongoDB client.
  • db: (required) String. Database name.
  • collection: (required) String. Collection name.
  • query: (required) Function that accepts the request object as parameter, that returns the query object.
  • projection: (optional) Object. Projection query with the fields that will be returned.
  • limit: (optional) Number. The number of returned results. Default value: 0 (0 is equivalent to setting no limit).
  • sort: (optional) Object. List of fields on which to sort the results. To specify sorting order, 1 and -1 are used. 1 is used for ascending order while -1 is used for descending order.
  • responseProperty: (optional) String. Property name on the response object res.locals where the returned docs will be stored ( res.locals[responseProperty] ). Default property: res.locals.results.
  • formatResults: (optional) Object to list formatters that transform results. The formatters property must be an array of functions. Each function accepts docs as parameter and returns the formatted results. The transformed results are pipelined through formatters.

Usage

const express = require('express')
const { MongoClient } = require('mongodb')
const { mongoFind } = require('@fundaciobit/express-redis-mongo')

const mongodbUri = 'mongodb://127.0.0.1:27017'

// Open MongoDB connection
MongoClient.connect(mongodbUri, { useUnifiedTopology: true, poolSize: 10 })
  .then(client => {
    createApp(client)
  })
  .catch(err => {
    console.log(err.message)
    process.exit(1)
  })

const createApp = (mongoClient) => {
  const app = express()

  app.get('/companies/island/:island',
    mongoFind({
      mongoClient,
      db: 'companies_db',
      collection: 'companies_col',
      query: (req) => ({ island: req.params.island }),
      projection: { name: 1, address: 1, postalCode: 1, city: 1 },
      limit: 10,  // docs retrieved
      sort: { name: 1 },
      formatResults: {
        formatters: [(docs) => {
          return docs.map(x => ({
            companyName: x.name,
            postalAddress: `${x.address}, ${x.postalCode} (${x.city})`
          }))
        }]
      },
      responseProperty: 'companies'
    }),
    (req, res) => {
      const { companies } = res.locals
      res.status(200).json(companies)
    })

  app.use((err, req, res, next) => {
    if (!err.statusCode) err.statusCode = 500
    res.status(err.statusCode).send(err.toString())
  })

  const port = 3000
  app.listen(port, () => { console.log(`Server running on port ${port}...`) })
}

mongoFindOne

Middleware wrapper for the MongoDB findOne method with optional parameter to format the result. The retrieved document will be available on the response via the res.locals.result by default.

Parameters

  • mongoClient: (required) MongoDB client.
  • db: (required) String. Database name.
  • collection: (required) String. Collection name.
  • query: (required) Function that accepts the request object as parameter, that returns the query object.
  • projection: (optional) Object. Projection query with the fields that will be returned.
  • responseProperty: (optional) String. Property name on the response object res.locals where the returned doc will be stored ( res.locals[responseProperty] ). Default property: res.locals.result.
  • formatResult: (optional) Function to transform the query result. The function accepts doc as parameter and returns the formatted result.

Usage

const express = require('express')
const { MongoClient, ObjectID } = require('mongodb')
const { mongoFindOne } = require('@fundaciobit/express-redis-mongo')

const mongodbUri = 'mongodb://127.0.0.1:27017'

// Open MongoDB connection
MongoClient.connect(mongodbUri, { useUnifiedTopology: true, poolSize: 10 })
  .then(client => {
    createApp(client)
  })
  .catch(err => {
    console.log(err.message)
    process.exit(1)
  })

const createApp = (mongoClient) => {
  const app = express()

  app.get('/companies/:id',
    mongoFindOne({
      mongoClient,
      db: 'companies_db',
      collection: 'companies_col',
      query: (req) => ({ _id: new ObjectID(req.params.id) }),
      projection: { title: 1 },
      formatResult: (doc) => ({ companyName: doc.title }),
      responseProperty: 'company'
    }),
    (req, res) => {
      const { company } = res.locals
      if (company) return res.status(200).json(company)
      res.status(404).send('Document not found')
    })

  app.use((err, req, res, next) => {
    if (!err.statusCode) err.statusCode = 500
    res.status(err.statusCode).send(err.toString())
  })

  const port = 3000
  app.listen(port, () => { console.log(`Server running on port ${port}...`) })
}

mongoInsertOne

Middleware wrapper for the MongoDB insertOne method. Inserts a document into a collection. The _id value of the inserted document is available on the response via the res.locals.insertedId (ObjectID) by default.

Parameters

  • mongoClient: (required) MongoDB client.
  • db: (required) String. Database name.
  • collection: (required) String. Collection name.
  • docToInsert: (required) Function that accepts the request and response objects as parameters, that returns the document to insert (object).
  • responseProperty: (optional) String. Property name on the response object res.locals where the inserted _id will be stored ( res.locals[responseProperty] ). If not specified, the _id will be available on the property res.locals.insertedId. The _id is returned as an ObjectID.

Usage

const express = require('express')
const bodyParser = require('body-parser')
const { MongoClient } = require('mongodb')
const { mongoInsertOne } = require('@fundaciobit/express-redis-mongo')

const mongodbUri = 'mongodb://127.0.0.1:27017'

// Open MongoDB connection
MongoClient.connect(mongodbUri, { useUnifiedTopology: true, poolSize: 10 })
  .then(client => {
    createApp(client)
  })
  .catch(err => {
    console.log(err.message)
    process.exit(1)
  })

const createApp = (mongoClient) => {
  const app = express()
  app.use(bodyParser.json())

  app.post('/companies',
    mongoInsertOne({
      mongoClient,
      db: 'companies_db',
      collection: 'companies_col',
      docToInsert: (req, res) => req.body
    }),
    (req, res) => {
      const { insertedId } = res.locals
      res.status(200).json({ _id: insertedId })
    })

  app.use((err, req, res, next) => {
    if (!err.statusCode) err.statusCode = 500
    res.status(err.statusCode).send(err.toString())
  })

  const port = 3000
  app.listen(port, () => { console.log(`Server running on port ${port}...`) })
}

mongoUpdateOne

Middleware wrapper for the MongoDB updateOne method. Update a single document in a collection based on the filter.

Parameters

  • mongoClient: (required) MongoDB client.
  • db: (required) String. Database name.
  • collection: (required) String. Collection name.
  • filter: (required) Function that accepts the request object as parameter, that returns the query object.
  • contentToUpdate: (required) Function that accepts the request and response objects as parameters, that returns the document fields to update (object).

Usage

const express = require('express')
const bodyParser = require('body-parser')
const { MongoClient, ObjectID } = require('mongodb')
const { mongoUpdateOne } = require('@fundaciobit/express-redis-mongo')

const mongodbUri = 'mongodb://127.0.0.1:27017'

// Open MongoDB connection
MongoClient.connect(mongodbUri, { useUnifiedTopology: true, poolSize: 10 })
  .then(client => {
    createApp(client)
  })
  .catch(err => {
    console.log(err.message)
    process.exit(1)
  })

const createApp = (mongoClient) => {
  const app = express()
  app.use(bodyParser.json())

  app.patch('/companies/:id',
    mongoUpdateOne({
      mongoClient,
      db: 'companies_db',
      collection: 'companies_col',
      filter: (req) => ({ _id: new ObjectID(req.params.id) }),
      contentToUpdate: (req, res) => ({ ...req.body })
    }),
    (req, res) => {
      res.status(200).send('Document successfully updated')
    })

  app.use((err, req, res, next) => {
    if (!err.statusCode) err.statusCode = 500
    res.status(err.statusCode).send(err.toString())
  })

  const port = 3000
  app.listen(port, () => { console.log(`Server running on port ${port}...`) })
}

mongoReplaceOne

Middleware wrapper for the MongoDB replaceOne method. Replaces a single document within the collection based on the filter. If upsert: true and no documents match the filter, then mongoReplaceOne creates a new document based on the replacement document and the _id value of the upserted document will be available on the response via res.locals.upsertedId (String) by default.

Parameters

  • mongoClient: (required) MongoDB client.
  • db: (required) String. Database name.
  • collection: (required) String. Collection name.
  • filter: (required) Function that accepts the request object as parameter, that returns the query object.
  • contentToReplace: (required) Function that accepts the request and response objects as parameters, that returns the document to replace (object).
  • upsert: (optional) Boolean. Indicates creation of a new document if upsert: true and no documents match the filter. Default value: false.
  • responseProperty: (optional) String. Property name on the response object res.locals where the upserted _id will be stored ( res.locals[responseProperty] ). If not specified, the _id will be available on res.locals.upsertedId. The _id is returned as a string.

Usage

const express = require('express')
const bodyParser = require('body-parser')
const { MongoClient, ObjectID } = require('mongodb')
const { mongoReplaceOne } = require('@fundaciobit/express-redis-mongo')

const mongodbUri = 'mongodb://127.0.0.1:27017'

// Open MongoDB connection
MongoClient.connect(mongodbUri, { useUnifiedTopology: true, poolSize: 10 })
  .then(client => {
    createApp(client)
  })
  .catch(err => {
    console.log(err.message)
    process.exit(1)
  })

const createApp = (mongoClient) => {
  const app = express()
  app.use(bodyParser.json())

  app.put('/companies/:id',
    mongoReplaceOne({
      mongoClient,
      db: 'companies_db',
      collection: 'companies_col',
      filter: (req) => ({ _id: new ObjectID(req.params.id) }),
      contentToReplace: (req, res) => ({ ...req.body }),
      upsert: true
    }),
    (req, res) => {
      const { upsertedId } = res.locals
      if (upsertedId) return res.status(200).json({ _id: upsertedId })  // Created new doc
      res.status(200).send('Document successfully replaced')
    })

  app.use((err, req, res, next) => {
    if (!err.statusCode) err.statusCode = 500
    res.status(err.statusCode).send(err.toString())
  })

  const port = 3000
  app.listen(port, () => { console.log(`Server running on port ${port}...`) })
}

mongoDeleteOne

Middleware wrapper for the MongoDB deleteOne operation. Deletes the first document that matches the filter.

Parameters

  • mongoClient: (required) MongoDB client.
  • db: (required) String. Database name.
  • collection: (required) String. Collection name.
  • filter: (required) Function that accepts the request object as parameter, that returns the query object.

Usage

const express = require('express')
const { MongoClient, ObjectID } = require('mongodb')
const { mongoDeleteOne } = require('@fundaciobit/express-redis-mongo')

const mongodbUri = 'mongodb://127.0.0.1:27017'

// Open MongoDB connection
MongoClient.connect(mongodbUri, { useUnifiedTopology: true, poolSize: 10 })
  .then(client => {
    createApp(client)
  })
  .catch(err => {
    console.log(err.message)
    process.exit(1)
  })

const createApp = (mongoClient) => {
  const app = express()

  app.delete('/companies/:id',
    mongoDeleteOne({
      mongoClient,
      db: 'companies_db',
      collection: 'companies_col',
      filter: (req) => ({ _id: new ObjectID(req.params.id) })
    }),
    (req, res) => {
      res.status(200).send('Document successfully deleted')
    })

  app.use((err, req, res, next) => {
    if (!err.statusCode) err.statusCode = 500
    res.status(err.statusCode).send(err.toString())
  })

  const port = 3000
  app.listen(port, () => { console.log(`Server running on port ${port}...`) })
}

mongoCreateIndex

Middleware wrapper for the MongoDB createIndex method. Creates an index on a MongoDB collection.

Parameters

  • mongoClient: (required) MongoDB client.
  • db: (required) String. Database name.
  • collection: (required) String. Collection name.
  • keys: (required) Object. Contains the field and value pairs where the field is the index key and the value describes the type of index for that field (see MongoDB documentation for details).
  • options: (optional) Object. Contains a set of options that controls the creation of the index (see MongoDB documentation for details).

Usage

const express = require('express')
const bodyParser = require('body-parser')
const { MongoClient } = require('mongodb')
const { mongoInsertOne, mongoCreateIndex } = require('@fundaciobit/express-redis-mongo')

const mongodbUri = 'mongodb://127.0.0.1:27017'

// Open MongoDB connection
MongoClient.connect(mongodbUri, { useUnifiedTopology: true, poolSize: 10 })
  .then(client => {
    createApp(client)
  })
  .catch(err => {
    console.log(err.message)
    process.exit(1)
  })

const createApp = (mongoClient) => {
  const app = express()
  app.use(bodyParser.json())

  app.post('/companies',
    mongoInsertOne({ mongoClient, db: 'companies_db', collection: 'companies_col', docToInsert: (req, res) => req.body }),
    mongoCreateIndex({ mongoClient, db, collection, keys: { company_id: 1 }, options: { sparse: true, unique: true } }),
    (req, res) => {
      const { insertedId } = res.locals
      res.status(200).json({ _id: insertedId })
    })

  app.use((err, req, res, next) => {
    if (!err.statusCode) err.statusCode = 500
    res.status(err.statusCode).send(err.toString())
  })

  const port = 3000
  app.listen(port, () => { console.log(`Server running on port ${port}...`) })
}

Sample Use Case of Combined Middlewares

The following use case shows the combination of several Express middlewares to authenticate users, validate IP addresses and JSON Schemas, and caching results from MongoDB to Redis.

Note: Some middlewares used in the use case are defined in the module @fundaciobit/express-middleware

const express = require('express')
const bodyParser = require('body-parser')
const redis = require('redis')
const { MongoClient, ObjectID } = require('mongodb')
const { ipv4, validateJsonSchema, signJWT, verifyJWT } = require('@fundaciobit/express-middleware')
const { redisGet, redisSet, redisDel, mongoFindOne, mongoFind, mongoInsertOne, mongoUpdateOne, mongoDeleteOne } = require('@fundaciobit/express-redis-mongo')

const mongodbUri = 'mongodb://127.0.0.1:27017'
const db = 'users_db'
const collection = 'users_col'

const REDIS_DB_INDEX = 0
const client = redis.createClient({ db: REDIS_DB_INDEX })

const usersSchema = {
  type: 'object',
  required: ['username', 'password', 'isAdmin', 'name', 'surname', 'age', 'address', 'city', 'postalCode'],
  properties: {
    username: { type: 'string' },
    password: { type: 'string', minLength: 6, maxLength: 10 },
    isAdmin: { type: 'boolean' },
    name: { type: 'string' },
    surname: { type: 'string' },
    age: { type: 'number', minimum: 0 },
    address: { type: 'string' },
    city: { type: 'string' },
    postalCode: { type: 'string', pattern: '^\\d+$' }
  },
  additionalProperties: false
}

const loginSchema = {
  type: 'object',
  required: ['username', 'password'],
  properties: {
    username: { type: 'string' },
    password: { type: 'string', minLength: 6, maxLength: 10 }
  },
  additionalProperties: false
}

const secret = 'my_secret'
const ipAddressesWhitelist = ['120.230.33.44', '120.230.33.45', '127.0.0.1']

class AuthenticationError extends Error {
  constructor(message) {
    super(message)
    this.name = 'AuthenticationError'
    this.statusCode = 401
  }
}

// Open MongoDB connection
MongoClient.connect(mongodbUri, { useUnifiedTopology: true, poolSize: 10 })
  .then(client => {
    console.log('Connected to MongoDB...')
    createApp(client)
  })
  .catch(err => {
    console.log(err.message)
    process.exit(1)
  })

const createApp = (mongoClient) => {
  const app = express()

  app.use(bodyParser.json())

  app.use(
    ipv4(),
    (req, res, next) => {
      const { ipv4 } = req
      if (ipAddressesWhitelist.indexOf(ipv4) !== -1) {
        next()
      } else {
        throw new AuthenticationError(`Forbidden access for IP '${ipv4}'`)
      }
    })

  // Login endpoint
  // ---------------
  app.post('/login',
    validateJsonSchema({ schema: loginSchema, instanceToValidate: (req) => req.body }),
    mongoFindOne({ mongoClient, db, collection, query: (req) => ({ username: req.body.username, password: req.body.password }), responseProperty: 'user' }),
    (req, res, next) => {
      const { user } = res.locals
      if (user) {
        req.user = { username: user.username, isAdmin: user.isAdmin }
        next()
      } else {
        throw new AuthenticationError(`Invalid credentials for user: ${req.body.username}`)
      }
    },
    signJWT({ payload: (req) => ({ username: req.user.username, isAdmin: req.user.isAdmin }), secret, signOptions: { expiresIn: '24h' } }),
    (req, res) => { res.status(200).json({ token: req.token }) }
  )

  // Create users endpoint
  // ----------------------
  app.post('/users',
    verifyJWT({ secret }),
    (req, res, next) => {
      const { tokenPayload } = req
      if (tokenPayload.isAdmin) {
        next()
      } else {
        throw new AuthenticationError(`Only admin users can create new users`)
      }
    },
    validateJsonSchema({ schema: usersSchema, instanceToValidate: (req) => req.body }),
    mongoInsertOne({ mongoClient, db, collection, docToInsert: (req, res) => req.body }),
    (req, res) => { res.status(200).json({ _id: res.locals.insertedId }) }
  )

  // Read users endpoint (filtering by city and caching results)
  // ------------------------------------------------------------
  app.get('/users/city/:city',
    verifyJWT({ secret }),
    redisGet({ client, key: (req) => req.path, parseResults: true }),  // Searching in Redis cache
    (req, res, next) => {
      const { redisValue } = res.locals
      if (redisValue) {
        console.log('Sending Redis cached results...')
        res.status(200).json(redisValue)  // Sending chached results
      } else {
        next()  // Key not found, proceed to searching users in MongoDB...
      }
    },
    mongoFind({ mongoClient, db, collection, query: (req) => ({ city: req.params.city }), formatResults: { formatters: [(docs) => { return docs.map(x => ({ name: x.name, age: x.age })) }] } }),
    redisSet({ client, key: (req) => req.path, value: (req, res) => JSON.stringify(res.locals.results), expiration: 60 }),  // Caching results in Redis for 60 seconds
    (req, res) => {
      console.log(' ...caching MongoDB results in Redis, sending results...')
      res.status(200).json(res.locals.results)
    })

  // Update users endpoint (emptying cache after updation)
  // ------------------------------------------------------
  const usersSchemaNoRequired = { ...usersSchema }
  delete usersSchemaNoRequired.required  // Deleted the 'required' field of the JSON schema to support validation of a subset of fields
  app.patch('/users/:id',
    verifyJWT({ secret }),
    (req, res, next) => {
      if (!req.tokenPayload.isAdmin) throw new AuthenticationError(`Only admin users can update users`)
      next()
    },
    validateJsonSchema({ schema: usersSchemaNoRequired, instanceToValidate: (req) => req.body }),
    mongoUpdateOne({ mongoClient, db, collection, filter: (req) => ({ _id: new ObjectID(req.params.id) }), contentToUpdate: (req, res) => ({ ...req.body }) }),
    // Remove all related key/values from Redis cache
    redisDel({ client, key: (req) => `/users/city/Palma` }),
    redisDel({ client, key: (req) => `/users/city/La%20Habana` }),
    (req, res) => { res.status(200).send('Document successfully updated. Cache removed.') }
  )

  // Delete users endpoint (emptying cache after deletion)
  // ------------------------------------------------------
  app.delete('/users/:id',
    verifyJWT({ secret }),
    (req, res, next) => {
      if (!req.tokenPayload.isAdmin) throw new AuthenticationError(`Only admin users can delete users`)
      next()
    },
    mongoDeleteOne({ mongoClient, db, collection, filter: (req) => ({ _id: new ObjectID(req.params.id) }) }),
    // Remove all related key/values from Redis cache
    redisDel({ client, key: (req) => `/users/city/Palma` }),
    redisDel({ client, key: (req) => `/users/city/La%20Habana` }),
    (req, res) => { res.status(200).send('Document successfully deleted. Cache removed.') }
  )

  app.use((err, req, res, next) => {
    if (!err.statusCode) err.statusCode = 500
    res.status(err.statusCode).send(err.toString())
  })

  const port = 3000
  app.listen(port, () => { console.log(`Server running on port http://localhost:${port} ...`) })
}