I have used Redis in production for years. In a previous role, our stack used Redis 6 on Azure Cache for Redis with a Spring Boot backend and Jedis. It worked, but advanced capabilities often came with extra decisions around cost, packaging, and service tier selection.
Looking back, that tradeoff may also help explain some of the platform direction we are seeing now, including the move toward Azure Managed Redis and a clearer separation in positioning and capabilities.
If we wanted richer search behavior, that typically pushed us toward higher service tiers and additional operational planning. For side projects and experiments, that friction was enough to keep many ideas in the "maybe later" bucket.
That context is why this project exists.
Redis is often introduced as "just a cache," but that framing misses how far the platform has evolved. In this project, I treated Redis 8 as the primary operational data engine for a full-stack movie application: document storage, full-text search, aggregations, and time-series telemetry.
The result is a practical reference implementation, not a toy script. The app supports:
- JSON-backed movie records
- Full-text and faceted search
- Numeric filtering and sorting
- Aggregation dashboards
- Time-series event tracking
- CRUD workflows in a React UI
This article is deliberately verbose and implementation-heavy, but with a practical story arc: what was painful before, what changed in Redis 8, and what that change looks like in a real app.
If you want the full project, the sample repo is here: GitHub repository.
TL;DR
- I used to treat Redis primarily as a fast key-value/cache layer in Redis 6 era workloads.
- Redis 8 made it easier to approach Redis as a multi-model operational data backend.
- I built a Movie Library app to test this directly with RediSearch, RedisJSON, and RedisTimeSeries.
- The result: one Redis service powers CRUD, full-text search, analytics, and time-series event tracking.
Why This Was a Big Shift for Me
Coming from Redis 6 + managed cloud usage, I was used to a split between "simple Redis usage" and "advanced Redis usage." The second path usually meant more planning around feature availability, pricing, and platform choices.
For teams with budget and clear production requirements, that can be reasonable. For learning, prototyping, and internal tools, it can be a blocker.
With this project, I wanted to test whether Redis 8 reduced that friction enough to change day-to-day developer behavior.
In practice, the setup is intentionally simple: one Redis 8 container, one Node.js API container, and one React frontend container. If you want to inspect the exact Compose and Docker configuration, it is easier to browse it directly in the GitHub repository than to repeat it inline here.
No custom module loading is required in this setup. The backend starts, connects, creates a search index, and serves traffic. Operationally, that dramatically improves first-run experience for learning projects and demos, especially compared with the "figure out modules first, build app second" workflow many of us had before.
The Build: A Full-Stack Movie Library
This is a three-container architecture:
-
redis(port6379) -
backendNode.js + Express API (port3001) -
frontendReact + Vite app (port5173)
Backend stack details:
-
expressfor API routes -
redisofficialnode-redisclient (^4.7.0) -
zodfor request/query validation -
express-rate-limitfor traffic throttling -
morganfor request logging
Frontend stack details:
- React 19
- React Router 7
- Redux Toolkit
- Recharts for charts
- Tailwind CSS + shadcn/ui components
RedisJSON, CRUD, and Search UX
I started with the baseline product loop every app needs: create, read, update, delete, then make discovery pleasant with search and filters.
One early lesson: my first search experience felt "technically correct" but practically flat. Results came back, but relevance was not great for title-heavy queries. Giving title a higher search weight immediately improved that, and it reminded me that search quality is mostly about thoughtful schema and scoring choices, not just endpoint wiring.
Data Model: RedisJSON as the Source of Truth
Each movie is stored as a JSON document at key pattern movie:{id} using RedisJSON. A typical record includes core metadata such as title, plot, genres, year, rating, votes, cast, director, runtime, language, poster, and tags. The exact seed data and JSON shape are available in the GitHub repository.
The backend seed process loads real movie records from movies.json, then generates synthetic titles, plots, cast lists, and metadata until the dataset reaches 500 movies. That larger cardinality makes filtering, sorting, and aggregation behavior more visible than a tiny static set, which is important if you want search and dashboard behavior to feel believable.
Search Design: How It Actually Works
On startup, the API creates RediSearch index idx:movies over JSON documents with prefix movie:.
Field mapping used by the app:
-
$.title->TEXT(WEIGHT 2) -
$.plot->TEXT -
$.director->TEXT -
$.cast[*]->TEXT -
$.genres[*]->TAG -
$.tags[*]->TAG -
$.language->TAG -
$.year->NUMERIC SORTABLE -
$.rating->NUMERIC SORTABLE -
$.votes->NUMERIC SORTABLE
Two practical implications:
- Weighting
titlehigher thanplotimproves relevance for title-driven queries. - Marking numeric fields sortable enables efficient server-side ordering for UX controls like "top rated" or "newest first".
I also found that getting sortable numeric fields right up front saved rework later. In earlier projects, I had deferred sorting strategy and paid for it with awkward API changes once product requirements became concrete.
API Surface and Behavior
Health and operational endpoints
-
GET /healthpings Redis and returns service state. - Every non-health request increments
ts:activityusing RedisTimeSeries for global API throughput telemetry.
CRUD endpoints
-
POST /moviescreates a record with generatedtt...style ID. -
GET /movies/:idfetches one movie. -
PUT /movies/:idupdates an existing movie (404 if absent). -
DELETE /movies/:iddeletes a movie (204 on success).
Validation is done with Zod. Example constraints include:
-
yearmust be integer between1888and2030 -
ratingmust be between0and10 -
votesmust be non-negative integer -
postermust be URL (or empty string)
Search endpoint
GET /movies/search supports combined full-text + structured filtering + pagination + sorting.
Supported query params:
qgenretaglanguage-
yearFrom,yearTo -
minRating,maxRating -
sortBy(rating | year | votes) -
sortOrder(ASC | DESC) -
limit(1-100) -
offset(>= 0)
A representative request would be a search for shawshank, filtered to the Drama genre, constrained to movies with rating >= 8, and sorted by rating descending.
The backend dynamically composes RediSearch syntax such as:
-
@genres:{Drama}for tag filtering -
@rating:[8 +inf]for numeric thresholds - Combined query form
(shawshank) @genres:{Drama} @rating:[8 +inf]
Input hardening detail: tag-like fields are escaped before interpolation to reduce query parser edge cases from punctuation.
Every successful search also increments ts:searches, which later powers trend charts.
The practical effect is that product behavior feeds analytics behavior automatically: users search, and you immediately gain a signal you can graph and monitor.
That "single action, dual value" pattern was one of my favorite outcomes in this build. In previous systems, I often had to bolt analytics on after core features shipped. Here, product events and telemetry evolved together.
Analytics with FT.AGGREGATE
The analytics endpoints push computation to Redis instead of pulling records into Node and reducing in application code.
This was the moment the architecture clicked for me. In older Redis usage patterns, I would usually pull records into the service and aggregate there. It works, but it adds code paths, memory overhead, and maintenance burden. Using server-side aggregation simplified both the implementation and the mental model.
Implemented operations include movie counts by genre, average rating by genre, and decade-based grouping derived from the release year. The exact query shapes are in the GitHub repository, but the important point here is that the aggregation work stays inside Redis instead of moving into application-side loops.
Exposed API routes:
GET /analytics/genresGET /analytics/ratingsGET /analytics/decadesGET /analytics/top-rated
/analytics/top-rated uses FT.SEARCH with SORTBY rating DESC and LIMIT 0 10 to return top titles.
I kept this endpoint intentionally simple because "top rated" is one of those deceptively expensive features if you do it repeatedly in the application layer under load.
Time-Series Telemetry with RedisTimeSeries
The project tracks three classes of events:
- Search volume (
ts:searches) - API activity (
ts:activity) - Per-movie views (
ts:movie:views:{id})
Write examples in this app:
-
POST /movies/:id/view->TS.ADD ts:movie:views:{id} * 1 - Search requests ->
TS.ADD ts:searches * 1 - Non-health API requests ->
TS.ADD ts:activity * 1
Read and downsample examples:
GET /movies/:id/views?bucket=3600000GET /timeseries/searches?bucket=3600000GET /timeseries/activity?bucket=86400000
All of these rely on TS.RANGE with AGGREGATION SUM and a configurable bucket size (default 3,600,000 ms, i.e. 1 hour).
If a series key does not yet exist, the API returns [] rather than a hard error, which simplifies frontend state handling.
This small API decision turned out to matter a lot in UI polish. Returning empty arrays lets charts render gracefully on first use and avoids noisy error states for a perfectly valid condition: "no data yet."
Reliability and Runtime Safeguards
This sample includes several practical safeguards that are easy to forget in demos:
- Redis connection retry loop (
15retries,2sdelay) - Backend waits on Redis service health in Compose
- Backend healthcheck probes
GET /health - Rate limiting:
100requests/minute per IP - CORS scoped to frontend origin
These are not enterprise-hardening substitutes, but they are meaningful defaults for local and staging environments.
I added the Redis retry loop after seeing the classic local race: API container starts milliseconds before Redis is ready, then fails fast. With retries in place, startup became boring in the best way.
Frontend Workflow and UX Model
The React app implements pages for:
- Search and filter
- Add movie
- Edit movie
- Movie detail
- Analytics dashboard
- Time-series dashboard
The search experience combines:
- Text query
- Faceted filtering (genre, tag, language)
- Numeric ranges (year, rating)
- Server-side sorting and pagination
State is managed with Redux Toolkit slices and async APIs per feature (movies, filters, analytics, timeseries).
In practice, this keeps query-building deterministic and enables consistent "URL-ish" state transitions between views.
Seed Strategy: Why the Data Looks Realistic Enough
The seed script does more than static insertion:
- Starts from curated real movie examples
- Expands to 500 documents with synthetic generation
- Adds 30 days of search/activity signals
- Adds per-movie view entries with probabilistic sparsity
This matters because aggregation and charting workflows are often misleading when driven by tiny uniform datasets.
Running the Project
You can run the project fully in Docker or split it into local frontend/backend development against Redis in Docker. Rather than duplicate the setup commands throughout the article, I would point readers to the GitHub repository for the latest start-up steps, project structure, and environment notes.
Default endpoints:
- API:
http://localhost:3001 - Frontend:
http://localhost:5173
What This Project Demonstrates Clearly
- Redis can act as a multi-model operational backend in a single service boundary.
-
FT.SEARCH+FT.AGGREGATEcover a surprisingly wide analytics/search spectrum without a second database. - RedisTimeSeries reduces bespoke metrics plumbing for product-level event trends.
- A Node.js + React team can adopt this incrementally: start with JSON storage, then add search, then aggregations, then telemetry.
Gaps and Next Steps
The sample is intentionally practical, but not production-complete. Major follow-ups would be:
- Authentication/authorization
- Integration and load tests
- Cursor-based pagination for high offsets
- Background jobs for denormalized materializations
- Observability beyond logs (traces, structured metrics)
- Optional vector similarity for recommendation UX
Final Takeaway
Redis 8 becomes most compelling when you evaluate it as a system-building platform rather than a key-value primitive. In this movie app, a single Redis deployment handles:
- document persistence
- search relevance and faceting
- aggregation queries
- time-series telemetry
with straightforward operational wiring in Docker and a conventional Node/React stack.
If you want a concrete way to learn Redis beyond cache tutorials, this architecture is a solid, extensible starting point.
If your own Redis history looks like mine (fast cache first, advanced features later), Redis 8 is worth a fresh look with a project that exercises multiple features end-to-end.
Personally, this project changed how I scope Redis in new designs. I still use it as a cache when that is the right fit, but I now consider it much earlier as an operational data layer when search, aggregations, and event trends are part of the product surface.
Repository
The full sample project is available here: https://github.com/ykpraveen/rediseach-sample.
If you publish updates, that repository is also the best place to keep the article in sync with the latest code, commands, and configuration.
Questions, issues, or ideas for extending the sample are welcome.
If you publish your own Redis 8 build, share it. I would love to compare approaches, especially around vector search, ranking strategies, and production hardening patterns.
Top comments (0)