Skip to content

LabReserve

Web system to check availability and book labs and equipment, with automated tests.

Problem

Booking labs and equipment without a central system leads to schedule conflicts and manual control. The project also tested how to build a working app with AI agents following good engineering practices.

Decisions

  • TypeScript end to end, with React for the interface and SQLite as a simple database with no separate server.
  • Plan before code: scope split into small stories with acceptance criteria.
  • Test-first: every behavior change starts with a failing test.
  • Validation gate with typecheck, lint, build, tests and E2E before calling anything done.

Stack

  • TypeScript
  • React
  • SQLite

Result

Local system with availability search, bookings with schedule conflict detection, filters, cancellation and event history.

Learnings

Using tests, planning and review to stay in control of code written with AI agents.

How I used AI

Built with AI agents. CLAUDE.md pins down the architecture, the business rules that must stay true, test-first work and the full gate (typecheck, lint, build, tests and E2E). AGENT_ISSUES.md is the persistent handoff board between sessions. The UI delivery went through a self-audit and two independent, blind reviews run in parallel, recorded under docs/verification/; it was approved in the sixth round.

Evidence

  • ILUSTRAÇÃO · ILLUSTRATION docs/ARCHITECTURE.md web/ React + Vite, HTTP client server/ node:http, routes, validation application/ LabReserveService, contracts domain/ TimeInterval, Reservation no HTTP, React or SQLite persistence/ SQLite (node:sqlite) implements contracts ──▶ depende de · depends on - -▶ implementa · implements
    IllustrationArchitecture: modular monolith with dependencies pointing inward, as described in the repository's docs/ARCHITECTURE.md.
  • ILUSTRAÇÃO · ILLUSTRATION src/application/lab-reserve-service.ts POST /api/reservations TimeInterval.create(startAt, endAt) ISO 8601 + timezone, end > start 400 INVALID_TIME_RANGE BEGIN … COMMIT (ROLLBACK on any error) findResource(resourceId) 404 RESOURCE_NOT_FOUND findConflict(resourceId, start, end) status = 'CONFIRMED' (cancelled don't block) newStart < existingEnd && newEnd > existingStart 409 RESERVATION_CONFLICT 10:00–11:00 vs 11:00–12:00 equal limits: allowed insertReservation(reservation) status: 'CONFIRMED' insertEvent(event) type: 'RESERVATION_CREATED' 201 { "data": reservation } ──▶ segue para · proceeds to borda rosada = erro · rose border = error
    IllustrationBooking creation flow: interval validation, conflict check and atomic write of the booking and its event.
  •   overlaps(other: TimeInterval): boolean {
        return this.startAt < other.endAt && this.endAt > other.startAt;
      }

    Overlap rule in the domain: equal limits don't conflict. View in repository

  •   createReservation(input: CreateReservationInput): Reservation {
        const interval = TimeInterval.create(input.startAt, input.endAt);
        return this.repository.transaction(() => {
          if (!this.repository.findResource(input.resourceId)) {
            throw new ApplicationError('RESOURCE_NOT_FOUND', 'Recurso não encontrado.');
          }
          const conflict = this.repository.findConflict(input.resourceId, interval.startAt, interval.endAt);
          if (conflict) {
            throw new ApplicationError(
              'RESERVATION_CONFLICT',
              'O recurso já está reservado nesse período.',
              { conflictingReservationId: conflict.id },
            );
          }
          const createdAt = this.clock.now().toISOString();
          const reservation = createConfirmedReservation({
            id: this.ids.generate(), resourceId: input.resourceId,
            startAt: interval.startAt, endAt: interval.endAt, createdAt,
          });
          const event: ReservationEvent = {
            id: this.ids.generate(), reservationId: reservation.id,
            type: 'RESERVATION_CREATED', occurredAt: createdAt,
          };
          this.repository.insertReservation(reservation);
          this.repository.insertEvent(event);
          return reservation;
        });
      }

    Creation use case: conflict check and write of the booking and its event in the same transaction. View in repository

  • test('detecta sobreposição estrita e permite intervalos consecutivos', () => {
      const existing = TimeInterval.create('2026-09-16T10:00:00Z', '2026-09-16T11:00:00Z');
    
      assert.equal(
        existing.overlaps(TimeInterval.create('2026-09-16T10:30:00Z', '2026-09-16T11:30:00Z')),
        true,
      );
      assert.equal(
        existing.overlaps(TimeInterval.create('2026-09-16T11:00:00Z', '2026-09-16T12:00:00Z')),
        false,
      );
    });

    Domain test pinning the rule: partial overlap conflicts, back-to-back slots don't. View in repository

  • $ npm test
    ✔ responde 413 legível para corpos realmente grandes sem resetar a conexão (386.123ms)
    ✔ lista recursos e cria reserva pelo contrato HTTP (55.2194ms)
    ✔ lista, cancela e mostra histórico com erros estruturados (77.333ms)
    ✔ responde 404 para o histórico de uma reserva inexistente (10.1546ms)
    ✔ protege respostas e rejeita corpos que não sejam JSON (89.2057ms)
    ✔ registra falhas inesperadas sem expor seus detalhes (8.7134ms)
    ✔ mantém a resposta interna segura quando o logger falha (12.5998ms)
    ✔ informa disponibilidade, histórico global e erros de domínio pelo contrato (42.4548ms)
    ✔ cancela uma reserva confirmada uma única vez (4.9019ms)
    ✔ normaliza um intervalo ISO 8601 com offset para UTC (3.9276ms)
    ✔ rejeita um intervalo cujo término seja igual ao início (1.2695ms)
    ✔ rejeita uma data que não esteja em ISO 8601 com fuso explícito (0.5249ms)
    ✔ rejeita datas ISO 8601 sintaticamente válidas, mas inexistentes (0.415ms)
    ✔ detecta sobreposição estrita e permite intervalos consecutivos (0.5614ms)
    ✔ o projeto declara scripts reproduzíveis sem versões flutuantes (3.5348ms)
    ✔ cancelamento gera evento, libera intervalo e suporta filtros (9.6265ms)
    ✔ cria reserva e evento atomicamente, recusa conflito e aceita horário consecutivo (6.282ms)
    ✔ reverte a reserva quando a gravação do evento falha (1.5119ms)
    ✔ um banco novo recebe laboratório e equipamento determinísticos (5.8135ms)
    ℹ tests 19
    ℹ suites 0
    ℹ pass 19
    ℹ fail 0
    ℹ cancelled 0
    ℹ skipped 0
    ℹ todo 0
    ℹ duration_ms 1194.8207
    Real output of the domain, integration and API tests (node:test), run on 2026-09-30 at commit 204de08, Node 24.20.0.
  • $ npm run test:e2e
    Running 3 tests using 2 workers
    ok 1 tests\e2e\reservations.spec.ts:73:1 › consulta período com data vazia sem deixar carregamento pendente (3.0s)
    ok 2 tests\e2e\reservations.spec.ts:31:1 › cria, rejeita conflito, permite período consecutivo, cancela e mostra histórico (4.1s)
    ok 3 tests\e2e\reservations.spec.ts:85:1 › aplica filtros de reserva (3.1s)
    3 passed (12.5s)
    Real output of the Playwright E2E scenarios (Chromium), in the same run.
  • Source code, implementation plan and verification records on GitHub