Introducing Redis OM for Node.js

Fast and easy object mapping for JavaScript and TypeScript

I wrote a thing—well, actually several of us wrote things—I just wrote the Node.js version. I think it’s a pretty cool thing that’s worthy of your attention, and I’m excited about it. The thing I’m talking about: Redis OM for Node.js.

What’s Redis OM? Redis OM is a library that makes it easy to use Redis by mapping Redis data structures straight to your code. The OM bit stands for object mapping, but we plan to do more than just that in the future.

Read on for more details.

Faster than fast

You probably already know that Redis is fast—faster than fast—and it stores the sorts of things your programs care about: hashes, lists, sets, things like that. These are the structures I think in as a programmer. When I’m explaining what Redis is to an uninitiated techie, I tell them that Redis is all those data structures you learned about in college with a wire-protocol plopped in front of them.

Features make Redis even awesomer (yes, I know that’s not a word) by introducing new data structures for your programs to care about. Probabilistic adds probabilistic data structures like Bloom and Cuckoo filters.

The most awesomest modules

But there are two modules in particular that make Redis the powerful in-memory database every programmer wants it to be: JSON and Search and Query.

JSON brings hierarchy to the table. Hashes are great, but what if you want to embed hashes in your hash? JSON has you covered by letting you store my hierarchical data in JSON documents.

Meanwhile, Search and Query lets you find the data structures you care about. Sure, you can create manual indices using sets, but that approach is limited and manual. Yuck. Search and Query lets you write queries to go right to the data you want.

Combined, Search and Query and JSON make Redis a pretty nifty document database. You get the hierarchy you want and the ability to find the things in it that you care about. Best of both worlds and all that.

In fact, we think this is such a tasty combination, we’re rolling them together and just calling them JSON.

Redis OM for Node.js

The query language for Search and Query is powerful. Very powerful. It allows you to search Hashes and JSON documents within Redis in varied and sophisticated ways. And when I need all that power, it’s very cool. But sometimes I don’t need all that power. And I am a lazy developer—I want it to be as easy as possible (and no easier).

Making things as easy as possible is what I was trying to do when I wrote Redis OM for Node.js. Redis OM makes it simple to add Redis to your Node.js application by mapping hashes and JSON documents to classes that you define. No complex commands, just pure code with a fluent interface. Take a look.

Define an entity:

class Album extends Entity {}

let schema = new Schema(Album, {
  artist: { type: 'string' },
  title: { type: 'string', textSearch: true },
  year: { type: 'number' }
});

Create a new entity and save it:

let album = repository.createEntity()
album.artist = "Mushroomhead"
album.title = "The Righteous & The Butterfly"
album.year = 2014
await repository.save(album)

Search for matching entities:

let albums = await repository.search()
  .where('artist').equals('Mushroomhead')
  .and('title').matches('butterfly')
  .and('year').is.greaterThan(2000).returnAll()

Now I’m fairly biased since I wrote this library and all, but I think that’s pretty dadgum cool! Let’s take a closer look at this syntax and learn how it works.

How it works

There are four classes you need to care about in Redis OM for Node.js. They are Entity, Schema, Client, and Repository.

import { Entity, Schema, Client, Repository } from 'redis-om'
class Album extends Entity {}
let schema = new Schema(Album, {
  artist: { type: 'string' },
  title: { type: 'string' },
  year: { type: 'number' },
  genres: { type: 'array' },
  outOfPublication: { type: 'boolean' }
})
let client = new Client()
await client.open('redis://localhost:6379')
let repository = new Repository(schema, client)
let album, id

// create an entity and save it
album = repository.createEntity()

album.artist = "Mushroomhead"
album.title = "The Righteous & The Butterfly"
album.year = 2014
album.genres = [ 'metal' ]
album.outOfPublication = true

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// read an entity
album = await repository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')

// update an entity
album.genres = [ 'metal', 'nu metal', 'avantgarde' ]
album.outOfPublication = false

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// delete an entity
await repository.remove('01FJYWEYRHYFT8YTEGQBABJ43J')
let albums = await repository.search()
  .where('artist').equals('Mushroomhead')
  .and('title').matches('butterfly')
  .and('year').is.greaterThan(2000).returnAll()

Entities are the classes that you work with. The things being created, read, updated, and deleted. The things being searched for. Any class that extends Entity is an entity. Usually, you’ll define an entity with a single line of code, but you can add custom logic in there as well:

