Skip to main content
Wilook Backoffice - Fashion E-commerce Management
Completed
DASHBOARD

Case study

Wilook Backoffice - Fashion E-commerce Management

Admin application for a fashion e-commerce business, built in client-rendered Nuxt 4 with Vue 3, Vuetify 4 and a managed PostgreSQL database. It covers CRUD product management with advanced filters and image upload, visual outfit composition across five slots by drag and drop, and customer tracking. Data access goes through a service layer that normalises database errors, consumed by Vue composables; filters live in the URL and pagination is resolved server-side.

Duration
November 2022 to May 2023
Team
1 person
Status
Completed
Share on TwitterShare on LinkedInShare on FacebookCopy link

By the numbers

51 → 1

Queries per product grid

N+1 removed on a 50-product grid

5

Look slots

Visual outfit composition

SPA

Rendering

Nuxt 4 without SSR, Bun output

7 months

Duration

November 2022 to May 2023, solo

Where the project comes from

Before this tool, looks were composed in a shared spreadsheet. The spreadsheet served as the working interface, and every selection prepared for a customer required a sequence of manipulations that stretched the process out.

The back-office was built for that. Composing an outfit now happens on a single screen, with the product visuals in view, which has sharply cut the time it takes to put a look together. The style questionnaires filled in by customers became visible during composition, instead of being looked up separately.

The context

Wilook Backoffice is the admin tool for a fashion e-commerce business. It covers three daily jobs: keeping the product catalog current, composing outfits by pairing several items, and tracking customers along with their style preferences. It is a desktop tool, used at length and repetitively, which puts list responsiveness ahead of visual flourish.

The application is a Nuxt SPA: server rendering is off, because none of these screens has any reason to be indexed and all of them sit behind authentication. Built solo from design to deployment, during my apprenticeship at Quanted Square, from November 2022 to May 2023.

The code described here runs on current versions, Nuxt 4 and a Bun build: I have kept the codebase up to date since. Same project and same architectural decisions, not a rewrite.

A back-office is judged on unglamorous criteria, and three constraints drove the decisions.

Lists that keep growing. A fashion catalog runs to thousands of references declined across sizes and colorways. Search, sorting and pagination have to stay stable as it fills out.

Cards that are paid for in the plural. A product card fetching its own thumbnail is painless alone and ruinous by fifty. The real cost of a screen is measured in queries per render, not per component.

A back-office does not expose itself. Product and customer data must only be readable when authenticated, without breaking public image delivery along the way.

The path of a piece of data

The project’s structural decision sits in this route: a component displays, it never reaches the database.

From component to data source

display

Vue component

It renders what it is given, it never reaches the database.

state

Domain composable

One per domain: reactive state, pagination, errors.

access

Service layer

One class per domain, the only point of contact with the database.

data

Managed database

PostgreSQL, authentication and image storage.

server

Server route

The dashboard counts server-side, with the user's session.

The service base translates database errors into typed application errors: not found, network, server. No component ever has to recognise an error code from the data layer.

That boundary was not drawn for elegance. It is what allowed unit queries to be replaced by batched loads without touching a single screen.

The technical choices

Data access

One service layer per domain

An abstract class carries query execution and error translation; one class per domain inherits from it. Components only ever see domain objects and typed errors.

Filter state

Stored in the URL

A filtered view becomes an address: shareable, bookmarkable, and compatible with the back button. Nine named parameters, read back by the query instead of being duplicated in memory.

Large lists

Server pagination with infinite scroll

Twenty items per page, offset derived from the current list, concurrent calls ignored and end of list detected. The browser never holds the whole catalog.

Security

Lock at the database level, with the session

The dashboard's server route builds its access from the request cookies, never with an admin key. The lock stays the database's own, even when the computation moves server-side.

Two choices round these out. Server state lives in Vue composables, with no caching library: each domain exposes its refs for list, loading, error, total and end of list, and the scope did not justify one more layer. And the global store is reduced to what is strictly local to the screen, the filter drawer’s visibility, since everything else lives on the server or in the URL.

The architecture that follows

The route of a piece of data
dashboarddatabase lock preservedVue componentsthey render, they never reach the databaseFilters in the URLshareable view, back buttonDomain composablesreactive state · pagination · errorsService layernormalised queries and errorsServer routeaggregates, with the sessionManaged databasePostgreSQL · authentication · storage
Scroll the diagram horizontally.
No component reaches the database: that is what made the performance pass possible without touching the screens.

The look editor is the one part where the interface leads: five named slots, drag and drop from the catalog, and assigning the look to a customer. The rest of the application is deliberately plain, it is a working tool and not a showcase.

The pass that mattered most

The starting brief was “move work to the server”. The assessment showed the route under consideration did not apply here, and that the real cost was elsewhere: in the number of queries a plain render triggered.

Before

  • ✗50-product grid: 51 queries, each card fetching its own thumbnail
  • ✗Look grid: up to six sequential queries per card, stalling the render
  • ✗Lists loaded with a full select, images and descriptions included
  • ✗Dashboard: three counts from the browser, neither parallel nor awaited

After

  • ✓50-product grid: one query, the thumbnail already being in the row
  • ✓Looks hydrated in two batched queries whatever their count
  • ✓Dedicated projection for lists, reflected in a summary type
  • ✓Dashboard: one server route, three parallel counts

The server-component route was dropped on evidence, not on principle: a fragment rendered server-side by the framework has no access to the privileged data client, which is only exposed to the server layer. The benefit sought was obtained through the stack’s native path, a server route, where that access exists.

What the project does not have

There is no automated test suite in this repository. Quality control rests on strict typing, the linter’s static analysis and manual screen verification. That is a scope call owned solo, and it is also the first thing I would revisit: the service layer, isolated from the rest and with no dependency on the interface, is exactly the boundary that would make those tests cheap to write.

Where the product stands

Delivered in May 2023, after seven months. The application handles the catalog, outfit composition and customer profiles, with filters in the URL, pagination resolved by the database and reads locked at the database level. The performance pass brought list screens down to a handful of queries where they used to trigger dozens.

The interface

Wilook back-office interface showing the product list

Product catalog management with filters and infinite scroll

Lessons LearnedLessons Learned

Technical

A list's cost is paid per card, not per page

Technical

Every product thumbnail fetched its own image although the data was already in the row loaded by the list. Fifty cards, fifty-one queries. A component that queries on its own behalf costs nothing alone and everything in the plural.

Design

The URL is an underrated state store

Design

Putting filters in the URL makes them shareable and enables the back button, while removing that much state to manage in the app. Provided the query reads them back: the drawer wrote the URL, the fetch ignored it.

Technical

Infinite scroll is judged on its guards

Technical

Loading the next page is the easy part. What breaks in production is the concurrent call duplicating a page, the list overwritten instead of appended, and the end never being detected.

Navigation

Articles

Projects

Actions