{
  "schema_version": 2,
  "id": "integrate/redisom-for-node-js",
  "title": "RedisOM for Node.js",
  "url": "https://redis.io/docs/latest/integrate/redisom-for-node-js/",
  "summary": "Learn how to build with Redis Stack and Node.js",
  "aliases": [
    "/develop/connect/clients/om-clients/stack-node/"
  ],
  "tags": [
    "docs",
    "integrate",
    "oss",
    "rs",
    "rc"
  ],
  "last_updated": "2026-09-17T14:54:53+01:00",
  "children": [],
  "page_type": "content",
  "content_hash": "1bd27909820a885003e19bcde159bab326b9b02e603104245ac0c01551974d35",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "This tutorial will show you how to build a simple, RESTful API that reads, writes, and finds data on persons (including first name, last name, and  age) using Node.js and Redis Stack. You'll also add a simple location tracking feature for a bit of extra interest. You'll be using [Express](https://expressjs.com/) and [Redis OM](https://github.com/redis/redis-om-node) to do this."
    },
    {
      "id": "what-is-redis-om",
      "title": "What is Redis OM?",
      "role": "content",
      "text": "Redis OM is an object-mapping library that provides high-level abstractions for working with Redis as a document database. You can use Redis OM to map Redis data types,specifically Hashes and JSON documents, to objects in your preferred programming language.\n\nRedis OM leverages the JSON and Redis Search features included in Redis Stack, enabling you to:\n\n- **Model your data** as objects with schemas and validation\n- **Store and retrieve** complex data structures seamlessly\n- **Search and query** your data using full-text search, numeric ranges, and geospatial queries\n- **Index your data** automatically for optimal query performance\n\nWith Redis OM, you work with familiar object-oriented patterns while Redis Stack handles the underlying data storage, indexing, and querying efficiently.\n\n> [!NOTE]\n> Redis Stack was replaced by Redis Open Source as of version 8.0."
    },
    {
      "id": "prerequisites",
      "title": "Prerequisites",
      "role": "content",
      "text": "Like anything software-related, you need to have some dependencies installed before you can get started:\n\n- [Node.js 14.8+](https://nodejs.org/en/): In this tutorial, we're using JavaScript's top-level `await` feature which was introduced in Node 14.8. So, make sure you are using that version or later.\n- [Redis Stack](https://redis.io/docs/latest/operate/oss_and_stack/install/archive/install-stack): You need a version of Redis Stack, either running locally on your machine or [in the cloud](https://redis.com/try-free/?utm_source=redisio&utm_medium=referral&utm_campaign=2023-09-try_free&utm_content=cu-redis_cloud_users).\n- [Redis Insight](https://redis.com/redis-enterprise/redis-insight/): We'll use this to look inside Redis and make sure our code is doing what we think it's doing."
    },
    {
      "id": "starter-code",
      "title": "Starter code",
      "role": "content",
      "text": "We're not going to code this completely from scratch. Instead, we've provided some starter code for you. Go ahead and clone it to a folder of your convenience:\n\n    git clone git@github.com:redis-developer/express-redis-om-workshop.git\n\nNow that you have the starter code, let's explore it a bit. Opening up `server.js` in the root we see that we have a simple Express app that uses [*Dotenv*](https://www.npmjs.com/package/dotenv) for configuration and [Swagger UI Express](https://www.npmjs.com/package/swagger-ui-express) for testing our API:\n\n\nimport 'dotenv/config'\n\nimport express from 'express'\nimport swaggerUi from 'swagger-ui-express'\nimport YAML from 'yamljs'\n\n/* create an express app and use JSON */\nconst app = new express()\napp.use(express.json())\n\n/* set up swagger in the root */\nconst swaggerDocument = YAML.load('api.yaml')\napp.use('/', swaggerUi.serve, swaggerUi.setup(swaggerDocument))\n\n/* start the server */\napp.listen(8080)\n\n\nAlongside this is `api.yaml`, which defines the API we're going to build and provides the information Swagger UI Express needs to render its UI. You don't need to mess with it unless you want to add some additional routes.\n\nThe `persons` folder has some JSON files and a shell script. The JSON files are sample persons—all musicians because fun—that you can load into the API to test it. The shell script—`load-data.sh`—will load all the JSON files into the API using `curl`.\n\nThere are two empty folders, `om` and `routers`. The `om` folder is where all the Redis OM code will go. The `routers` folder will hold code for all of our Express routes."
    },
    {
      "id": "configure-and-run",
      "title": "Configure and run",
      "role": "configuration",
      "text": "The starter code is perfectly runnable if a bit thin. Let's configure and run it to make sure it works before we move on to writing actual code. First, get all the dependencies:\n\n    npm install\n\nThen, set up a `.env` file in the root that Dotenv can make use of. There's a `sample.env` file in the root that you can copy and modify:\n\n    cp sample.env .env\n\nThe contents of `.env` looks like this:\n\n\n# Put your local Redis Stack URL here. Want to run in the\n# cloud instead? Sign up at https://redis.com/try-free/.\nREDIS_URL=redis://localhost:6379\n\n\nThere's a good chance this is already correct. However, if you need to change the `REDIS_URL` for your particular environment (e.g., you're running Redis Stack in the cloud), this is the time to do it. Once done, you should be able to run the app:\n\n    npm start\n\nNavigate to `http://localhost:8080` and check out the client that Swagger UI Express has created. None of it *works* yet because we haven't implemented any of the routes. But, you can try them out and watch them fail!\n\nThe starter code runs. Let's add some Redis OM to it so it actually *does* something!"
    },
    {
      "id": "setting-up-a-client",
      "title": "Setting up a Client",
      "role": "configuration",
      "text": "First things first, let's set up a **client**. The `Client` class is the thing that knows how to talk to Redis on behalf of Redis OM. One option is to put our client in its own file and export it. This ensures that the application has one and only one instance of `Client` and thus only one connection to Redis Stack. Since Redis and JavaScript are both (more or less) single-threaded, this works neatly.\n\nLet's create our first file. In the `om` folder add a file called `client.js` and add the following code:\n\n\nimport { Client } from 'redis-om'\n\n/* pulls the Redis URL from .env */\nconst url = process.env.REDIS_URL\n\n/* create and open the Redis OM Client */\nconst client = await new Client().open(url)\n\nexport default client\n\n\n> Remember that _top-level await_ stuff we mentioned earlier? There it is!\n\nNote that we are getting our Redis URL from an environment variable. It was put there by Dotenv and read from our `.env` file. If we didn't have the `.env` file or have a `REDIS_URL` property in our `.env` file, this code would gladly read this value from the *actual* environment variables.\n\nAlso note that the `.open()` method conveniently returns `this`. This `this` (can I say *this* again? I just did!) lets us chain the instantiation of the client with the opening of the client. If this isn't to your liking, you could always write it like this:\n\n\n/* create and open the Redis OM Client */\nconst client = new Client()\nawait client.open(url)"
    },
    {
      "id": "entity-schema-and-repository",
      "title": "Entity, Schema, and Repository",
      "role": "content",
      "text": "Now that we have a client that's connected to Redis, we need to start mapping some persons. To do that, we need to define an `Entity` and a `Schema`. Let's start by creating a file named `person.js` in the `om` folder and importing `client` from `client.js` and the `Entity` and `Schema` classes from Redis OM:\n\n\nimport { Entity, Schema } from 'redis-om'\nimport client from './client.js'"
    },
    {
      "id": "entity",
      "title": "Entity",
      "role": "content",
      "text": "Next, we need to define an **entity**. An `Entity` is the class that holds you data when you work with it—the thing being mapped to. It is what you create, read, update, and delete. Any class that extends `Entity` is an entity. We'll define our `Person` entity with a single line:\n\n\n/* our entity */\nclass Person extends Entity {}"
    },
    {
      "id": "schema",
      "title": "Schema",
      "role": "content",
      "text": "A **schema** defines the fields on your entity, their types, and how they are mapped internally to Redis. By default, entities map to JSON documents. Let's create our `Schema` in `person.js`:\n\n\n/* create a Schema for Person */\nconst personSchema = new Schema(Person, {\n  firstName: { type: 'string' },\n  lastName: { type: 'string' },\n  age: { type: 'number' },\n  verified: { type: 'boolean' },\n  location: { type: 'point' },\n  locationUpdated: { type: 'date' },\n  skills: { type: 'string[]' },\n  personalStatement: { type: 'text' }\n})\n\n\nWhen you create a `Schema`, it modifies the `Entity` class you handed it (`Person` in our case) adding getters and setters for the properties you define. The type those getters and setters accept and return are defined with the type parameter as shown above. Valid values are: `string`, `number`, `boolean`, `string[]`, `date`, `point`, and `text`.\n\nThe first three do exactly what you think—they define a property that is a [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), a [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number), or a [`Boolean`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean). `string[]` does what you'd think as well, specifically defining an [`Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of strings.\n\n`date` is a little different, but still more or less what you'd expect. It defines a property that returns a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) and can be set using not only a `Date` but also a `String` containing an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date or a `Number` with the [UNIX epoch time](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_ecmascript_epoch_and_timestamps) in *milliseconds*.\n\nA `point` defines a point somewhere on the globe as a longitude and a latitude. It creates a property that returns and accepts a simple object with the properties of `longitude` and `latitude`. Like this:\n\n\nlet point = { longitude: 12.34, latitude: 56.78 }\n\n\nA `text` field is a lot like a `string`. If you're just reading and writing objects, they are identical. But if you want to *search* on them, they are very, very different. We'll talk about search more later, but the tl;dr is that `string` fields can only be matched on their whole value—no partial matches—and are best for keys while `text` fields have full-text search enabled on them and are optimized for human-readable text."
    },
    {
      "id": "repository",
      "title": "Repository",
      "role": "content",
      "text": "Now we have all the pieces that we need to create a **repository**. A `Repository` is the main interface into Redis OM. It gives us the methods to read, write, and remove a specific `Entity`. Create a `Repository` in `person.js` and make sure it's exported as you'll need it when we start implementing out API:\n\n\n/* use the client to create a Repository just for Persons */\nexport const personRepository = new Repository(personSchema, client)\n\n\nWe're almost done with setting up our repository. But we still need to create an index or we won't be able to search. We do that by calling `.createIndex()`. If an index already exists and it's identical, this function won't do anything. If it's different, it'll drop it and create a new one. Add a call to `.createIndex()` to `person.js`:\n\n\n/* create the index for Person */\nawait personRepository.createIndex()\n\n\nThat's all we need for `person.js` and all we need to start talking to Redis using Redis OM. Here's the code in its entirety:\n\n\nimport { Entity, Schema } from 'redis-om'\nimport client from './client.js'\n\n/* our entity */\nclass Person extends Entity {}\n\n/* create a Schema for Person */\nconst personSchema = new Schema(Person, {\n  firstName: { type: 'string' },\n  lastName: { type: 'string' },\n  age: { type: 'number' },\n  verified: { type: 'boolean' },\n  location: { type: 'point' },\n  locationUpdated: { type: 'date' },\n  skills: { type: 'string[]' },\n  personalStatement: { type: 'text' }\n})\n\n/* use the client to create a Repository just for Persons */\nexport const personRepository = client.fetchRepository(personSchema)\n\n/* create the index for Person */\nawait personRepository.createIndex()\n\n\nNow, let's add some routes in Express."
    },
    {
      "id": "set-up-the-person-router",
      "title": "Set up the Person Router",
      "role": "content",
      "text": "Let's create a truly RESTful API with the CRUD operations mapping to PUT, GET, POST, and DELETE respectively. We're going to do this using [Express Routers](https://expressjs.com/en/4x/api.html#router) as this makes our code nice and tidy. Create a file called `person-router.js` in the `routers` folder and in it import `Router` from Express and `personRepository` from `person.js`. Then create and export a `Router`:\n\n\nimport { Router } from 'express'\nimport { personRepository } from '../om/person.js'\n\nexport const router = Router()\n\n\nImports and exports done, let's bind the router to our Express app. Open up `server.js` and import the `Router` we just created:\n\n\n/* import routers */\nimport { router as personRouter } from './routers/person-router.js'\n\n\nThen add the `personRouter` to the Express app:\n\n\n/* bring in some routers */\napp.use('/person', personRouter)\n\n\nYour `server.js` should now look like this:\n\n\nimport 'dotenv/config'\n\nimport express from 'express'\nimport swaggerUi from 'swagger-ui-express'\nimport YAML from 'yamljs'\n\n/* import routers */\nimport { router as personRouter } from './routers/person-router.js'\n\n/* create an express app and use JSON */\nconst app = new express()\napp.use(express.json())\n\n/* bring in some routers */\napp.use('/person', personRouter)\n\n/* set up swagger in the root */\nconst swaggerDocument = YAML.load('api.yaml')\napp.use('/', swaggerUi.serve, swaggerUi.setup(swaggerDocument))\n\n/* start the server */\napp.listen(8080)\n\n\nNow we can add our routes to create, read, update, and delete persons. Head back to the `person-router.js` file so we can do just that."
    },
    {
      "id": "creating-a-person",
      "title": "Creating a Person",
      "role": "content",
      "text": "We'll create a person first as you need to have persons in Redis before you can do any of the reading, writing, or removing of them. Add the PUT route below. This route will call `.createAndSave()` to create a `Person` from the request body and immediately save it to the Redis:\n\n\nrouter.put('/', async (req, res) => {\n  const person = await personRepository.createAndSave(req.body)\n  res.send(person)\n})\n\n\nNote that we are also returning the newly created `Person`. Let's see what that looks like by actually calling our API using the Swagger UI. Go to http://localhost:8080 in your browser and try it out. The default request body in Swagger will be fine for testing. You should see a response that looks like this:\n\n\n{\n  \"entityId\": \"01FY9MWDTWW4XQNTPJ9XY9FPMN\",\n  \"firstName\": \"Rupert\",\n  \"lastName\": \"Holmes\",\n  \"age\": 75,\n  \"verified\": false,\n  \"location\": {\n    \"longitude\": 45.678,\n    \"latitude\": 45.678\n  },\n  \"locationUpdated\": \"2022-03-01T12:34:56.123Z\",\n  \"skills\": [\n    \"singing\",\n    \"songwriting\",\n    \"playwriting\"\n  ],\n  \"personalStatement\": \"I like piña coladas and walks in the rain\"\n}\n\n\nThis is exactly what we handed it with one exception: the `entityId`. Every entity in Redis OM has an entity ID which is—as you've probably guessed—the unique ID of that entity. It was randomly generated when we called `.createAndSave()`. Yours will be different, so make note of it.\n\nYou can see this newly created JSON document in Redis with Redis Insight. Go ahead and launch Redis Insight and you should see a key with a name like `Person:01FY9MWDTWW4XQNTPJ9XY9FPMN`. The `Person` bit of the key was derived from the class name of our entity and the sequence of letters and numbers is our generated entity ID. Click on it to take a look at the JSON document you've created.\n\nYou'll also see a key named `Person:index:hash`. That's a unique value that Redis OM uses to see if it needs to recreate the index or not when `.createIndex()` is called. You can safely ignore it."
    },
    {
      "id": "reading-a-person",
      "title": "Reading a Person",
      "role": "content",
      "text": "Create down, let's add a GET route to read this newly created `Person`:\n\n\nrouter.get('/:id', async (req, res) => {\n  const person = await personRepository.fetch(req.params.id)\n  res.send(person)\n})\n\n\nThis code extracts a parameter from the URL used in the route—the `entityId` that we received previously. It uses the `.fetch()` method on the `personRepository` to retrieve a `Person` using that `entityId`. Then, it returns that `Person`.\n\nLet's go ahead and test that in Swagger as well. You should get back exactly the same response. In fact, since this is a simple GET, we should be able to just load the URL into our browser. Test that out too by navigating to http://localhost:8080/person/01FY9MWDTWW4XQNTPJ9XY9FPMN, replacing the entity ID with your own.\n\nNow that we can read and write, let's implement the *REST* of the HTTP verbs. REST... get it?"
    },
    {
      "id": "updating-a-person",
      "title": "Updating a Person",
      "role": "content",
      "text": "Let's add the code to update a person using a POST route:\n\n\nrouter.post('/:id', async (req, res) => {\n\n  const person = await personRepository.fetch(req.params.id)\n\n  person.firstName = req.body.firstName ?? null\n  person.lastName = req.body.lastName ?? null\n  person.age = req.body.age ?? null\n  person.verified = req.body.verified ?? null\n  person.location = req.body.location ?? null\n  person.locationUpdated = req.body.locationUpdated ?? null\n  person.skills = req.body.skills ?? null\n  person.personalStatement = req.body.personalStatement ?? null\n\n  await personRepository.save(person)\n\n  res.send(person)\n})\n\n\nThis code fetches the `Person` from the `personRepository` using the `entityId` just like our previous route did. However, now we change all the properties based on the properties in the request body. If any of them are missing, we set them to `null`. Then, we call `.save()` and return the changed `Person`.\n\nLet's test this in Swagger too, why not? Make some changes. Try removing some of the fields. What do you get back when you read it after you've changed it?"
    },
    {
      "id": "deleting-a-person",
      "title": "Deleting a Person",
      "role": "content",
      "text": "Deletion—my favorite! Remember kids, deletion is 100% compression. The route that deletes is just as straightforward as the one that reads, but much more destructive:\n\n\nrouter.delete('/:id', async (req, res) => {\n  await personRepository.remove(req.params.id)\n  res.send({ entityId: req.params.id })\n})\n\n\nI guess we should probably test this one out too. Load up Swagger and exercise the route. You should get back JSON with the entity ID you just removed:\n\n\n{\n  \"entityId\": \"01FY9MWDTWW4XQNTPJ9XY9FPMN\"\n}\n\n\nAnd just like that, it's gone!"
    },
    {
      "id": "all-the-crud",
      "title": "All the CRUD",
      "role": "content",
      "text": "Do a quick check with what you've written so far. Here's what should be the totality of your `person-router.js` file:\n\n\nimport { Router } from 'express'\nimport { personRepository } from '../om/person.js'\n\nexport const router = Router()\n\nrouter.put('/', async (req, res) => {\n  const person = await personRepository.createAndSave(req.body)\n  res.send(person)\n})\n\nrouter.get('/:id', async (req, res) => {\n  const person = await personRepository.fetch(req.params.id)\n  res.send(person)\n})\n\nrouter.post('/:id', async (req, res) => {\n\n  const person = await personRepository.fetch(req.params.id)\n\n  person.firstName = req.body.firstName ?? null\n  person.lastName = req.body.lastName ?? null\n  person.age = req.body.age ?? null\n  person.verified = req.body.verified ?? null\n  person.location = req.body.location ?? null\n  person.locationUpdated = req.body.locationUpdated ?? null\n  person.skills = req.body.skills ?? null\n  person.personalStatement = req.body.personalStatement ?? null\n\n  await personRepository.save(person)\n\n  res.send(person)\n})\n\nrouter.delete('/:id', async (req, res) => {\n  await personRepository.remove(req.params.id)\n  res.send({ entityId: req.params.id })\n})"
    },
    {
      "id": "preparing-to-search",
      "title": "Preparing to search",
      "role": "content",
      "text": "CRUD completed, let's do some searching. In order to search, we need data to search over. Remember that `persons` folder with all the JSON documents and the `load-data.sh` shell script? Its time has arrived. Go into that folder and run the script:\n\n    cd persons\n    ./load-data.sh\n\nYou should get a rather verbose response containing the JSON response from the API and the names of the files you loaded. Like this:\n\n\n{\"entityId\":\"01FY9Z4RRPKF4K9H78JQ3K3CP3\",\"firstName\":\"Chris\",\"lastName\":\"Stapleton\",\"age\":43,\"verified\":true,\"location\":{\"longitude\":-84.495,\"latitude\":38.03},\"locationUpdated\":\"2022-01-01T12:00:00.000Z\",\"skills\":[\"singing\",\"football\",\"coal mining\"],\"personalStatement\":\"There are days that I can walk around like I'm alright. And I pretend to wear a smile on my face. And I could keep the pain from comin' out of my eyes. But sometimes, sometimes, sometimes I cry.\"} <- chris-stapleton.json\n{\"entityId\":\"01FY9Z4RS2QQVN4XFYSNPKH6B2\",\"firstName\":\"David\",\"lastName\":\"Paich\",\"age\":67,\"verified\":false,\"location\":{\"longitude\":-118.25,\"latitude\":34.05},\"locationUpdated\":\"2022-01-01T12:00:00.000Z\",\"skills\":[\"singing\",\"keyboard\",\"blessing\"],\"personalStatement\":\"I seek to cure what's deep inside frightened of this thing that I've become\"} <- david-paich.json\n{\"entityId\":\"01FY9Z4RSD7SQMSWDFZ6S4M5MJ\",\"firstName\":\"Ivan\",\"lastName\":\"Doroschuk\",\"age\":64,\"verified\":true,\"location\":{\"longitude\":-88.273,\"latitude\":40.115},\"locationUpdated\":\"2022-01-01T12:00:00.000Z\",\"skills\":[\"singing\",\"dancing\",\"friendship\"],\"personalStatement\":\"We can dance if we want to. We can leave your friends behind. 'Cause your friends don't dance and if they don't dance well they're no friends of mine.\"} <- ivan-doroschuk.json\n{\"entityId\":\"01FY9Z4RSRZFGQ21BMEKYHEVK6\",\"firstName\":\"Joan\",\"lastName\":\"Jett\",\"age\":63,\"verified\":false,\"location\":{\"longitude\":-75.273,\"latitude\":40.003},\"locationUpdated\":\"2022-01-01T12:00:00.000Z\",\"skills\":[\"singing\",\"guitar\",\"black eyeliner\"],\"personalStatement\":\"I love rock n' roll so put another dime in the jukebox, baby.\"} <- joan-jett.json\n{\"entityId\":\"01FY9Z4RT25ABWYTW6ZG7R79V4\",\"firstName\":\"Justin\",\"lastName\":\"Timberlake\",\"age\":41,\"verified\":true,\"location\":{\"longitude\":-89.971,\"latitude\":35.118},\"locationUpdated\":\"2022-01-01T12:00:00.000Z\",\"skills\":[\"singing\",\"dancing\",\"half-time shows\"],\"personalStatement\":\"What goes around comes all the way back around.\"} <- justin-timberlake.json\n{\"entityId\":\"01FY9Z4RTD9EKBDS2YN9CRMG1D\",\"firstName\":\"Kerry\",\"lastName\":\"Livgren\",\"age\":72,\"verified\":false,\"location\":{\"longitude\":-95.689,\"latitude\":39.056},\"locationUpdated\":\"2022-01-01T12:00:00.000Z\",\"skills\":[\"poetry\",\"philosophy\",\"songwriting\",\"guitar\"],\"personalStatement\":\"All we are is dust in the wind.\"} <- kerry-livgren.json\n{\"entityId\":\"01FY9Z4RTR73HZQXK83JP94NWR\",\"firstName\":\"Marshal\",\"lastName\":\"Mathers\",\"age\":49,\"verified\":false,\"location\":{\"longitude\":-83.046,\"latitude\":42.331},\"locationUpdated\":\"2022-01-01T12:00:00.000Z\",\"skills\":[\"rapping\",\"songwriting\",\"comics\"],\"personalStatement\":\"Look, if you had, one shot, or one opportunity to seize everything you ever wanted, in one moment, would you capture it, or just let it slip?\"} <- marshal-mathers.json\n{\"entityId\":\"01FY9Z4RV2QHH0Z1GJM5ND15JE\",\"firstName\":\"Rupert\",\"lastName\":\"Holmes\",\"age\":75,\"verified\":true,\"location\":{\"longitude\":-2.518,\"latitude\":53.259},\"locationUpdated\":\"2022-01-01T12:00:00.000Z\",\"skills\":[\"singing\",\"songwriting\",\"playwriting\"],\"personalStatement\":\"I like piña coladas and taking walks in the rain.\"} <- rupert-holmes.json\n\n\nA little messy, but if you don't see this, then it didn't work!\n\nNow that we have some data, let's add another router to hold the search routes we want to add. Create a file named `search-router.js` in the routers folder and set it up with imports and exports just like we did in `person-router.js`:\n\n\nimport { Router } from 'express'\nimport { personRepository } from '../om/person.js'\n\nexport const router = Router()\n\n\nImport the `Router` into `server.js` the same way we did for the `personRouter`:\n\n\n/* import routers */\nimport { router as personRouter } from './routers/person-router.js'\nimport { router as searchRouter } from './routers/search-router.js'\n\n\nThen add the `searchRouter` to the Express app:\n\n\n/* bring in some routers */\napp.use('/person', personRouter)\napp.use('/persons', searchRouter)\n\n\nRouter bound, we can now add some routes."
    },
    {
      "id": "search-all-the-things",
      "title": "Search all the things",
      "role": "content",
      "text": "We're going to add a plethora of searches to our new `Router`. But the first will be the easiest as it's just going to return everything. Go ahead and add the following code to `search-router.js`:\n\n\nrouter.get('/all', async (req, res) => {\n  const persons = await personRepository.search().return.all()\n  res.send(persons)\n})\n\n\nHere we see how to start and finish a search. Searches start just like CRUD operations start—on a `Repository`. But instead of calling `.createAndSave()`, `.fetch()`, `.save()`, or `.remove()`, we call `.search()`. And unlike all those other methods, `.search()` doesn't end there. Instead, it allows you to build up a query (which you'll see in the next example) and then resolve it with a call to `.return.all()`.\n\nWith this new route in place, go into the Swagger UI and exercise the `/persons/all` route. You should see all of the folks you added with the shell script as a JSON array.\n\nIn the example above, the query is not specified—we didn't build anything up. If you do this, you'll just get everything. Which is what you want sometimes. But not most of the time. It's not really searching if you just return everything. So let's add a route that lets us find persons by their last name. Add the following code:\n\n\nrouter.get('/by-last-name/:lastName', async (req, res) => {\n  const lastName = req.params.lastName\n  const persons = await personRepository.search()\n    .where('lastName').equals(lastName).return.all()\n  res.send(persons)\n})\n\n\nIn this route, we're specifying a field we want to filter on and a value that it needs to equal. The field name in the call to `.where()` is the name of the field specified in our schema. This field was defined as a `string`, which matters because the type of the field determines the methods that are available query it.\n\nIn the case of a `string`, there's just `.equals()`, which will query against the value of the entire string. This is aliased as `.eq()`, `.equal()`, and `.equalTo()` for your convenience. You can even add a little more syntactic sugar with calls to `.is` and `.does` that really don't do anything but make your code pretty. Like this:\n\n\nconst persons = await personRepository.search().where('lastName').is.equalTo(lastName).return.all()\nconst persons = await personRepository.search().where('lastName').does.equal(lastName).return.all()\n\n\nYou can also invert the query with a call to `.not`:\n\n\nconst persons = await personRepository.search().where('lastName').is.not.equalTo(lastName).return.all()\nconst persons = await personRepository.search().where('lastName').does.not.equal(lastName).return.all()\n\n\nIn all these cases, the call to `.return.all()` executes the query we build between it and the call to `.search()`. We can search on other field types as well. Let's add some routes to search on a `number` and a `boolean` field:\n\n\nrouter.get('/old-enough-to-drink-in-america', async (req, res) => {\n  const persons = await personRepository.search()\n    .where('age').gte(21).return.all()\n  res.send(persons)\n})\n\nrouter.get('/non-verified', async (req, res) => {\n  const persons = await personRepository.search()\n    .where('verified').is.not.true().return.all()\n  res.send(persons)\n})\n\n\nThe `number` field is filtering persons by age where the age is great than or equal to 21. Again, there are aliases and syntactic sugar:\n\n\nconst persons = await personRepository.search().where('age').is.greaterThanOrEqualTo(21).return.all()\n\n\nBut there are also more ways to query:\n\n\nconst persons = await personRepository.search().where('age').eq(21).return.all()\nconst persons = await personRepository.search().where('age').gt(21).return.all()\nconst persons = await personRepository.search().where('age').gte(21).return.all()\nconst persons = await personRepository.search().where('age').lt(21).return.all()\nconst persons = await personRepository.search().where('age').lte(21).return.all()\nconst persons = await personRepository.search().where('age').between(21, 65).return.all()\n\n\nThe `boolean` field is searching for persons by their verification status. It already has some of our syntactic sugar in it. Note that this query will match a missing value or a false value. That's why I specified `.not.true()`. You can also call `.false()` on boolean fields as well as all the variations of `.equals`.\n\n\nconst persons = await personRepository.search().where('verified').true().return.all()\nconst persons = await personRepository.search().where('verified').false().return.all()\nconst persons = await personRepository.search().where('verified').equals(true).return.all()\n\n\n> So, we've created a few routes and I haven't told you to test them. Maybe you have anyhow. If so, good for you, you rebel. For the rest of you, why don't you go ahead and test them now with Swagger? And, going forward, just test them when you want. Heck, create some routes of your own using the provided syntax and try those out too. Don't let me tell you how to live your life.\n\nOf course, querying on just one field is never enough. Not a problem, Redis OM can handle `.and()` and `.or()` like in this route:\n\n\nrouter.get('/verified-drinkers-with-last-name/:lastName', async (req, res) => {\n  const lastName = req.params.lastName\n  const persons = await personRepository.search()\n    .where('verified').is.true()\n      .and('age').gte(21)\n      .and('lastName').equals(lastName).return.all()\n  res.send(persons)\n})\n\n\nHere, I'm just showing the syntax for `.and()` but, of course, you can also use `.or()`."
    },
    {
      "id": "full-text-search",
      "title": "Full-text search",
      "role": "content",
      "text": "If you've defined a field with a type of `text` in your schema, you can perform full-text searches against it. The way a `text` field is searched is different from how a `string` is searched. A `string` can only be compared with `.equals()` and must match the entire string. With a `text` field, you can look for words within the string.\n\nA `text` field is optimized for human-readable text, like an essay or song lyrics. It's pretty clever. It understands that certain words (like *a*, *an*, or *the*) are common and ignores them. It understands how words are grammatically similar and so if you search for *give*, it matches *gives*, *given*, *giving*, and *gave* too. And it ignores punctuation.\n\nLet's add a route that does full-text search against our `personalStatement` field:\n\n\nrouter.get('/with-statement-containing/:text', async (req, res) => {\n  const text = req.params.text\n  const persons = await personRepository.search()\n    .where('personalStatement').matches(text)\n      .return.all()\n  res.send(persons)\n})\n\n\nNote the use of the `.matches()` function. This is the only one that works with `text` fields. It takes a string that can be one or more words—space-delimited—that you want to query for. Let's try it out. In Swagger, use this route to search for the word \"walk\". You should get the following results:\n\n\n[\n  {\n    \"entityId\": \"01FYC7CTR027F219455PS76247\",\n    \"firstName\": \"Rupert\",\n    \"lastName\": \"Holmes\",\n    \"age\": 75,\n    \"verified\": true,\n    \"location\": {\n      \"longitude\": -2.518,\n      \"latitude\": 53.259\n    },\n    \"locationUpdated\": \"2022-01-01T12:00:00.000Z\",\n    \"skills\": [\n      \"singing\",\n      \"songwriting\",\n      \"playwriting\"\n    ],\n    \"personalStatement\": \"I like piña coladas and taking walks in the rain.\"\n  },\n  {\n    \"entityId\": \"01FYC7CTNBJD9CZKKWPQEZEW14\",\n    \"firstName\": \"Chris\",\n    \"lastName\": \"Stapleton\",\n    \"age\": 43,\n    \"verified\": true,\n    \"location\": {\n      \"longitude\": -84.495,\n      \"latitude\": 38.03\n    },\n    \"locationUpdated\": \"2022-01-01T12:00:00.000Z\",\n    \"skills\": [\n      \"singing\",\n      \"football\",\n      \"coal mining\"\n    ],\n    \"personalStatement\": \"There are days that I can walk around like I'm alright. And I pretend to wear a smile on my face. And I could keep the pain from comin' out of my eyes. But sometimes, sometimes, sometimes I cry.\"\n  }\n]\n\n\nNotice how the word \"walk\" is matched for Rupert Holmes' personal statement that contains \"walks\" *and* matched for Chris Stapleton's that contains \"walk\". Now search \"walk raining\". You'll see that this returns Rupert's entry only even though the exact text of neither of these words is found in his personal statement. But they are *grammatically* related so it matched them. This is called stemming and it's a pretty cool feature of Redis Stack that Redis OM exploits.\n\nAnd if you search for \"a rain walk\" you'll *still* match Rupert's entry even though the word \"a\" is not in the text. Why? Because it's a common word that's not very helpful with searching. These common words are called stop words and this is another cool feature of Redis Stack that Redis OM just gets for free."
    },
    {
      "id": "searching-the-globe",
      "title": "Searching the globe",
      "role": "content",
      "text": "Redis Stack, and therefore Redis OM, both support searching by geographic location. You specify a point in the globe, a radius, and the units for that radius and it'll gleefully return all the entities therein. Let's add a route to do just that:\n\n\nrouter.get('/near/:lng,:lat/radius/:radius', async (req, res) => {\n  const longitude = Number(req.params.lng)\n  const latitude = Number(req.params.lat)\n  const radius = Number(req.params.radius)\n\n  const persons = await personRepository.search()\n    .where('location')\n      .inRadius(circle => circle\n          .longitude(longitude)\n          .latitude(latitude)\n          .radius(radius)\n          .miles)\n        .return.all()\n\n  res.send(persons)\n})\n\n\nThis code looks a little different than the others because the way we define the circle we want to search is done with a function that is passed into the `.inRadius` method:\n\n\ncircle => circle.longitude(longitude).latitude(latitude).radius(radius).miles\n\n\nAll this function does is accept an instance of a [`Circle`](https://github.com/redis/redis-om-node/blob/main/docs/classes/Circle.md) that has been initialized with default values. We override those values by calling various builder methods to define the origin of our search (i.e. the longitude and latitude), the radius, and the units that radius is measured in. Valid units are `miles`, `meters`, `feet`, and `kilometers`.\n\nLet's try the route out. I know we can find Joan Jett at around longitude -75.0 and latitude 40.0, which is in eastern Pennsylvania. So use those coordinates with a radius of 20 miles. You should receive in response:\n\n\n[\n  {\n    \"entityId\": \"01FYC7CTPKYNXQ98JSTBC37AS1\",\n    \"firstName\": \"Joan\",\n    \"lastName\": \"Jett\",\n    \"age\": 63,\n    \"verified\": false,\n    \"location\": {\n      \"longitude\": -75.273,\n      \"latitude\": 40.003\n    },\n    \"locationUpdated\": \"2022-01-01T12:00:00.000Z\",\n    \"skills\": [\n      \"singing\",\n      \"guitar\",\n      \"black eyeliner\"\n    ],\n    \"personalStatement\": \"I love rock n' roll so put another dime in the jukebox, baby.\"\n  }\n]\n\n\nTry widening the radius and see who else you can find."
    },
    {
      "id": "adding-location-tracking",
      "title": "Adding location tracking",
      "role": "content",
      "text": "We're getting toward the end of the tutorial here, but before we go, I'd like to add that location tracking piece that I mentioned way back in the beginning. This next bit of code should be easily understood if you've gotten this far as it's not really doing anything I haven't talked about already.\n\nAdd a new file called `location-router.js` in the `routers` folder:\n\n\nimport { Router } from 'express'\nimport { personRepository } from '../om/person.js'\n\nexport const router = Router()\n\nrouter.patch('/:id/location/:lng,:lat', async (req, res) => {\n\n  const id = req.params.id\n  const longitude = Number(req.params.lng)\n  const latitude = Number(req.params.lat)\n\n  const locationUpdated = new Date()\n\n  const person = await personRepository.fetch(id)\n  person.location = { longitude, latitude }\n  person.locationUpdated = locationUpdated\n  await personRepository.save(person)\n\n  res.send({ id, locationUpdated, location: { longitude, latitude } })\n})\n\n\nHere we're calling `.fetch()` to fetch a person, we're updating some values for that person—the `.location` property with our longitude and latitude and the `.locationUpdated` property with the current date and time. Easy stuff.\n\nTo use this `Router`, import it in `server.js`:\n\n\n/* import routers */\nimport { router as personRouter } from './routers/person-router.js'\nimport { router as searchRouter } from './routers/search-router.js'\nimport { router as locationRouter } from './routers/location-router.js'\n\n\nAnd bind the router to a path:\n\n\n/* bring in some routers */\napp.use('/person', personRouter, locationRouter)\napp.use('/persons', searchRouter)\n\n\nAnd that's that. But this just isn't enough to satisfy. It doesn't show you anything new, except maybe the usage of a `date` field. And, it's not really location *tracking*. It just shows where these people last were, no history. So let's add some!.\n\nTo add some history, we're going to use a [Redis Stream](https://redis.io/docs/latest/develop/data-types/streams). Streams are a big topic but don't worry if you’re not familiar with them, you can think of them as being sort of like a log file stored in a Redis key where each entry represents an event. In our case, the event would be the person moving about or checking in or whatever.\n\nBut there's a problem. Redis OM doesn’t support Streams even though Redis Stack does. So how do we take advantage of them in our application? By using [Node Redis](https://github.com/redis/node-redis). Node Redis is a low-level Redis client for Node.js that gives you access to all the Redis commands and data types. Internally, Redis OM is creating and using a Node Redis connection. You can use that connection too. Or rather, Redis OM can be *told* to use the connection you are using. Let me show you how."
    },
    {
      "id": "using-node-redis",
      "title": "Using Node Redis",
      "role": "content",
      "text": "Open up `client.js` in the `om` folder. Remember how we created a Redis OM `Client` and then called `.open()` on it?\n\n\nconst client = await new Client().open(url)\n\n\nWell, the `Client` class also has a `.use()` method that takes a Node Redis connection. Modify `client.js` to open a connection to Redis using Node Redis and then `.use()` it:\n\n\nimport { Client } from 'redis-om'\nimport { createClient } from 'redis'\n\n/* pulls the Redis URL from .env */\nconst url = process.env.REDIS_URL\n\n/* create a connection to Redis with Node Redis */\nexport const connection = createClient({ url })\nawait connection.connect()\n\n/* create a Client and bind it to the Node Redis connection */\nconst client = await new Client().use(connection)\n\nexport default client\n\n\nAnd that's it. Redis OM is now using the `connection` you created. Note that we are exporting both the `client` *and* the `connection`. Got to export the `connection` if we want to use it in our newest route."
    },
    {
      "id": "storing-location-history-with-streams",
      "title": "Storing location history with Streams",
      "role": "content",
      "text": "To add an event to a Stream we need to use the [XADD](https://redis.io/docs/latest/commands/xadd) command. Node Redis exposes that as `.xAdd()`. So, we need to add a call to `.xAdd()` in our route. Modify `location-router.js` to import our `connection`:\n\n\nimport { connection } from '../om/client.js'\n\n\nAnd then in the route itself add a call to `.xAdd()`:\n\n\n  ...snip...\n  const person = await personRepository.fetch(id)\n  person.location = { longitude, latitude }\n  person.locationUpdated = locationUpdated\n  await personRepository.save(person)\n\n  let keyName = `${person.keyName}:locationHistory`\n  await connection.xAdd(keyName, '*', person.location)\n  ...snip...\n\n\n`.xAdd()` takes a key name, an event ID, and a JavaScript object containing the keys and values that make up the event, i.e. the event data. For the key name, we're building a string using the `.keyName` property that `Person` inherited from `Entity` (which will return something like `Person:01FYC7CTPKYNXQ98JSTBC37AS1`) combined with a hard-coded value. We're passing in `*` for our event ID, which tells Redis to just generate it based on the current time and previous event ID. And we're passing in the location—with properties of longitude and latitude—as our event data.\n\nNow, whenever this route is exercised, the longitude and latitude will be logged and the event ID will encode the time. Go ahead and use Swagger to move Joan Jett around a few times.\n\nNow, go into Redis Insight and take a look at the Stream. You'll see it there in the list of keys but if you click on it, you'll get a message saying that \"This data type is coming soon!\". If you don't get this message, congratulations, you live in the future! For us here in the past, we'll just issue the raw command instead:\n\n    XRANGE Person:01FYC7CTPKYNXQ98JSTBC37AS1:locationHistory - +\n\nThis tells Redis to get a range of values from a Stream stored in the given the key name—`Person:01FYC7CTPKYNXQ98JSTBC37AS1:locationHistory` in our example. The next values are the starting event ID and the ending event ID. `-` is the beginning of the Stream. `+` is the end. So this returns everything in the Stream:\n\n    1) 1) \"1647536562911-0\"\n      2) 1) \"longitude\"\n          2) \"45.678\"\n          3) \"latitude\"\n          4) \"45.678\"\n    2) 1) \"1647536564189-0\"\n      2) 1) \"longitude\"\n          2) \"45.679\"\n          3) \"latitude\"\n          4) \"45.679\"\n    3) 1) \"1647536565278-0\"\n      2) 1) \"longitude\"\n          2) \"45.680\"\n          3) \"latitude\"\n          4) \"45.680\"\n\nAnd just like that, we're tracking Joan Jett."
    },
    {
      "id": "wrap-up",
      "title": "Wrap-up",
      "role": "content",
      "text": "So, now you know how to use Express + Redis OM to build an API backed by Redis Stack. And, you've got yourself some pretty decent started code in the process. Good deal! If you want to learn more, you can check out the [documentation](https://github.com/redis/redis-om-node) for Redis OM. It covers the full breadth of Redis OM's features.\n\nAnd thanks for taking the time to work through this. I sincerely hope you found it useful. If you have any questions, the [Redis Discord server](https://discord.gg/redis) is by far the best place to get them answered. Join the server and ask away!"
    }
  ],
  "examples": []
}