import { Entity, Schema, Client, Repository } from 'redis-om'
class Album extends Entity {}
let schema = new Schema(Album, {
  artist: { type: 'string' },
  title: { type: 'string' },
  year: { type: 'number' },
  genres: { type: 'array' },
  outOfPublication: { type: 'boolean' }
})
let client = new Client()
await client.open('redis://localhost:6379')
let repository = new Repository(schema, client)
let album, id

// create an entity and save it
album = repository.createEntity()

album.artist = "Mushroomhead"
album.title = "The Righteous & The Butterfly"
album.year = 2014
album.genres = [ 'metal' ]
album.outOfPublication = true

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// read an entity
album = await repository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')

// update an entity
album.genres = [ 'metal', 'nu metal', 'avantgarde' ]
album.outOfPublication = false

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// delete an entity
await repository.remove('01FJYWEYRHYFT8YTEGQBABJ43J')
let albums = await repository.search()
  .where('artist').equals('Mushroomhead')
  .and('title').matches('butterfly')
  .and('year').is.greaterThan(2000).returnAll()

Schemas define the fields on your entity, their types, and how they are mapped internally to Redis. By default, entities map to Hashes in Redis, but you can also map them to JSON documents:

import { Entity, Schema, Client, Repository } from 'redis-om'
class Album extends Entity {}
let schema = new Schema(Album, {
  artist: { type: 'string' },
  title: { type: 'string' },
  year: { type: 'number' },
  genres: { type: 'array' },
  outOfPublication: { type: 'boolean' }
})
let client = new Client()
await client.open('redis://localhost:6379')
let repository = new Repository(schema, client)
let album, id

// create an entity and save it
album = repository.createEntity()

album.artist = "Mushroomhead"
album.title = "The Righteous & The Butterfly"
album.year = 2014
album.genres = [ 'metal' ]
album.outOfPublication = true

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// read an entity
album = await repository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')

// update an entity
album.genres = [ 'metal', 'nu metal', 'avantgarde' ]
album.outOfPublication = false

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// delete an entity
await repository.remove('01FJYWEYRHYFT8YTEGQBABJ43J')
let albums = await repository.search()
  .where('artist').equals('Mushroomhead')
  .and('title').matches('butterfly')
  .and('year').is.greaterThan(2000).returnAll()

When you create a Schema, it modifies the Entity you handed it, adding getters and setters for the properties you define. The type those getters and setters accept and return are defined with the type property above.

Clients connect you to Redis. A Client has methods to open, close, and execute raw commands against Redis. You’ll mostly use open and close:

import { Entity, Schema, Client, Repository } from 'redis-om'
class Album extends Entity {}
let schema = new Schema(Album, {
  artist: { type: 'string' },
  title: { type: 'string' },
  year: { type: 'number' },
  genres: { type: 'array' },
  outOfPublication: { type: 'boolean' }
})
let client = new Client()
await client.open('redis://localhost:6379')
let repository = new Repository(schema, client)
let album, id

// create an entity and save it
album = repository.createEntity()

album.artist = "Mushroomhead"
album.title = "The Righteous & The Butterfly"
album.year = 2014
album.genres = [ 'metal' ]
album.outOfPublication = true

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// read an entity
album = await repository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')

// update an entity
album.genres = [ 'metal', 'nu metal', 'avantgarde' ]
album.outOfPublication = false

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// delete an entity
await repository.remove('01FJYWEYRHYFT8YTEGQBABJ43J')
let albums = await repository.search()
  .where('artist').equals('Mushroomhead')
  .and('title').matches('butterfly')
  .and('year').is.greaterThan(2000).returnAll()

A Schema and a Client are required to instantiate a Repository. Repositories provide the means to read, write, and remove entities. And the means to search for them:

import { Entity, Schema, Client, Repository } from 'redis-om'
class Album extends Entity {}
let schema = new Schema(Album, {
  artist: { type: 'string' },
  title: { type: 'string' },
  year: { type: 'number' },
  genres: { type: 'array' },
  outOfPublication: { type: 'boolean' }
})
let client = new Client()
await client.open('redis://localhost:6379')
let repository = new Repository(schema, client)
let album, id

// create an entity and save it
album = repository.createEntity()

album.artist = "Mushroomhead"
album.title = "The Righteous & The Butterfly"
album.year = 2014
album.genres = [ 'metal' ]
album.outOfPublication = true

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// read an entity
album = await repository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')

