I just finished a 50-day "new tech every day" series. For the next 50 days I'm doing the opposite: ONE production-grade app, one feature a day — and building both the Spring Boot backend AND the React frontend each day. This is Day 1.
The app: OrderHub, an e-commerce order-fulfillment backend that will grow into a real event-driven microservices system (Redis → Kafka → sagas → Kubernetes). But today it starts where every solid service starts: a clean REST API with a proper layered architecture.
🌐 Live UI: https://frontend-pied-six-23.vercel.app
👉 Repo (read the commits in order): https://github.com/dev48v/order-hub-from-zero
The backend: layers, one job each
HTTP → Controller → Service → Repository → Domain
thin rules interface model
Each layer only talks to the one below it. That separation is the single most important thing for a codebase that has to survive 50 days of new features — you change one layer without breaking the others.
Controller stays thin — translate HTTP, nothing else:
@PostMapping
ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest r) {
var o = service.placeOrder(r.customer(), r.item(), r.quantity());
return ResponseEntity.created(URI.create("/api/orders/" + o.getId()))
.body(OrderResponse.from(o));
}
DTOs separate the API from the domain — and validate at the boundary:
public record CreateOrderRequest(
@NotBlank String customer,
@NotBlank String item,
@Positive int quantity) {}
@Valid means a bad body is a 400 with zero code from me.
The repository is an interface — today an in-memory ConcurrentHashMap implements it; on Day 2 Spring Data JPA implements it against Postgres, and the service/controller/tests don't change a line:
public interface OrderRepository {
Order save(Order o);
Optional<Order> findById(String id);
List<Order> findAll();
}
Constructor injection wires it all together (no @Autowired in modern Spring), which also makes every class trivially unit-testable.
The frontend: React 19, modern stack
The same day ships the UI: React 19 + Vite + TypeScript + Tailwind v4, shadcn-style components. The key piece is one API module with a mock fallback:
export const api = {
listOrders: () => USING_MOCK ? mock.list() : http('/api/orders'),
placeOrder: (r) => USING_MOCK ? mock.place(r)
: http('/api/orders', { method: 'POST', body: JSON.stringify(r) }),
}
If VITE_API_URL points at the deployed backend, it calls the real API; if not, it falls back to in-memory data. That's why the live Vercel demo works even while a free-tier backend is cold-starting — the UI never breaks.
Types mirror the Java DTOs so the front and back ends can't silently drift, and the data flow is plain hooks: useEffect loads on mount, handlers place/confirm and refetch.
Why this format
A daily "new tech" series is fun but it doesn't compound — each day is disconnected. Building ONE app feature-by-feature does: by Day 50 there's a real, deployable, event-driven system you can read commit by commit. And doing BE + FE together every day is how you actually ship.
Day 2: persist orders with JPA + PostgreSQL (watch the in-memory repository get swapped with nothing above it changing).
Follow along — repo + live UI linked above. 🚀
Top comments (0)