Redis is one of Fast Company's Most Innovative Companies of 2026

Learn more

Tutorial

How to use Redis as a NoSQL database for a production app

March 25, 202610 minute read
William Johnston
William Johnston
TL;DR:
Use Redis as your NoSQL database when you need fast reads and writes, flexible document storage, and built-in search. In this app, each ticket lives as a JSON document, and a Redis Search index powers full-text search, TAG filtering, sorting, and pagination. Auth sessions are separate Redis keys with TTL, so login state expires automatically.
Note: This tutorial uses the code from the following git repository:

#What you'll learn

  • How to model a production app around Redis JSON documents.
  • How to create a search index with FT.CREATE over JSON documents.
  • How to query, filter, and paginate documents with FT.SEARCH.
  • How to use Redis for auth tokens with TTL.
  • Where Redis fits well as a primary database, and where persistence tradeoffs matter.

#What you'll build

You'll work with a TypeScript app that exposes:
  • POST /api/session to log in.
  • GET /api/tickets to list and filter tickets.
  • GET /api/tickets/:id to read one ticket.
  • POST /api/tickets to create a ticket.
  • PATCH /api/tickets/:id to update a ticket.
  • DELETE /api/tickets/:id to delete a ticket.

#What is a NoSQL database?

A NoSQL database stores data without requiring fixed table schemas or SQL joins. Redis fits this model because it gives you JSON documents as a first-class data type and Redis Search as a query layer on top. You model your data around your access patterns instead of normalizing it into tables.
Redis works well as a primary NoSQL database when the app has simple, predictable access patterns — read by key, filter by index, search by keyword, expire by TTL. This tutorial builds an issue tracker that uses exactly those patterns.

#Why use Redis as a NoSQL database?

Redis gives you fast reads and writes, a built-in search and indexing engine, and TTL-based expiry for short-lived state. For an app like this issue tracker, that means:
  • Each ticket is one JSON document with a single key.
  • A Redis Search index auto-indexes documents for full-text search, TAG filtering, and sorted pagination.
  • Auth sessions expire automatically without a cleanup job.

#Prerequisites

  • Bun 1.3+
  • Docker
  • A Redis instance locally

#Step 1. Clone the repo

#Step 2. Start the app with Docker

Docker is the primary run path for this tutorial.
That starts the app on http://localhost:8080 and Redis on 127.0.0.1:6379.
Note: The app uses Redis 8.0+ so JSON and Redis Search commands are available out of the box. That keeps the example close to a real production deployment, not a toy key-value sample.

#Step 3. Log in and get a bearer token

The app seeds an admin user at startup. Log in to get a session token:
The response includes a token field. Save it for the next steps:
That token is a bearer token stored in Redis with a TTL. Once it expires, the user must log in again.

#Step 4. Create a ticket

Use your token to create a ticket:
The app writes the ticket as a JSON document at tickets:<id> and Redis Search auto-indexes it. The response includes the generated id, timestamps, and createdBy.

#Step 5. List and filter tickets

The list route supports full-text search, TAG filters, and pagination — all handled server-side by FT.SEARCH:
Try different filters:
The response includes total, page, pageSize, hasMore, and a documents array of ticket summaries.

#Step 6. Update and delete a ticket

Updates are partial. This request only changes the fields you send:
The search index updates automatically when the JSON document changes.
Delete a ticket:
The app removes the JSON document, and the search index drops it.

#Step 7. Run the tests

The project includes integration and unit tests:
The integration tests start a Redis container, exercise the full CRUD flow, and verify filtering and pagination.

#How it works

#How does the data model work?

The app stores two kinds of data in Redis:
  • Tickets are JSON documents keyed as tickets:<id>. Each document contains the full ticket with metadata like createdAt, updatedBy, and labels.
  • Users are JSON documents keyed as users:<id>, with an email-to-key lookup at users:email:<email>.
  • Sessions are plain string keys at sessions:<token> with a TTL. The value is the user id.
This gives you one source of truth per entity, fast key-based lookups, and automatic expiry for sessions.

#How does the search index work?

At startup, the app creates a Redis Search index over all keys that match the tickets: prefix:
  • TEXT fields (title, description) support full-text search — when a user sends ?q=billing, Redis tokenizes the query and matches against these fields.
  • TAG fields (status, priority, assignee, labels, requesterEmail) support exact-match filtering — ?status=open becomes @status:{open} in the query.
  • SORTABLE on createdAt lets Redis sort results by creation date without loading every document.
When you call JSON.SET or JSON.DEL, the search index updates automatically. No manual index bookkeeping is needed.

#How does querying work?

The list route builds a single FT.SEARCH query from the request parameters:
Redis handles filtering, full-text matching, sorting, and pagination in one round-trip. The app maps the result documents to ticket summaries and returns them.

#How does authentication work?

Login verifies the user's credentials and creates a session token:
  1. Look up the user by email using the users:email:<email> key.
  2. Compare the password hash.
  3. Generate a UUID token and store it with SET sessions:<token> <userId> EX <ttl>.
Protected routes extract the bearer token from the Authorization header, look up the session key, and load the user. If the session key has expired, Redis returns nil and the request is rejected.

#Request flow

The app is intentionally small, but the shape is production-ready:
  • server/components/auth seeds the admin user, hashes passwords, and issues session tokens.
  • server/components/tickets stores tickets as JSON and delegates querying to Redis Search.
  • server/config.ts centralizes Redis, auth, and runtime settings.
  • compose.yml makes Docker the main run path and keeps Redis data on a volume.

#FAQ

#Can Redis be used as a primary database?

Yes. Redis can be the primary database when your app needs fast access, a flexible data model, and built-in search and indexing. This tutorial uses Redis that way for tickets and sessions.

#When should I use Redis as a NoSQL database?

Use it when your access patterns are known, low latency matters, and you want built-in search without a separate engine. Redis is a strong fit for app state, tickets, sessions, queues, and other operational data.

#How do I query JSON data in Redis?

Create a search index with FT.CREATE that maps JSON paths to field types like TEXT and TAG. Then query it with FT.SEARCH. In this app, FT.SEARCH idx:tickets "@status:{open} billing" finds open tickets matching the keyword "billing" in a single command.

#How do I create a search index for JSON documents?

Call FT.CREATE with ON JSON and a key prefix. Define each field you want to query or sort on — TEXT for full-text search, TAG for exact-match filters, and SORTABLE for ordering results. Redis maintains the index automatically as documents are created, updated, or deleted.

#What is the difference between Redis Search and manual indexes?

Manual indexes require your app to maintain sets or sorted sets alongside each document. Every create, update, and delete must update every index, which introduces race conditions and extra code. Redis Search maintains the index atomically — when you call JSON.SET, the index updates in the same operation.

#What about persistence and expiry?

Use TTL for short-lived state like sessions. For durable app data, keep Redis persistence enabled and back it with a volume or managed Redis service. This demo uses Docker volume persistence so the data survives container restarts.

#Troubleshooting

#The app starts but returns a Redis error

Check that REDIS_URL in your .env file points to a running Redis instance. This app uses Redis 8.0+ for JSON and Redis Search support. If you are using Docker, verify the container is healthy:

#The ticket list returns empty results

Make sure the search index was created at startup. The app calls FT.CREATE during initialization — if Redis was not available at startup, the index may be missing. Restart the app to recreate it. If the index exists but results are empty, create at least one ticket with POST /api/tickets.

#Docker Compose fails to start

Make sure Docker is running and that ports 8080 and 6379 are not already in use by another service.

#Next steps

#Additional resources