Back to all Projects

Ai Recipe Generator

Ai Recipe Generator featured image
  • NestJs

  • NextJs

  • Typescript

  • Fastify

  • Auth

Tech Stack & Features

  • NestJs
  • NextJs
  • ShadCN / Tailwind

Integrations:

  • NestJs

  • NextJs

  • Typescript

  • Fastify

  • Auth

About this Project

Smart Recipes — Full-stack web app for managing a pantry and generating AI recipes from ingredients. Users sign up, add pantry items, select ingredients, and get AI-generated recipes they can save and manage. Backend (NestJS, TypeScript) Domain-driven layout: Separate domains for auth, users, pantry, recipes, ingredients, and AI, each with controllers, services, repositories, and shared types. Auth: JWT access tokens plus HTTP-only refresh cookies, rotation on refresh, Argon2 password hashing, and configurable CORS/origin checks. Data layer: Prisma with a shared unit-of-work and repository interfaces so services stay testable and transactions are explicit. Validation: TypeBox schemas on routes and a shared validation pipeline; structured error codes and filters (Prisma, validation, unhandled) with safe, logged error responses. AI integration: Pluggable provider (e.g. Gemini/GROQ/Ollama) for recipe generation, wired behind a single service interface. Frontend (Next.js 16, React 19) App Router: Route groups for auth vs protected areas, client-side auth init and protected layout so session is restored and unauthenticated users are redirected. Session handling: Access token in memory, refresh via cookie; AuthManager handles init, proactive refresh, and 401 retry so the app works across subdomains. Feature-first structure: Features own components, API modules, and Zustand stores; shared UI (Radix/shadcn), routing config, and axios interceptors keep patterns consistent. Forms and UX: react-hook-form + Zod, reusable dialogs (add pantry item, confirm delete), responsive layout, and accessible forms and controls. DevOps / deployment Backend: Runs in Docker; listens on 0.0.0.0 for reverse proxies; Prisma migrations run on startup (prisma migrate deploy) for zero-downtime schema updates. Env: Single validated env schema (Zod) on the backend; frontend uses NEXT_PUBLIC_API_URL for the API base.

Challenges

  • Domain boundaries

    Deciding what belongs in which domain (auth, users, recipes, etc.) and keeping cross-domain dependencies one-way so the codebase doesn’t turn into a tangled graph.

  • Layering

    Keeping controllers thin, business logic in services, and data access behind repositories so you can test and change one layer without rewriting the rest.

  • Consistent patterns

    Using the same structure (e.g. controller → service → repository, or feature → api/store/components) across domains so onboarding and refactors are predictable.

  • Validation at the edge

    Validating and parsing input at the API boundary (DTOs/schemas) so invalid data never reaches services and error responses are consistent.

  • Transaction boundaries

    Defining which operations run in a single transaction (e.g. “create user + issue token”) and handling failures and rollbacks in a consistent way.

  • Dependency direction

    Keeping high-level modules (e.g. auth flow) depending on abstractions (interfaces) and low-level modules (e.g. Prisma) implementing them, so you can test and swap implementations.

  • Session lifecycle

    Restoring session on load (e.g. refresh token), refreshing the access token before expiry, and handling 401s (retry vs redirect to login) in a single, predictable flow.

  • Types

    Sharing or mirroring types (e.g. API response shapes) between frontend and backend where it helps, without creating a hard dependency on a single shared package if that’s not desired.

What I Learned

On this project I learned to structure a NestJS app around domain modules (auth, users, recipes, pantry, ingredients, AI), each with its own controller, services, and repository layer, so business logic stays testable and dependencies flow in one direction. Using the Fastify adapter instead of Express meant adapting to Fastify’s API (e.g. reply instead of res, cookie and CORS plugins) and being aware of defaults like binding to 127.0.0.1, which broke in Docker until we explicitly listened on 0.0.0.0. I applied guards and exception filters consistently: a global JWT guard with @Public() for auth routes, a Prisma filter to map DB errors to stable status codes and messages, and a validation filter so invalid request bodies are handled in one place. I also used custom decorators and dependency injection (e.g. @Inject(UNIT_OF_WORK), repository interfaces with useExisting) to keep controllers thin and services decoupled from Prisma. Overall, the project reinforced how Nest’s module system, DI, and Fastify’s performance and plugin model work together in a production-style API.