// update an entity
album.genres = [ 'metal', 'nu metal', 'avantgarde' ]
album.outOfPublication = false

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// delete an entity
await repository.remove('01FJYWEYRHYFT8YTEGQBABJ43J')
let albums = await repository.search()
  .where('artist').equals('Mushroomhead')
  .and('title').matches('butterfly')
  .and('year').is.greaterThan(2000).returnAll()

Once we have a repository, we can use it to create, read, update, and delete entities. Here I’m doing just that with a favorite album of mine by Mushroomhead:

import { Entity, Schema, Client, Repository } from 'redis-om'
class Album extends Entity {}
let schema = new Schema(Album, {
  artist: { type: 'string' },
  title: { type: 'string' },
  year: { type: 'number' },
  genres: { type: 'array' },
  outOfPublication: { type: 'boolean' }
})
let client = new Client()
await client.open('redis://localhost:6379')
let repository = new Repository(schema, client)
let album, id

// create an entity and save it
album = repository.createEntity()

album.artist = "Mushroomhead"
album.title = "The Righteous & The Butterfly"
album.year = 2014
album.genres = [ 'metal' ]
album.outOfPublication = true

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// read an entity
album = await repository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')

// update an entity
album.genres = [ 'metal', 'nu metal', 'avantgarde' ]
album.outOfPublication = false

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// delete an entity
await repository.remove('01FJYWEYRHYFT8YTEGQBABJ43J')
let albums = await repository.search()
  .where('artist').equals('Mushroomhead')
  .and('title').matches('butterfly')
  .and('year').is.greaterThan(2000).returnAll()

We can also use it to search for entities:

import { Entity, Schema, Client, Repository } from 'redis-om'
class Album extends Entity {}
let schema = new Schema(Album, {
  artist: { type: 'string' },
  title: { type: 'string' },
  year: { type: 'number' },
  genres: { type: 'array' },
  outOfPublication: { type: 'boolean' }
})
let client = new Client()
await client.open('redis://localhost:6379')
let repository = new Repository(schema, client)
let album, id

// create an entity and save it
album = repository.createEntity()

album.artist = "Mushroomhead"
album.title = "The Righteous & The Butterfly"
album.year = 2014
album.genres = [ 'metal' ]
album.outOfPublication = true

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// read an entity
album = await repository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')

// update an entity
album.genres = [ 'metal', 'nu metal', 'avantgarde' ]
album.outOfPublication = false

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// delete an entity
await repository.remove('01FJYWEYRHYFT8YTEGQBABJ43J')
let albums = await repository.search()
  .where('artist').equals('Mushroomhead')
  .and('title').matches('butterfly')
  .and('year').is.greaterThan(2000).returnAll()

And, for your copy-and-pasting convenience, here’s that sample as one big block of code:

import { Entity, Schema, Client, Repository } from 'redis-om'

class Album extends Entity {}
let schema = new Schema(Album, {
  artist: { type: 'string' },
  title: { type: 'string' },
  year: { type: 'number' },
  genres: { type: 'array' },
  outOfPublication: { type: 'boolean' }
})

let client = new Client()
await client.open('redis://localhost:6379')

let repository = new Repository(schema, client)

let album, id

// create an entity and save it
album = repository.createEntity()

album.artist = "Mushroomhead"
album.title = "The Righteous & The Butterfly"
album.year = 2014
album.genres = [ 'metal' ]
album.outOfPublication = true

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// read an entity
album = await repository.fetch('01FJYWEYRHYFT8YTEGQBABJ43J')

// update an entity
album.genres = [ 'metal', 'nu metal', 'avantgarde' ]
album.outOfPublication = false

id = await repository.save(album) // '01FJYWEYRHYFT8YTEGQBABJ43J'

// delete an entity
await repository.remove('01FJYWEYRHYFT8YTEGQBABJ43J')

let albums = await repository.search()
  .where('artist').equals('Mushroomhead')
  .and('title').matches('butterfly')
  .and('year').is.greaterThan(2000).returnAll()

And that’s Redis OM for Node.js in, as they say, a nutshell.

Wrapping up

This has been a quick overview of what Redis OM for Node.js can do. If you’d like to learn more, there’s a tutorial that will guide you through building a simple service with Redis OM. And if you want to go deeper, and you totally should, check out the README and API docs on GitHub.

Of course, this is brand new software that hasn’t been tested in the wild yet. You are that wild. Please try it out. Kick the tires. Try to break it. See what’s there and what could be there. And when you find that bug or think of that perfect feature, let me know by opening an issue or sending a pull request. Your help in making Redis OM better is sincerely appreciated. Thanks!