Production-Ready-Product-Catalog-API: The Small Node Service That Tries to Think Like Production

A layered Express API that pairs cache-aside reads, Redis rate limiting, real database tests, and hardened SQL into a compact backend template.

8 min read • View on GitHub • More from viraj2005-doom

A compact backend machine sits on a white workbench, with a product catalog ledger at the center and four surrounding subsystems: a Redis cache drawer, a rate-limit gauge, a PostgreSQL filing cabinet, and an auth shield. Paper request cards move through the machine in a clean left-to-right flow, showing how the service checks, stores, and returns data. The image explains that production readiness is the product, not just the endpoints.
The repo is less a toy catalog than a small systems blueprint, with every operational layer visible at once.
Key Takeaways

Why This API Feels Like a Real Backend

Most sample APIs stop when the endpoints work. This one keeps going. It adds the boring parts that make software survivable in production: Redis-backed rate limiting, cache-aside reads, cache invalidation, structured errors, security middleware, multi-stage Docker, and integration tests that hit real PostgreSQL and Redis containers.

That is the editorial trick here. The repo is not trying to be a catalog app that happens to have a few extras. It is trying to be a compact template for what a serious Node service looks like once it leaves tutorial land.

The stack is straightforward on purpose: Express, PostgreSQL, Redis, JWT, bcrypt, Helmet, Jest, Supertest, and Docker. The value is not novelty. It is how many production concerns get packed into a small surface area without collapsing into a framework maze.

The Request Path: Controller, Service, Repository

The architecture follows a clean controller-service-repository split. Controllers handle HTTP shape. Services make decisions. Repositories talk to the database. That separation matters here because caching, validation, and SQL safety all live in different layers for a reason.

The request path is simple until caching and mutation enter the picture. Then the layer split becomes the whole story.

// service layer sketch
async function getAllProducts(query) {
  const cacheKey = `products:${JSON.stringify(query)}`;
  const cached = await redisClient.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const products = await productRepository.getAllProducts(query);
  await redisClient.setEx(cacheKey, 300, JSON.stringify(products));
  return products;
}

That shape is the whole point. The controller stays thin, the service owns the cache decision, and the repository stays focused on SQL. Once you have pagination, filters, and rate limits, mixing those concerns into one file becomes a maintenance tax.

The Clever Part: Cache-Aside With Aggressive Invalidations

The repository’s most interesting choice is not that it caches. It is how it invalidates. Reads use cache-aside. Updates and deletes remove the specific product key and also clear the broader product list caches. That is blunt, but it keeps stale paginated results from surviving longer than they should.

A close-up shows one edited product card triggering a chain reaction inside a cache shelf. One individual cache key is removed, then several list keys dissolve at once while the underlying database record remains stable behind them. The image explains why the repo favors broad invalidation over preserving every list cache entry after a mutation.
One change can poison many cached lists, so the code chooses the safer purge.

That tradeoff is worth noticing. Fine-grained cache retention looks elegant on paper, but it often creates uncertainty about which list views are still valid. This repo picks a simpler operational rule: when a product changes, list caches are cheap enough to throw away.

The result is easy to explain and hard to misuse. That is good engineering, especially for a template repo whose readers may adapt it under pressure.

Redis Is Doing Two Jobs Here

Redis backs both the cache and the rate limiter. That is practical, not decorative. It gives the service shared mutable state that survives restarts and works across multiple instances, which is exactly what you want when the same API node may be scaled out behind a load balancer.

The rate limiter uses increment plus expiry, so each client IP gets a moving window tracked in Redis. The API also returns standard rate-limit headers, which makes the behavior visible instead of mysterious. That is a small detail with a real UX payoff for API consumers.

Use caseWhy Redis fitsWhat would break if it were in memory
CacheFast reads for product lists and product detail lookupsEvery restart would cold-start the cache
Rate limitingShared counters across workers and instancesLimits would reset per process and become inconsistent
MutabilityCentral state for invalidation and countersHorizontal scaling would fragment behavior

SQL That Resists Its Own Footguns

The repository layer uses raw SQL, but not recklessly. It manually builds filters for search and category, then whitelists sort fields so an attacker cannot smuggle SQL through an ORDER BY parameter. That is the important distinction. This is raw SQL with guardrails, not raw SQL because the author forgot better options.

const allowedSort = ['name', 'price', 'created_at'];
const sortBy = allowedSort.includes(query.sortBy) ? query.sortBy : 'created_at';
const sortOrder = query.sortOrder === 'asc' ? 'ASC' : 'DESC';

const sql = `
  SELECT *
  FROM products
  WHERE ($1::text IS NULL OR name ILIKE $1)
  ORDER BY ${sortBy} ${sortOrder}
  LIMIT $2 OFFSET $3
`;

That pattern keeps the flexibility of hand-built SQL while avoiding one of the easiest injection mistakes. It also matches the repo’s broader philosophy: explicit over magical, observable over implicit.

What Production-Ready Means in Docker and CI

The Dockerfile uses a multi-stage build, which trims the final image down to runtime essentials. That matters less as a buzzword than as a signal. It says the project is thinking about shipping, not just running locally.

The CI pipeline is even more telling. It runs integration tests against real PostgreSQL and Redis containers. That means the code is not only checked for syntax and mocks. The actual database and cache behavior are exercised, which is where a lot of sample APIs quietly fall apart.

AreaTutorial patternThis repo
BuildSingle-stage image with dev tools left insideMulti-stage image with a slimmer runtime footprint
TestsMocks for database and cacheReal PostgreSQL and Redis containers in CI
Failure modeCode passes without proving infrastructure behaviorInfrastructure assumptions get tested before merge

How It Stacks Up Against Larger Platforms

This project lives in a narrow niche. It is smaller than Strapi, lighter than Medusa and Saleor, and more focused than Directus. Those platforms give you broad product systems or content management. This repo gives you a disciplined backend skeleton for one focused service.

ProjectPrimary purposeStackStrengthTradeoffBest for
Production-Ready-Product-Catalog-APIFocused product catalog serviceNode.js, Express, PostgreSQL, RedisClear production-minded backend blueprintNot a complete commerce suiteTeams that want a lean API template
StrapiHeadless CMSNode.jsHuge ecosystem and flexible content modelingMore platform than purpose-built serviceGeneral content and catalog management
MedusaCommerce engineNode.jsFull commerce features and extensibilityHeavier operational footprintE-commerce teams needing a platform
SaleorHeadless commercePython, Django, GraphQLStrong product and order systemDifferent stack and broader scopeComplex commerce operations
DirectusDatabase-driven APINode.jsFast database-to-API workflowLess opinionated about backend disciplineTeams exposing existing data quickly

So the comparison is not about winning. It is about category. Those tools are full platforms. This repo is closer to a blueprint for the plumbing that serious teams often want to own themselves.

Who This Repo Is Actually For

This is useful if you are a backend engineer, architect, or founder who wants a compact, production-minded template for a focused API. It is useful if you care about the shape of the system as much as the endpoints themselves.

It is less useful if you want a full e-commerce suite, a visual CMS, or a broad commerce ecosystem out of the box. The repo’s value is narrower and better defined than that. It shows how a lean Node service can behave like something you would trust in production.