Tutorial
How to use Redis as a NoSQL database for a production app
March 25, 202610 minute read
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.CREATEover 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/sessionto log in.GET /api/ticketsto list and filter tickets.GET /api/tickets/:idto read one ticket.POST /api/ticketsto create a ticket.PATCH /api/tickets/:idto update a ticket.DELETE /api/tickets/:idto 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 likecreatedAt,updatedBy, andlabels. - Users are JSON documents keyed as
users:<id>, with an email-to-key lookup atusers: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=openbecomes@status:{open}in the query. - SORTABLE on
createdAtlets 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:
- Look up the user by email using the
users:email:<email>key. - Compare the password hash.
- 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/authseeds the admin user, hashes passwords, and issues session tokens.server/components/ticketsstores tickets as JSON and delegates querying to Redis Search.server/config.tscentralizes Redis, auth, and runtime settings.compose.ymlmakes 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.
