Skip to main content
Back to Projects
E-Commerce API logo

E-Commerce API

Backend Service

E-commerce API architecture diagram E-Ticaret API Arayüzü olmayan bir servis — sözleşme, kimlik ve gözlemlenebilirlik Express + Prisma · Docker ile paketlenir Swagger dokümanı kodun yanında üretilir İstemci web · mobil · üçüncü taraf tek sözleşme, tek doküman Ara katman zinciri helmet → cors → rate limit → JWT doğrulama → rol kontrolü → Zod şema doğrulama Hata gövdesi tek biçimde; log alanları fast-redact ile maskelenir, token ve parola log'a girmez. 15 ROTA GRUBU auth product category producer tag cart order coupon address wishlist review webhook auditLog health /metrics Prisma · PostgreSQL 18 model · 12 migration ürün, varyant görseli, kategori, üretici, etiket, kullanıcı, sipariş, kupon, yorum, denetim kaydı Kimlik: JWT + refresh + OAuth kısa ömürlü erişim token'ı, ayrı tabloda refresh token arctic ile sağlayıcı akışı · OAuthAccount modeli parolalar bcrypt ile saklanır Gözlemlenebilirlik: winston ile yapılandırılmış log, morgan ile istek kaydı, prom-client ile metrik ucu. Arayüzü olmayan bir serviste "çalışıyor mu" sorusunun tek cevabı bunlar.
A headless Express + Prisma service. Every request passes the same middleware chain: helmet, cors, rate limit, JWT verification, role check and Zod schema validation. Fifteen route groups map onto eighteen Prisma models; identity is JWT plus refresh plus OAuth, and observability comes from winston, morgan and prom-client. The error envelope is uniform and log fields are redacted.

An e-commerce backend with no interface: 15 route groups, 18 Prisma models, identity through JWT and refresh tokens, provider sign-in, rate limiting, an audit trail and a metrics endpoint. The contract is generated from Swagger and the service is packaged with Docker.

Technologies Used

  • Node.js
  • Express
  • TypeScript
  • Prisma
  • PostgreSQL
  • Zod
  • JWT
  • Swagger
  • Docker

Key Features

  • 15 route groups — catalogue, cart, orders, coupons, reviews, wishlist, webhooks, audit
  • JWT access token + refresh token in its own table + provider sign-in
  • Order status is defined in a separate state machine, not inside controllers
  • Protection against repeated requests — the same request is not processed twice
  • Structured logs, request identifiers and redaction; tokens and passwords never enter the log
  • A Prometheus-compatible metrics endpoint behind authentication

Development Time

2 months

Problem

In an application with an interface, the answer to "is it working" is the screen. In a service with no interface there is no such answer: the service can be up and behaving wrongly, and only the consumer notices — usually late. So the real problem here is not business logic but contract and visibility. Second, in an e-commerce API a repeated request is ordinary: the network drops, the client retries, the payment provider sends the same notification a second time. If "what happens if this is processed twice" is answered separately at every endpoint, one endpoint eventually gets forgotten.

Users

Two kinds of consumer. The direct client — a web or mobile interface: reads the catalogue, writes carts and orders, manages sessions. The server-side consumer — a payment or shipping provider: sends webhooks, proves its identity with a shared secret, and may repeat the same notification. There is a third party as well: whoever operates the service, whose need is not a screen but logs, metrics and an audit trail.

My Role

Single developer. Data model and migrations, the middleware chain, identity and authorisation layer, order state machine, audit and observability layer, the Swagger contract and the Docker package.

Architecture

A single service on Express; a request passes through a fixed middleware chain: security headers, resource-sharing policy, request identifier, rate limiting, JWT verification, role check, ownership check and Zod schema validation. At the end of the chain the controller only runs business logic; error catching and the shape of the error body are collected in a single error middleware, so every endpoint produces the same error envelope. Order status is not inside controllers but defined in a separate state machine module — which state can move to which lives in one place. Repeated requests are handled by a dedicated idempotency middleware, so no endpoint invents its own solution. On the data side, Prisma and PostgreSQL; the schema carries a separate search vector column for product search, and listing endpoints use cursor-based pagination rather than page numbers. Observability has three parts: structured logs, request logging and a metrics endpoint.

