DEV Community

Cover image for How We Built a Weather-Aware Daily Recipe Pipeline With FastAPI
Fancy Fox Recipes
Fancy Fox Recipes

Posted on Fully Autonomous

How We Built a Weather-Aware Daily Recipe Pipeline With FastAPI

Fancy Fox publishes one AI-assisted seasonal
recipe idea each day. What began as a linear prompt eventually became a small
state machine: gather context, generate a recipe, validate structured output,
render static pages, and promote the result.

The interesting engineering work was not the happy path. It was deciding which
steps should fail the run, which should degrade gracefully, and which browser
dependencies should be recreated on demand.

The pipeline as a state machine

Each run moves through explicit steps instead of one long function:

date + location
    -> weather context
    -> recent-recipe context
    -> structured recipe
    -> validation
    -> image
    -> static site build
    -> optional social promotion
Enter fullscreen mode Exit fullscreen mode

This structure matters because the consequences are different. A malformed
recipe should stop publication. A missing optional promotion should not erase a
valid recipe. A browser failure should report that promotion failed rather than
claiming success.

Validate at the boundary

Model output is untrusted input. The pipeline parses it into a Pydantic model
before it reaches templates or storage:

class RecipeIngredient(BaseModel):
    short_name: str
    full_name: str
    quantity: str
    prep: str | None = None


class Recipe(BaseModel):
    id: str
    name: str
    description: str
    recipeCategory: str
    recipeCuisine: str
    ingredients: list[RecipeIngredient]
    prepTime: int
    cookTime: int
    totalTime: int
Enter fullscreen mode Exit fullscreen mode

Even small placeholder values deserve normalization. A generated string such as
"none" is not equivalent to Python's None, and leaving it untouched can
leak nonsense into rendered recipe text or structured data.

EMPTY_PREP_VALUES = {"-", "n/a", "none", "null", "not applicable"}

@validator("prep", pre=True)
def normalize_empty_prep(cls, value):
    if isinstance(value, str) and value.strip().casefold() in EMPTY_PREP_VALUES:
        return None
    return value
Enter fullscreen mode Exit fullscreen mode

The principle is broader than recipes: normalize at the boundary, then let the
rest of the program work with one representation.

Let optional work return honestly

Our promotion flow once treated “there is no special recipe today” as an error.
That made a normal condition look like an outage. The simplest fix was to let
the step return silently when no eligible item exists.

The opposite bug was more damaging: a promotion path could log “promoted” even
after the browser action failed. We changed the contract so the caller only
announces success when the platform-specific function returns a positive result.

posted = await promote_recipe(recipe)
if posted:
    logger.info("Promoted %s", recipe.id)
else:
    logger.warning("Promotion skipped or failed for %s", recipe.id)
Enter fullscreen mode Exit fullscreen mode

Side effects should return enough information for their caller to tell the
truth.

Treat browser state as disposable

Social platforms occasionally require a real browser session. Assuming that a
human will keep Chromium open indefinitely is fragile, especially on a personal
workstation.

Our browser manager now follows a simple lifecycle:

  1. Connect to an existing controlled browser if it is healthy.
  2. Otherwise launch a fresh instance with the saved application profile.
  3. Open the required platform and run the action.
  4. Close resources owned by the run.

That turns “the browser was closed” from a mysterious production failure into a
normal startup condition.

Make operational logs survive the terminal

Uvicorn output is useful until the terminal scrollback disappears. We attach a
rotating file handler during app startup so application logs, tracebacks, and
server logs share one local diagnostic stream:

handler = RotatingFileHandler(
    "logs/fancy_fox.log",
    maxBytes=10 * 1024 * 1024,
    backupCount=1,
)
Enter fullscreen mode Exit fullscreen mode

Ten megabytes plus one backup is enough to investigate recent runs without
allowing a daily service to fill the disk.

Static output still benefits from an application server

FastAPI is the development and orchestration surface, while the public website
is rendered as static HTML. That gives recipe pages stable URLs, complete
metadata, crawlable links, and very little runtime work for a visitor.

The public archive now contains more than 1,500 pages, but the sitemap remains a
curated set of 50 URLs. Public availability, indexability, and active promotion
are separate decisions.

What we would do next

The generation pipeline is no longer the main constraint. Distribution and
feedback are. The next useful loop connects a recipe impression to a save,
return visit, or app open, then uses that signal to improve the featured set.

You can inspect the public result at Fancy Fox or
save recipes in the iOS
app
.

We also published a CC0 sample of 30 recipes and matching images on
GitHub
for
developers and food-data researchers.

Disclosure: This article is from the team building Fancy Fox. Recipe concepts
and imagery on the service are AI-assisted.

Top comments (0)