DEV Community

Cover image for Redis 8 in Practice: Building a Full-Stack Movie Library with Search, JSON, Time Series, and Real API Workloads
Praveen Yadav
Praveen Yadav

Posted on

Redis 8 in Practice: Building a Full-Stack Movie Library with Search, JSON, Time Series, and Real API Workloads

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:

  1. redis (port 6379)
  2. backend Node.js + Express API (port 3001)
  3. frontend React + Vite app (port 5173)

Backend stack details:

  • express for API routes
  • redis official node-redis client (^4.7.0)
  • zod for request/query validation
  • express-rate-limit for traffic throttling
  • morgan for 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:

  1. Weighting title higher than plot improves relevance for title-driven queries.
  2. 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 /health pings Redis and returns service state.
  • Every non-health request increments ts:activity using RedisTimeSeries for global API throughput telemetry.

CRUD endpoints

  • POST /movies creates a record with generated tt... style ID.
  • GET /movies/:id fetches one movie.
  • PUT /movies/:id updates an existing movie (404 if absent).
  • DELETE /movies/:id deletes a movie (204 on success).

Validation is done with Zod. Example constraints include:

  • year must be integer between 1888 and 2030
  • rating must be between 0 and 10
  • votes must be non-negative integer
  • poster must be URL (or empty string)

Search endpoint

GET /movies/search supports combined full-text + structured filtering + pagination + sorting.

Supported query params:

  • q
  • genre
  • tag
  • language
  • 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/genres
  • GET /analytics/ratings
  • GET /analytics/decades
  • GET /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=3600000
  • GET /timeseries/searches?bucket=3600000
  • GET /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 (15 retries, 2s delay)
  • Backend waits on Redis service health in Compose
  • Backend healthcheck probes GET /health
  • Rate limiting: 100 requests/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

  1. Redis can act as a multi-model operational backend in a single service boundary.
  2. FT.SEARCH + FT.AGGREGATE cover a surprisingly wide analytics/search spectrum without a second database.
  3. RedisTimeSeries reduces bespoke metrics plumbing for product-level event trends.
  4. 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)