Key Decisions

  1. Idempotency lives in middleware, not inside endpoints

    A repeated request is not specific to one endpoint; adding to a cart, creating an order and processing a webhook share the same problem. Once the solution is in middleware, no endpoint can forget it — the protection is attached to the route definition, not to a controller's discipline.

    Trade-offThe client has to generate an idempotency key and send the same key on retry; a client that does not cannot benefit from the protection. The store of processed keys also grows continuously and has to be cleaned up.

  2. Order status is a separate module, not inside controllers

    The order lifecycle is where the system branches most: payment, preparation, shipping, delivery, cancellation and returns. If the transition rules are scattered across controllers, the same rule gets written differently in several places and drifts over time. Collecting them in one module means valid transitions can be read from one place.

    Trade-offThe rule lives in the application, not the database: an update made directly in SQL can bypass the state machine. Just as I reduced the overlap rule to a database constraint in Ustura, transitions could have been enforced at the data level here; they were not.

  3. The refresh token is kept in its own table

    The access token is short-lived and stateless; the refresh token exists as a record in the database. The reason is revocability: a stateless refresh token stays valid until it expires, leaving no way to close a stolen session. Provider sign-ins are also linked through a separate account model, so one user can carry more than one sign-in method.

    Trade-offEvery refresh request means a database read — the scaling advantage of a stateless design is deliberately given up here. The table also grows over time and expired records have to be cleaned up.

  4. Metrics and documentation endpoints were not left open

    The metrics endpoint describes the system's internal state and the Swagger interface describes the whole contract. Both are reconnaissance material for an attacker. Both were put behind authentication; the webhook endpoint is additionally verified with a shared secret.

    Trade-offClosed documentation means an extra access step for a developer who wants to consume the API. Protecting the metrics endpoint also requires separate configuration on the collector side — it does not work directly with a collector that does not support authentication.

  5. Log fields are redacted

    Writing a request body to the log makes debugging easier, but the same body can contain a password, a token or an address. Redaction happens at write time and per field; a sensitive field never enters the log rather than being cleaned up afterwards.

    Trade-offThe list of fields to redact is maintained by hand; a new sensitive field has to be added to it and nothing reminds you to do so. A redacted field is also invisible while debugging, so some errors cannot be traced through the log.

Stack Rationale

Express was chosen so the middleware chain stays readable: what a request passes through, and in what order, can be followed by reading the code — and that means security decisions stay visible. Prisma keeps the relations and migration history of an 18-model schema traceable; the schema file doubles as documentation for the data model. Zod collects request-body validation in a single middleware while also producing types from the same schema. The Swagger contract is written next to the code as comments — which makes it harder for a separate document to drift away from the code. Docker makes the service a single runnable unit together with its dependencies; in a service with no interface, "it works on my machine" means nothing.

Security

The middleware chain starts with security headers and the resource-sharing policy is explicitly defined. Rate limiting is configured per endpoint and is tighter on identity endpoints. Passwords are stored with bcrypt, the access token is short-lived, and because the refresh token is kept in the database it can be revoked. Authorisation has three levels: authentication, role check and resource ownership check — so even a user with the right role cannot edit someone else's record. The webhook endpoint is verified with a shared secret. File uploads pass through a separate middleware. Metrics and Swagger endpoints sit behind authentication. Changes are written to a separate audit log model; log fields are redacted at write time.

Performance

This is a backend service; a page-speed measurement is meaningless and there is no stored load-test output, so no number is written here. Structurally there are three decisions: listing endpoints use cursor-based pagination, so query cost stays constant on deep pages; a separate search vector column is kept in the schema for product search, so search does not scan text on every request; and because the metrics endpoint exposes values such as request count and duration, the service's behaviour can be followed by measurement rather than guesswork.

Results

Routes
15 route groups — identity, catalogue, cart, orders, coupons, reviews, addresses, wishlist, webhooks, audit, health, metrics
Schema
18 Prisma models · 11 migrations
TypeScript source files
99
Middleware
15 middleware modules — identity, authorisation, ownership, rate limiting, idempotency, validation, redaction, errors
Schema history
Migration dates run 2026-04 → 2026-06; the repository carries a single import commit
Architectural guarantee
Idempotency is defined in middleware; no endpoint can bypass the protection
Architectural guarantee
Metrics and documentation endpoints are not reachable without authentication

What I Would Do Differently

There are no automated tests in this repository, and for a backend service that is the most expensive gap. The easiest and most valuable place to test is obvious: the order state machine is already a pure module, testable as input and output, and it carries the most branching rule in the system. Today I would start there, then verify the idempotency middleware with the same request sent twice. The second point is where the state rule lives: I kept transitions in the application, whereas in Ustura reducing the overlap rule to a database constraint applied the protection to every write path at once. Here an update made directly in SQL can bypass the state machine; today I would at least add a check constraint preventing an invalid state.

Backend service

Let's find where to start

Tell me what you want to build; I'll tell you up front how long it takes and what the sensible first step is.