suya
v1.0.3
Published
A blazing-fast and strongly-typed exxpress middleware(s) that adds caching layer on top of your express API response to improve performance.
Downloads
1
Maintainers
Readme
Suya, a Simple, Fast Cache Middleware(s) for Express ⚡
Introduction
Suya is an express middleware(s) that adds caching layer on top of your express API response to reduce latency and improve API performance.
Installation
# through npm
$ npm i suya
# through yarn
$ yarn add suya
Features
- Lightweight Library.
- Simple API.
- Typescript Support.
- Many Cache Engines Support.
- Nice Terminal Logging.
Usage
To begin, you would need a cache/in memory store such as Redis, or Memcached installed on your machine or alternatively using NodeJS Internal Caching. If you want to quickly get up and running without installing Redis or Memcached on your machine. I highly recommend using managed cloud services like RedisLabs (redis) or Memcachier (memcached). No worries, they both have free plan with 25MB storage with no credit card required.
NodeJS Internal Caching
const express = require('express')
const { Suya } = require('suya')
const app = express()
const Cache = new Suya({
engine: {
name: 'node-cache',
},
})
// This is a middleware to cache forever.
// Methods supported: GET
let cacheForever = Cache.forever()
// This is a middleware to cache for a specific seconds.
// Methods supported: GET
let cacheWithDuration = Cache.duration(50)
// This is a middleware to reset cache on mutation.
// Methods supported: POST, PUT, PATCH, DELETE
let resetCacheOnMutate = Cache.resetOnMutate({
indicator: {
success: true,
},
})
let mockDB = () => {
let users = [
{
id: 1,
name: 'John Smith',
email: '[email protected]',
},
{
id: 2,
name: 'James Noah',
email: '[email protected]',
},
]
// mocking response time to be between 100ms - 600ms
let randResponseTime = Math.floor(Math.random() * 6 + 1) * 100
return new Promise((resolve, reject) => {
return setTimeout(() => {
resolve(users)
}, randResponseTime)
})
}
app.get('/users/forever', cacheForever, async (req, res, next) => {
let users = await mockDB()
res.status(200).json({
success: true,
data: {
users,
},
code: 200,
})
})
app.put('/users/forever', resetCacheOnMutate, async (req, res, next) => {
let users = await mockDB()
// res.status(400).json({
// // once the indicator set on .resetOnMutate({}) middleware doesn't match
// // like so, the data remain cached.
// success: false,
// error: {
// message: 'Email address is required!',
// },
// code: 400,
// })
res.status(200).json({
// once the indicator set on .resetOnMutate({}) middleware match like so,
// the cached data would get cleared it out.
success: true,
data: {
users,
},
code: 200,
})
})
app.get('/users/duration', cacheWithDuration, async (req, res, next) => {
let users = await mockDB()
res.status(200).json({
success: true,
data: {
users,
},
code: 200,
})
})
app.put('/users/duration', resetCacheOnMutate, async (req, res, next) => {
let users = await mockDB()
// res.status(400).json({
// // once the indicator set on .resetOnMutate({}) middleware doesn't match
// // like so, the data remain cached.
// success: false,
// error: {
// message: 'Email address is required!',
// },
// code: 400,
// })
res.status(200).json({
// once the indicator set on .resetOnMutate({}) middleware match like so,
// the cached data would get cleared it out.
success: true,
data: {
users,
},
code: 200,
})
})
const server = app.listen(2000, () =>
console.log('Server running at http://127.0.0.1:2000')
)
process.on('unhandledRejection', (err, promise) => {
console.log(`Error: ${err.message}`)
// close the server
server.close(async () => {
// close connection
await Cache.close()
process.exit(1)
})
})
Redis
const express = require('express')
const { Suya } = require('suya')
const app = express()
const Cache = new Suya({
engine: {
name: 'redis',
configs: {
redis: {
// node-redis configs options.
// https://github.com/NodeRedis/node-redis#options-object-properties
options: {
host: '127.0.0.1', // Redis host
port: 6379, // Redis port
password: '[pass]', // Redis password
family: 4, // 4 (IPv4) or 6 (IPv6)
db: 0, // Redis database
},
},
},
// whether suya should/shouldn't log to console
logging: true,
},
})
// This is a middleware to cache forever.
// Methods supported: GET
let cacheForever = Cache.forever()
// This is a middleware to cache for a specific seconds.
// Methods supported: GET
let cacheWithDuration = Cache.duration(50)
// This is a middleware to reset cache on mutation.
// Methods supported: POST, PUT, PATCH, DELETE
let resetCacheOnMutate = Cache.resetOnMutate({
indicator: {
success: true,
},
})
let mockDB = () => {
let users = [
{
id: 1,
name: 'John Smith',
email: '[email protected]',
},
{
id: 2,
name: 'James Noah',
email: '[email protected]',
},
]
// mocking response time to be between 100ms - 600ms
let randResponseTime = Math.floor(Math.random() * 6 + 1) * 100
return new Promise((resolve, reject) => {
return setTimeout(() => {
resolve(users)
}, randResponseTime)
})
}
app.get('/users/forever', cacheForever, async (req, res, next) => {
let users = await mockDB()
res.status(200).json({
success: true,
data: {
users,
},
code: 200,
})
})
app.put('/users/forever', resetCacheOnMutate, async (req, res, next) => {
let users = await mockDB()
// res.status(400).json({
// // once the indicator set on .resetOnMutate({}) middleware doesn't match
// // like so, the data remain cached.
// success: false,
// error: {
// message: 'Email address is required!',
// },
// code: 400,
// })
res.status(200).json({
// once the indicator set on .resetOnMutate({}) middleware match like so,
// the cached data would get cleared it out.
success: true,
data: {
users,
},
code: 200,
})
})
app.get('/users/duration', cacheWithDuration, async (req, res, next) => {
let users = await mockDB()
res.status(200).json({
success: true,
data: {
users,
},
code: 200,
})
})
app.put('/users/duration', resetCacheOnMutate, async (req, res, next) => {
let users = await mockDB()
// res.status(400).json({
// // once the indicator set on .resetOnMutate({}) middleware doesn't match
// // like so, the data remain cached.
// success: false,
// error: {
// message: 'Email address is required!',
// },
// code: 400,
// })
res.status(200).json({
// once the indicator set on .resetOnMutate({}) middleware match like so,
// the cached data would get cleared it out.
success: true,
data: {
users,
},
code: 200,
})
})
const server = app.listen(2000, () =>
console.log('Server running at http://127.0.0.1:2000')
)
process.on('unhandledRejection', (err, promise) => {
console.log(`Error: ${err.message}`)
// close the server
server.close(async () => {
// close connection
await Cache.close()
process.exit(1)
})
})
Memcached
const express = require('express')
const { Suya } = require('suya')
const app = express()
const Cache = new Suya({
engine: {
name: 'memcached',
configs: {
memcached: {
// server string format e.g
// single server - user:pass@server1:11211
// multiple servers - user:pass@server1:11211,user:pass@server2:11211
server: 'johndoe:[email protected]:11211', // local memcached server
// memjs configs options - https://github.com/memcachier/memjs
// some memjs options are overridden by suya. supported options are
// {
// retries: 2,
// retry_delay: 0.2,
// failoverTime: 60,
// }
options: {
retries: 2,
retry_delay: 0.2,
failoverTime: 60,
},
},
},
// whether suya should/shouldn't log to console
logging: true,
},
})
// This is a middleware to cache forever.
// Methods supported: GET
let cacheForever = Cache.forever()
// This is a middleware to cache for a specific seconds.
// Methods supported: GET
let cacheWithDuration = Cache.duration(50)
// This is a middleware to reset cache on mutation.
// Methods supported: POST, PUT, PATCH, DELETE
let resetCacheOnMutate = Cache.resetOnMutate({
indicator: {
success: true,
},
})
let mockDB = () => {
let users = [
{
id: 1,
name: 'John Smith',
email: '[email protected]',
},
{
id: 2,
name: 'James Noah',
email: '[email protected]',
},
]
// mocking response time to be between 100ms - 600ms
let randResponseTime = Math.floor(Math.random() * 6 + 1) * 100
return new Promise((resolve, reject) => {
return setTimeout(() => {
resolve(users)
}, randResponseTime)
})
}
app.get('/users/forever', cacheForever, async (req, res, next) => {
let users = await mockDB()
res.status(200).json({
success: true,
data: {
users,
},
code: 200,
})
})
app.put('/users/forever', resetCacheOnMutate, async (req, res, next) => {
let users = await mockDB()
// res.status(400).json({
// // once the indicator set on .resetOnMutate({}) middleware doesn't match
// // like so, the data remain cached.
// success: false,
// error: {
// message: 'Email address is required!',
// },
// code: 400,
// })
res.status(200).json({
// once the indicator set on .resetOnMutate({}) middleware match like so,
// the cached data would get cleared it out.
success: true,
data: {
users,
},
code: 200,
})
})
app.get('/users/duration', cacheWithDuration, async (req, res, next) => {
let users = await mockDB()
res.status(200).json({
success: true,
data: {
users,
},
code: 200,
})
})
app.put('/users/duration', resetCacheOnMutate, async (req, res, next) => {
let users = await mockDB()
// res.status(400).json({
// // once the indicator set on .resetOnMutate({}) middleware doesn't match
// // like so, the data remain cached.
// success: false,
// error: {
// message: 'Email address is required!',
// },
// code: 400,
// })
res.status(200).json({
// once the indicator set on .resetOnMutate({}) middleware match like so,
// the cached data would get cleared it out.
success: true,
data: {
users,
},
code: 200,
})
})
const server = app.listen(2000, () =>
console.log('Server running at http://127.0.0.1:2000')
)
process.on('unhandledRejection', (err, promise) => {
console.log(`Error: ${err.message}`)
// close the server
server.close(async () => {
// close connection
await Cache.close()
process.exit(1)
})
})
API
let Cache = new Suya({ engine: { name: [name], configs: { [configs] }, logging: [boolean] } })
[name]
- the name of the in memory engine to use. e.g name: 'node-cache' | 'redis' | 'memcached'[configs]
- the configurations for the selected engine. e.g// For Node-Cache // there's no configurations for node-cache // For Redis configs: { redis: { options: { // node-redis configurations options // https://github.com/NodeRedis/node-redis#options-object-properties }, } } // For Memcached configs: { memcached: { // server string format e.g // single server - user:pass@server1:11211 // multiple servers - user:pass@server1:11211,user:pass@server2:11211 server: '', // memjs configs options - https://github.com/memcachier/memjs // some memjs options are overridden by suya. supported options are // { // retries: 2, // retry_delay: 0.2, // failoverTime: 60, // } options: {}, } }
[logging]
- whether suya should/shouldn't log to console e.g logging: true | false. Default to true.
Cache.forever()
- midddleware to cache forever. Method supported: GET.Cache.duration([n])
- midddleware to cache for a specific duration (i.e time to live in cache engine) where [n] is the duration in seconds. Method supported: GET.Cache.resetOnMutate({ indicator: { [key]: [value] } })
- midddleware to reset cache on a successful mutation where [key] and [value] can be any indicator on successful mutation. e.g { success: true }. Method supported: POST, PUT, PATCH, DELETE.Cache.close()
- this is NOT a middleware, its just a helper method to close open connections.
Error Handling
Suya extends the global Error class. Some errors could be handle through express middleware like so:
// global express error handler
app.use((err, req, res, next) => {
if (err.name == 'SuyaError') {
return res.status(500).json({
success: false,
error: {
message: err.message,
},
})
}
})
NB: Some errors occur during the initializations of suya object and these errors are thrown when developers don't follow typescript compiler/rules according to suya types definition. These errors are been underlined during the development but the developer ignores them.
Tips
.forever()
middleware should be use when your data dont change often and use.resetOnMutate({ indicator: { [key]: [value] } })
to make it upto date on every mutation (POST, PUT, PATCH, DELETE)..duration([n])
middleware should be use when you are dealing with real time data (data that change often) and don't use.resetOnMutate({ indicator: { [key]: [value] } })
on mutation at all because using will clear up the cache on every mutation (POST, PUT, PATCH, DELETE) hence no performance improvement because the data is real time..duration([n])
only would get the cache cleared out as the duration elapse..close()
helper method should be use when node proccess crashes unexpectedly to close open connections to any external resources.
Contributors
Many thanks to all our contributors that helps to add core APIs to suya. I say a BIG thank you.
Contributions are welcome. Check CONTRIBUTING.md.
Tests
All the benchmark test suites are written with Jest and Axios.
Benchmarks tests
Nodecache
# terminal tab 1
# clone repo
$ git clone [repo_url]
# install dependencies
$ npm i
# start benchmark server
$ npm run start:benchmark:server:node-cache
## ~OUTPUT
## [NODECACHE] Server running at http://localhost:1000
# terminal tab 2
$ npm run benchmark:node-cache
## ~RESULT ON MY MACHINE
# PASS tests/benchmarks/node-cache.test.ts (42.591 s)
# The performance of suya with node-cache
# √ if there is any performance increase when using node-cache (9106 ms)
# Test Suites: 1 passed, 1 total
# Tests: 1 passed, 1 total
# Snapshots: 0 total
# Time: 46.997 s
Redis
# terminal tab 1
# clone repo
$ git clone [repo_url]
# install dependencies
$ npm i
# open tests/benchmarks/servers/redis.ts file and update your redis server credentials
# start benchmark server
$ npm run start:benchmark:server:redis
## ~OUTPUT
## [REDIS] Server running at http://localhost:2000
## -----------------------------------------------
## [REDIS] Suya connected to redis successfully!!!
## -----------------------------------------------
# terminal tab 2
$ npm run benchmark:redis
## ~RESULT ON MY MACHINE
# PASS tests/benchmarks/redis.test.ts (27.956 s)
# The performance of suya with redis
# √ if there is any performance increase when using redis (13092 ms)
# Test Suites: 1 passed, 1 total
# Tests: 1 passed, 1 total
# Snapshots: 0 total
# Time: 29.912 s
Memcached
# terminal tab 1
# clone repo
$ git clone [repo_url]
# install dependencies
$ npm i
# open tests/benchmarks/servers/memcached.ts file and update your memcached server credentials
# start benchmark server
$ npm run start:benchmark:server:memcached
## ~OUTPUT
## [MEMCACHED] Server running at http://localhost:3000
## -------------------------------------------------------
## [MEMCACHED] Suya connected to memcached successfully!!!
## -------------------------------------------------------
# terminal tab 2
$ npm run benchmark:memcached
## ~RESULT ON MY MACHINE
# PASS tests/benchmarks/memcached.test.ts (27.956 s)
# The performance of suya with memcached
# √ if there is any performance increase when using memcached (12073 ms)
# Test Suites: 1 passed, 1 total
# Tests: 1 passed, 1 total
# Snapshots: 0 total
# Time: 28.715 s
Changelog
- v1.0.3 - Improve documentation and remove dependencies that has it own types built-in i.e @types/colors and @types/node-cache. Created new release with v1.0.3 tag
- v1.0.2 - Fix bugs of throwing SuyaError in middleware(s) instead of passing the error to the next middleware in the cycle. Created new release with v1.0.2 tag
- v1.0.1 - Created new release with v1.0.1 tag which triggered Github Actions workflows to format, lint, build and re-publish the library. v1.0.1 is the initial release.
- v1.0.0 - Unpublished v1.0.0 from npm due to some errors, and remove v1.0.0 releases and tags from this repo.
- v1.0.0 - Commit all source codes, then I release v1.0.0 tag which triggered Github Actions workflows to format, lint, build and publish the library.
Versioning
I use SemVer for versioning.
License
MIT License
Copyright (c) 2020 [email protected]
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.