← All workCase study · Web application

Prenota Deeva

Booking for at-home hair, beauty and make-up services in Italy: the customer picks an address, the services, a professional and a time slot. A group flow covers bookings for more people.

Role
Frontend engineer, freelance via Linko Digital
Timeline
Sep 2024 – Apr 2026
Team
Deeva backend team, Linko Digital on tracking and A/B tests
Live
prenota.deeva.it

Problem

Deeva sends hairdressers, beauticians and make-up artists to the customer’s home. Bookings used to happen on WhatsApp. The customer wrote a message, and the staff checked availability and confirmed by hand.

The goal was a booking flow that customers complete on their own. They pick an address, the services, a professional and a time slot, and they can use a coupon or a gift card. A group flow covers bookings for more people at the same address. Payment happens at the appointment, by card or Satispay, so the app does not handle it. Customers also get a personal area with their bookings and details.

Architecture

The app is a single-page React app hosted on Render. It talks to Deeva’s REST API with a bearer token. The router loads the session before the first render. Google Places completes the address, GTM and VWO handle tracking and A/B tests, and Trustpilot shows reviews. When a booking is created, the API returns the URL of the confirmation page.

The booking SPA calls Deeva's REST API through a Zod-validated client, and uses Google Places, GTM, VWO and Trustpilot.Booking SPAReact · router loadersTanStack Query · ZustandAPI clientAxios + Zod~20 endpointsREST APIDeevaconfirmation URLDataDeevabearerGoogle Placesaddress searchGTM · VWOtracking · A/B testsTrustpilotreviewsRenderhosts the SPAbuilt by meowned by others
The app is a single-page React app hosted on Render. The router loads the session before the first render. When a booking is created, the API returns the URL of the confirmation page.

Scope

Mine

  • The booking app: the four flows, the checkout and the personal area
  • A typed client with Zod validation on about 20 endpoints
  • Session and data loading in the router
  • Analytics events tied to each step of the funnel

Other teams

  • REST API and data model (Deeva)
  • Tag configuration and A/B tests (Linko Digital)

Key decisions

One pattern per domain: schema, service, hook

Why. The API changed often during the project. Every response goes through an Axios wrapper that parses it with Zod, so the types match what the server really sends. Each domain has the same three files, so a new endpoint is quick to add and easy to review.

Trade-off. Each endpoint needs more boilerplate, and a failed parse stops the screen. A mismatch with the backend has to be fixed before it ships.

Generate the funnel routes from a typed list

Why. The four flows share most steps, but not all of them. The routes come from one list, and TypeScript derives the valid URLs from it. A link to a step that doesn’t exist fails the type check. A flow drops a shared step with one line.

Trade-off. The configuration is abstract. A new developer has to read the types to see the full list of URLs.

Load the session in the router

Why. The router loader fetches the user before the page renders and stores it in the TanStack Query cache. Pages read it from the cache, so there is no loading flash and no second request.

Trade-off. The token lives in localStorage. It is simple, but any script on the page can read it.

In code

The funnel routes, simplified:

appFlowRoutes.tsillustrative
const mainRoutes = [
  "location",
  "participants",
  "participants-salotto",
  "category-salotto",
  "category",
] as const;

const flows = [
  {
    flow: "beauty",
    routes: [
      ...withoutSteps(mainRoutes, ["category-salotto", "participants-salotto"]),
      "/booking/beauty/service",
      "/booking/beauty/address",
      "/booking/beauty/data",
      "/booking/beauty/checkout",
    ],
  },
  // hair, make-up and the group flow follow the same shape
] as const;

// Every valid URL, derived from the list above
type FlowRoute = (typeof flows)[number]["routes"][number];

const next: FlowRoute = "/booking/beauty/checkout"; // ok
const typo: FlowRoute = "/booking/beauty/payment"; // type error

Results

4
booking flows (hair, beauty, make-up, group) from one typed route list
~20
API endpoints behind a validated client
713 / 751
commits on the frontend are mine

Customers now book without writing to the staff. The app has been in production since 2024, and the client’s own developer joined the codebase in 2026. Since March 2026 an A/B test runs on the funnel. I can’t share usage numbers.