Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/astrxnomo/shop-microservers/llms.txt

Use this file to discover all available pages before exploring further.

The frontend is a Next.js 15 App Router application that provides the complete shopping experience for Shop Microservers: browsing a product catalog, managing a cart, authenticating, and viewing order history. It is a fully client-rendered experience — all data fetching happens in the browser via fetch calls that go through the Nginx gateway. The frontend never connects directly to any microservice; every request is routed through NEXT_PUBLIC_API_URL, which points to the gateway address. Authentication state is stored in localStorage as a raw JWT string.

Overview

Framework

Next.js 15 with App Router. Runs on port 3000 inside Docker.

UI Stack

React 19, Tailwind CSS 3, Radix UI primitives (@radix-ui/react-label, @radix-ui/react-slot), Lucide icons.

API Communication

All requests go to NEXT_PUBLIC_API_URL (default http://localhost). No direct microservice calls.

Auth Storage

JWT stored in localStorage under the key "token". Removed on logout (not yet implemented — clear manually or extend the app).

Pages

RouteFileDescription
/app/page.tsxProduct catalog grid with add-to-cart
/loginapp/login/page.tsxLogin form — stores JWT on success
/registerapp/register/page.tsxRegistration form — stores JWT on success
/cartapp/cart/page.tsxCart view with item removal and checkout
/ordersapp/orders/page.tsxOrder history list for the current user

/ — Product Catalog

Fetches GET /api/catalog/products on mount and renders a responsive grid of product cards. Each card shows the product image, category badge, name, price, and stock level. Authenticated users can add items to the cart directly from the grid. Unauthenticated users see a prompt to log in when they attempt to add an item.

/login and /register

Simple forms that call api.login() or api.register(). On success, both routes store the returned token and user object in localStorage and redirect to /.

/cart

Protected — redirects to /login if no token is found. Displays all cart items with quantity, per-item subtotal, and a grand total. Users can remove individual items or proceed to checkout, which calls api.placeOrder() and redirects to /orders on success.

/orders

Protected — redirects to /login if no token is found. Lists all orders in reverse-chronological order. Each order card shows an order ID (last 8 chars), creation date, status badge, line items, and the total.

API Client

The api object in frontend/lib/api.ts is a typed wrapper around fetch. All methods call through the gateway and handle the { success, data, error } envelope:
const BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost";

function getToken(): string | null {
  if (typeof window === "undefined") return null;
  return localStorage.getItem("token");
}

function authHeaders(): HeadersInit {
  const token = getToken();
  return token ? { Authorization: `Bearer ${token}` } : {};
}

async function request<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: {
      "Content-Type": "application/json",
      ...authHeaders(),
      ...init?.headers,
    },
  });
  const json = await res.json();
  if (!json.success) throw new Error(json.error ?? "Request failed");
  return json.data as T;
}

export const api = {
  // Catalog
  getProducts: () => request<Product[]>("/api/catalog/products"),
  getProduct: (id: string) => request<Product>(`/api/catalog/products/${id}`),

  // Cart
  getCart: () => request<Cart>("/api/cart/"),
  addToCart: (item: Omit<CartItem, "quantity"> & { quantity?: number }) =>
    request<CartItem[]>("/api/cart/items", {
      method: "POST",
      body: JSON.stringify(item),
    }),
  removeFromCart: (productId: string) =>
    request<CartItem[]>(`/api/cart/items/${productId}`, { method: "DELETE" }),

  // Orders
  getOrders: () => request<Order[]>("/api/orders/"),
  getOrder: (id: string) => request<Order>(`/api/orders/${id}`),
  placeOrder: () => request<Order>("/api/orders/", { method: "POST" }),

  // Auth
  register: (email: string, password: string) =>
    request<{ token: string; user: { id: string; email: string } }>(
      "/api/auth/register",
      { method: "POST", body: JSON.stringify({ email, password }) },
    ),
  login: (email: string, password: string) =>
    request<{ token: string; user: { id: string; email: string } }>(
      "/api/auth/login",
      { method: "POST", body: JSON.stringify({ email, password }) },
    ),
};

TypeScript Types

The api.ts module exports the following types, which are used throughout the app:
export type Product = {
  id: string;
  name: string;
  price: number;
  imageUrl: string;
  stock: number;
  category: string;
};

export type CartItem = {
  productId: string;
  name: string;
  price: number;
  quantity: number;
  imageUrl: string;
};

export type Cart = { items: CartItem[]; total: number };

export type OrderItem = {
  id: string;
  productId: string;
  name: string;
  price: number;
  quantity: number;
};

export type Order = {
  id: string;
  total: number;
  status: string;
  createdAt: string;
  items: OrderItem[];
};

Authentication Flow

1

User submits login or register form

The form calls api.login(email, password) or api.register(email, password), which posts to the gateway at /api/auth/login or /api/auth/register.
2

JWT is stored in localStorage

On a successful response, the token is persisted:
const { token, user } = await api.login(email, password);
localStorage.setItem("token", token);
localStorage.setItem("user", JSON.stringify(user));
3

Subsequent requests include the token

authHeaders() reads localStorage.getItem("token") before every request() call and injects Authorization: Bearer <token>. This happens automatically for all api.* calls — no per-call configuration needed.
4

Protected pages gate on token presence

The /cart and /orders pages check for the token in useEffect and redirect to /login if it is absent:
useEffect(() => {
  if (!localStorage.getItem("token")) {
    router.push("/login");
    return;
  }
  // fetch data...
}, [router]);

Environment Variable

NEXT_PUBLIC_API_URL
string
default:"http://localhost"
The base URL for all gateway calls. In Docker Compose this is set to http://localhost (or the host machine’s address when the gateway is exposed on port 80). Override this in your .env.local or docker-compose.yml if the gateway runs on a different host or port.

Response Handling

All API calls go through the request<T> function. It checks the success field of the response envelope and throws an Error with the error message if the call failed:
const json = await res.json();
if (!json.success) throw new Error(json.error ?? "Request failed");
return json.data as T;
Components catch this error and display it in the UI using local state (e.g., setError(err.message)).
The frontend never calls microservices directly. All traffic must go through the Nginx gateway. If you run the frontend outside of Docker and point NEXT_PUBLIC_API_URL at a direct microservice address, CORS errors and routing mismatches will occur because the services expect requests to arrive through the gateway path prefixes.

Key Dependencies

{
  "dependencies": {
    "next": "15.1.8",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "@radix-ui/react-label": "^2.1.1",
    "@radix-ui/react-slot": "^1.1.1",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^0.475.0",
    "tailwind-merge": "^2.6.0",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "tailwindcss": "^3.4.17",
    "autoprefixer": "^10.4.20",
    "postcss": "^8",
    "typescript": "^5"
  }
}

Build docs developers (and LLMs) love