Recipio: A Full-Stack Recipe Manager Built with Go and React

Recipio is a full-stack web application for collecting and organizing recipes from any source — social media posts, food blogs, cooking sites, or manual entry — into a single personal library. It also supports meal planning and automatic grocery list generation. The project served as a practical exercise in building a complete production-grade system end-to-end: RESTful backend in Go, a React frontend, Google OAuth authentication, AI-powered recipe parsing, and Docker-based deployment.

The app is live at sarap.recipes. sarap is Tagalog for “yummy.” The source code is available on GitHub.

Project Summary

The core problem Recipio solves is the fragmentation of recipes across the internet. A recipe might live in an Instagram caption, a food blog buried under advertisement paragraphs, or a group chat message. Recipio provides a single place to store them all, regardless of format or origin.

The application supports three main workflows:

  • Recipe library — add, edit, search, and view recipes with ingredients, instructions, and tags
  • Meal planning — build a weekly meal plan by assigning saved recipes to days
  • Grocery lists — automatically aggregate ingredients from a meal plan into a consolidated shopping list

Table of Contents

Architecture

Recipio uses a single-server architecture where the Go binary acts as both the web server and API server.

The frontend is built as a React SPA using Vite and Tailwind CSS.

The backend uses Go’s standard net/http library exclusively — no web framework. Handlers are higher-order functions that accept dependencies (database, parser, config) and return http.Handler, keeping each handler independently testable. Business logic sits behind a RecipeDatabase interface, with a SQLite implementation for production and a mock for tests.

Scaling

For simplicity, the Go server serves both the frontend and API. The code is structured such that the frontend can be served by Nginx with a separate, clustered API server if needed. If I were to scale this project, I’d first move away from SQLite to PostgreSQL. The database logic is written behind an interface, so the migration is as simple as writing a new implementation while keeping the business logic unchanged. The schema is also modular, with business logic relying on JOINs rather than embedding data, which makes future migrations more straightforward.

RESTful API

The backend exposes a RESTful API organized by resource. Routes are registered in a central routes.go file and grouped by feature area:

Resource Endpoints
Recipes GET/POST /recipes, GET/PUT/DELETE /recipes/{id}
Meal Plans GET/POST /meal-plans, GET/PUT/DELETE /meal-plans/{id}
Grocery GET /grocery (derived from active meal plan)
Parse POST /parse/text, POST /parse/url
Auth GET /auth/login, GET /auth/callback, POST /auth/logout

All request and response bodies use JSON. Handler integration tests use Go’s httptest package against a mock database, so the full HTTP stack is exercised without requiring a running SQLite instance.

Google OAuth Authentication

Authentication is handled via Google OAuth 2.0. When a user logs in, the backend generates a cryptographically random state token, stores it in an HttpOnly cookie, and redirects the user to Google’s authorization endpoint. On callback, the state parameter is validated against the cookie to prevent CSRF attacks before the authorization code is exchanged for a user profile.

Sessions are managed server-side, with the frontend storing only a session token in localStorage. An AuthContext in React gates all protected routes, redirecting unauthenticated users to the login page.

AI Recipe Parsing

The most technically interesting feature is AI-powered recipe parsing. Rather than building brittle web scrapers for individual sites, Recipio takes a different approach: fetch the raw page content, strip HTML tags and boilerplate, and pass the cleaned text to an LLM to extract structured recipe data.

This works in two modes:

URL parsing — the user submits a link. The backend fetches the page (with a 20-second timeout and a 2 MB body limit), strips <script> and <style> blocks, removes all remaining HTML tags, and trims the result to 8,000 characters before sending it to the LLM. Private/internal addresses are blocked to prevent SSRF.

Raw text parsing — the user pastes recipe text directly (e.g., copied from a social media post). The text is sanitized and sent to the LLM as-is.

In both cases, the LLM returns a structured recipe object — title, ingredients with quantities, step-by-step instructions, and tags — which is displayed to the user for review before saving. This approach generalises across any source that renders text, without needing site-specific parsers.

Frontend

Note: While I wrote the core frontend structure, Claude assisted with fleshing out the finer implementation details.

The frontend is built with React, Vite, and Tailwind CSS. TanStack Query (React Query) handles all data fetching, caching, and background synchronization, keeping the UI in sync with the backend without manual state management.

Pages are organized by feature (recipes/, mealplan/, grocery/) with API calls co-located in *_apis.jsx files next to their pages. Feather icons (react-icons) are used throughout for a consistent, lightweight icon set. The color theme uses indigo as the primary color and pink as the accent.

Frontend tests run with Vitest and @testing-library/react, covering the main recipe flows: list rendering, search filtering, add form (both manual and AI-parse paths), and the view/edit toggle. API calls are mocked via vi.mock so tests run entirely without a backend.

Deployment

Recipio ships as a single Docker image. The build process compiles the React frontend with Vite, embeds the static files into the Go binary, and produces a self-contained server that serves both the SPA and the API from one process.

make all   # builds frontend + backend into bin/
make run   # starts the server at http://localhost:4002

This single-binary deployment model keeps the operational footprint minimal. I simply run make deploy and my server immediately publishes the new container.

Right now, it’s being hosted on my home server with Traefik as a reverse proxy. Benefits of Traefik is the Let’s Encrypt integration so it’s simple to provide TLS connections. The DNS records are hosted on Cloudflare and it helps protect my home IP as well.

Key Takeaways

Building Recipio end-to-end reinforced a few things worth highlighting:

  • Interface-driven design in Go pays off immediately in testing. Every change is easy to test and validate. The downside is that a layer of abstraction can be confusing, but it’s worth it when changes are easily testable.
  • LLM-based parsing generalises better than scraping. I used a small model to do the parsing. At first, I had my doubts whether the small model could handle it, but with proper checks such as gatekeeping the output to a structured JSON schema, it performed surprisingly well.
  • OAuth simplifies authentication. Delegating authentication to a provider like Google removes the need for password storage in the database entirely, reducing both complexity and security risk.
  • Standard library is sufficient. Using net/http directly, without a framework, kept the dependency surface small and made the routing and middleware behaviour explicit and easy to follow. Combined with interface-driven design, smaller modules are easier to understand and maintain.

Rise N' Shine

The Rise N’ Shine project aims to automate the opening and closing of blinds based on the current sun position. Its purpose was to utilize the natural sunlight to maximize the amount of light in the room. It also allows a convenient solution for people who depend on sunlight to synchronize their circadian rhythm to the natural sun.