Recipe WebApp
A full-stack recipe management application with REST API, JWT authentication, and OCR scanning.
In active development — backend services and REST API complete; frontend in progress.
Overview
A web application for recording, organizing, and sharing family recipes — built around a Go REST API that handles all business logic server-side.
The core problem: family recipes live on handwritten cards, in text messages, or scattered across devices. This app centralizes them behind a proper API with user accounts, so recipes are accessible from anywhere and shareable between family members.
The backend is the primary engineering focus. It exposes a RESTful JSON API built with Go and the Gin framework, backed by PostgreSQL. The frontend is a React application that consumes the API — currently in development.
Concepts
UI designs created in Figma, showing desktop and mobile responsive layouts.
Architecture
The system follows a standard three-tier architecture:
React Frontend (SPA)
|
v
Gin REST API (Go) — stateless, JWT-authenticated
| \
v v
PostgreSQL OCR.space API (third-party, proxied)
Client → API → Database. The React frontend makes JSON requests to the Gin API. The API owns all business logic: input validation, authentication, authorization, and database access. PostgreSQL stores all persistent data.
Backend proxy pattern for external APIs. The OCR feature (image-to-text scanning) calls the OCR.space third-party API, but the frontend never talks to it directly. Instead, the frontend uploads an image to the Go API, which proxies the call to OCR.space server-side. This keeps API keys off the client, normalizes the response format, and lets the backend enforce authentication before any external call is made.
Service architecture. The API binary runs as a system service via kardianos/service, supporting graceful startup/shutdown with configurable timeouts. This means the same binary works as a foreground process during development and as a managed service in production.
API Design
The API is organized into resource groups, each with its own route file, handler, and store interface:
| Group | Endpoints | Purpose |
|---|---|---|
/recipes |
GET / — list allGET /:id — get onePOST / — createPATCH /:id — partial updateDELETE /:id — delete
|
Full CRUD for recipes. PATCH accepts partial payloads — only the fields you send are updated. |
/sessions |
POST / — login (create session)GET / — validate session *DELETE / — logout *
|
Session management. POST takes username + password, returns a signed JWT cookie. * = requires valid JWT. |
/image-to-text |
POST / — parse image *
|
Upload an image, receive extracted text. Proxies to OCR.space API server-side. * = requires valid JWT. Conditionally enabled via config. |
/status |
GET / — health check
|
Returns service status. No auth required — used by the frontend before login. |
Layered handler pattern. Each resource group follows the same structure: a route file wires endpoints to a handler struct, which depends on a store interface. The store interface abstracts the database layer, making handlers fully unit-testable with mock implementations — and every handler has tests.
Authentication
Authentication uses a custom JWT implementation with HMAC-SHA256 signing:
- Login: Client sends
POST /sessionswith username and password. The API verifies the password hash, builds a JWT containing the user ID, issuer, issued-at, and expiration timestamps, signs it with HS256, and sets it as an HTTP cookie. - Validation: Protected routes use a Gin middleware (
AuthenticateJWT) that extracts the JWT from the cookie, re-hashes header + payload to verify the signature hasn't been tampered with, and checks expiration. If valid, the user ID is injected into the Gin context for downstream handlers. - Logout:
DELETE /sessionsclears the session cookie.
The JWT secret, session duration, cookie name, and secure flag are all configurable via environment variables. Session duration defaults to 15 minutes.
What it protects: Session validation/deletion and the image-to-text endpoint sit behind the JWT middleware. Recipe CRUD endpoints have auth enforcement planned (with TODO annotations in the route definitions for authorization checks).
Data Model
PostgreSQL with versioned migrations managed by golang-migrate. The API creates its own schema (recipeapi) on startup.
| Table | Columns | Notes |
|---|---|---|
users |
id (bigserial PK)username (varchar, unique)passwordhash (varchar)created_at (timestamptz)
|
Passwords stored as hashes, never plaintext. |
recipes |
id (bigserial PK)name (varchar)description (text)created_at (timestamptz)creator_id (integer, FK → users)
|
Foreign key to users added via migration 005. Enforces NOT NULL — every recipe belongs to a user. |
Migration history: 5 migrations so far — initial recipe table creation, default timestamp fix, description NOT NULL constraint, users table creation, and the creator_id foreign key linking recipes to users.
Go structs in the models package define the data shapes used throughout the API, with JSON tags for serialization. Request bodies use separate structs (e.g., PatchRecipeRequestBody with pointer fields for partial updates).
Feature Highlights
OCR Image-to-Text Scanning
Users can photograph a handwritten recipe card and have the text extracted automatically. The implementation:
- Frontend uploads the image as multipart form data to
POST /image-to-text. - The handler saves the upload to a temp file, auto-resizes it if it exceeds the OCR API's file size limit, then passes it to the OCR store.
- The store calls OCR.space (Engine 3) via the
go_ocr_spacelibrary and returns the extracted text. - The backend returns the text as JSON. The frontend can then pre-fill a recipe creation form with it.
This is a backend proxy pattern: the API key stays server-side, the frontend only needs to upload an image and receive text. The endpoint is JWT-protected so only authenticated users can use it, and it's conditionally registered — if no OCR API key is configured, the route doesn't exist.
AI Recipe Generation (Planned)
The software requirements specification includes AI-powered recipe generation via prompt, allowing users to describe a dish and receive a generated recipe. This feature is planned but not yet implemented in the current codebase.
Structured Logging & Error Monitoring
The API uses logrus for structured logging with configurable log levels and a readable formatter for development. Production error monitoring is integrated via Sentry — when a Sentry DSN is configured, error-level logs are forwarded automatically with stack traces attached.
Deployment
Containerized with Docker. The API uses a multi-stage Dockerfile: a Go builder stage compiles the binary, then copies it (along with migration files) into a minimal Alpine image. The result is a small, self-contained container.
Docker Compose orchestrates the full stack locally and in production: the API service and a PostgreSQL database container with health checks. The DB container waits until migrations have run and data is queryable before the API starts accepting traffic.
CI/CD: A GitHub Actions workflow triggers on release creation, builds the Go binary for Windows, and attaches it to the GitHub release as a downloadable artifact.
Configuration is entirely environment-driven. Database credentials, JWT secret, session settings, OCR API key, Gin mode, Sentry DSN, and log level are all set via environment variables (with sensible defaults where appropriate). A .env file is supported for local development via godotenv.
Tech Stack
- Language
- Go
- Web Framework
- Gin
- Database
- PostgreSQL
- Auth
- Custom JWT (HS256), cookie-based
- Migrations
- golang-migrate
- OCR
- OCR.space API via go_ocr_space
- Logging
- Logrus + Sentry
- Service Management
- kardianos/service
- Frontend
- React (in progress)
- Containerization
- Docker + Docker Compose
- CI/CD
- GitHub Actions
- Testing
- Go testing with mock store interfaces
Documents
Software Requirements Specification
Project requirements, interface specs, and design constraints
GitHub