12 KiB
Microservice Importer
Standalone-tjänst för snabbimport av recept från webben. Extraherar receptdata (namn, beskrivning, ingredienser, instruktioner) från URL:er och konverterar till standardiserad Markdown-format.
Kopplat till: recipe-app — men kan användas helt oberoende.
Features
Quick-Import via URL
- Skrapa från ICA.se — JSON-LD structured data + HTML-parsing
- Generisk parser — Fallback för andra webbplatser
- Automatisk extraction:
- Receptnamn
- Beskrivning (från meta-taggar eller JSON-LD)
- Ingredienser med kvantitet, enhet och noter
- Instruktioner/tillvägagångssätt
- Källlänk (läggs till i footer)
Stödd format
- Bråkmängder:
1 1/2 dl,1/2 tsk - Enheter: g, kg, ml, dl, msk, tsk, st, port, efter smak, förp, klyfta, m.fl.
- Parentetiska noter:
2 dl grädde (vispgrädde)→ note sparas separat - Markdown-output: Strukturerat receptformat för vidare bearbetning
Parse-Markdown endpoint
Tolka Markdown-format recepter utan databaskomplikationer. Användbar för API-integration utan lokal DB.
Arkitektur
Backend (NestJS 10.3, TypeScript 5.4.5)
Port: 3001
src/
├── app.module.ts # Root module (Quick-import + Recipes)
├── main.ts # Startpunkt
├── common/
│ ├── filters/
│ │ └── global-exception.filter.ts # Centraliserad felhantering (svenska meddelanden)
│ └── utils/
│ └── normalize-name.ts # Namnormalisering (åäö-handling)
├── quick-import/ # URL-scraping & parsing
│ ├── quick-import.controller.ts # POST /api/quick-import
│ ├── quick-import.service.ts # Scraping-logik, parser-selection
│ ├── quick-import.module.ts
│ └── parsers/
│ ├── base.parser.ts # Abstrakt bas-klass
│ ├── ica.parser.ts # ICA.se-specifik (JSON-LD prioritet)
│ └── generic.parser.ts # Fallback för alla webbplatser
└── recipes/ # Markdown-tolkning (SOM DB!)
├── recipes.controller.ts # POST /api/recipes/parse-markdown
├── recipes.service.ts # Enkel markdown-parsing
├── recipes.module.ts
└── dto/
└── parse-markdown.dto.ts # Validering
Viktigt: Backend har INGEN databaskonfiguration (@prisma/client inte installerat).
Frontend (Next.js 16.2, React 19.2, TypeScript 5.4.5)
Port: 3000
app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── Navigation.tsx # Minimal nav (Home + Import)
├── import/page.tsx # PRIMARY FEATURE — Import UI
│ └── Inmatningsfält för URL/filsökväg
│ └── Visa resultat i realtid
├── api/
│ └── parse-markdown-proxy/route.ts # API proxy till backend
└── lib/
├── api.ts # Centraliserad API-access (fetchJson)
└── error-handler.ts # parseErrorResponse (svenska meddelanden)
Quick-Start
Prerequisites
- Node.js 22.x
- Docker & Docker Compose (valfritt)
Local Development
# 1. Installera backend
cd backend
npm install
npm run start:dev
# Backend kör på http://localhost:3001
# 2. (I ny terminal) Installera frontend
cd frontend
npm install
npm run dev
# Frontend kör på http://localhost:3000
Öppna http://localhost:3000 → Gå till /import → Klistra in URL
Docker
# Bygg och starta
docker compose up -d
# Frontend: http://localhost:3000
# Backend: http://localhost:3001
Stoppa:
docker compose down
API-dokumentation
POST /api/quick-import
Syfte: Skrapa webbsida och returnera Markdown-recept
Request:
{
"input": "https://ica.se/recept/kottfarssas-1234"
}
Response (Success 200):
{
"markdown": "# Köttfärssås\n\nEn klassisk...\n\n## Ingredienser\n- 500 g köttfärs\n...",
"source": "ica"
}
Response (Error 400/503):
{
"statusCode": 400,
"message": "Kunde inte hämta recept: HTTP 404. Kontrollera att länken är korrekt och försök igen.",
"timestamp": "2026-04-12T10:30:00.000Z"
}
Error-scenarier:
400— Tomt input, inte en URL, inte en filsökväg400— HTML-parsing misslyckades (receptnamn/ingredienser inte hittade)503— Network-fel (t.ex. webbsidan nåbar, men HTTP 500 från server)
POST /api/recipes/parse-markdown
Syfte: Tolka Markdown-receptformat utan database
Request:
{
"markdown": "# Receptnamn\n\n## Ingredienser\n- 500 g köttfärs\n\n## Tillvägagångssätt\nStek löken..."
}
Response (Success 200):
{
"name": "Receptnamn",
"description": "",
"instructions": "Stek löken...",
"ingredients": [
{
"rawName": "köttfärs",
"quantity": 500,
"unit": "g",
"note": null
}
]
}
Parser-arkitektur
Bas-parser (base.parser.ts)
Abstrakt klass som alla parsers ärver från. Innehåller gemensam parsing-logik:
abstract class RecipeParser {
abstract canHandle(url: string): boolean;
abstract parse(html: string): ParsedRecipe;
protected parseIngredientLine(line: string): ParsedIngredient | null {
// Shared logic för ingrediensparsning
}
}
parseIngredientLine-funktionen hanterar:
500 g köttfärs→{quantity: 500, unit: "g", name: "köttfärs"}1 1/2 dl grädde (vispgrädde)→{quantity: 1.5, unit: "dl", name: "grädde", note: "vispgrädde"}3 ägg→{quantity: 3, unit: "st", name: "ägg"}salt→{quantity: 0, unit: "", name: "salt"}
Kända enheter:
Vikt: g, kg, hg, mg
Volym: ml, dl, l, tl
Portioner: tsk, msk, krm
Övriga: st, port, burk, förp, paket, pris, portion, matsked, tesked, efter smak, klyfta
ICA Parser (ica.parser.ts)
Optimerad för ICA.se receptsidor.
Strategi:
- Försök extrahera JSON-LD structured data (prioritet)
- Fallback: HTML-regex parsing
JSON-LD mål:
- Receptnamn från
@type === "Recipe"→.name - Beskrivning från
.description - Ingredienser från
.recipeIngredient[] - Instruktioner från
.recipeInstructions[]
HTML-fallback:
- Titel:
<h1>eller<meta property="og:title"> - Beskrivning:
<meta name="description"> - Ingredienser:
<li class="...ingredient...">regex - Instruktioner:
<div class="...instruction...">regex
Generic Parser (generic.parser.ts)
Fallback-parser för alla okända webbplatser.
Strategi:
- Försök JSON-LD structured data (alla webbplatser kan ha detta)
- Fallback: Permissiv HTML-parsing
HTML-parsing försöker flera selectors:
- Ingredienser:
<li>,<div class="ingredient">,<p class="ingredient"> - Instruktioner:
<div class="instruction">,<ol>listor - Titel:
<h1>,<meta property="og:title">,<title>
Markdown-format och parsing
Input-format
# Receptnamn
Valfri beskrivning av receptet (1+ stycken).
## Ingredienser
- 500 g köttfärs
- 1 st lök
- 2.5 msk tomatpuré
- 1 dl grädde (vispgrädde)
- salt
## Tillvägagångssätt
Steg 1: Stek löken i smör.
Steg 2: Tillsätt köttfärsen och stek tills den är genomstekt.
Parsningsregler
| Element | Tolkning |
|---|---|
# Rubrik |
Receptnamn (första H1) |
Text mellan H1 och ## Ingredienser |
Beskrivning (flera rader OK, valfritt) |
## Ingredienser |
Ingredient-markerare (case-insensitive) |
- ANTAL ENHET NAMN |
Ingrediens med alla delar |
- ANTAL NAMN |
Ingrediens utan enhet (unit → "st") |
- NAMN |
Ingrediens utan kvantitet (quantity → 0) |
(text i parentes) |
Ingrediensnot (sparas separat) |
## Tillvägagångssätt / ## Instruktioner / ## Tillagning |
Instruktions-markerare |
| Text under instruktioner | Tillagningssteg (flera rader OK) |
Exempel:
Input: "- 1,5 dl grädde (vispgrädde)"
Output: {quantity: 1.5, unit: "dl", rawName: "grädde", note: "vispgrädde"}
Input: "- 3 ägg"
Output: {quantity: 3, unit: "st", rawName: "ägg", note: null}
Input: "- salt"
Output: {quantity: 0, unit: "", rawName: "salt", note: null}
Deployment
Docker Compose (Recommended)
cd microservice-importer
docker compose up -d
Services:
importer-api— NestJS backend (port 3001)importer-frontend— Next.js frontend (port 3000)
Stoppa:
docker compose down
Loggar:
docker compose logs -f importer-api
docker compose logs -f importer-frontend
Manuell Docker build
# Backend
docker build -f backend/Dockerfile -t importer-api:local .
docker run -p 3001:3001 importer-api:local
# Frontend (ny terminal)
docker build -f frontend/Dockerfile -t importer-frontend:local .
docker run -p 3000:3000 importer-frontend:local
Environment variables
Konfigureras i docker compose eller .env:
Backend:
PORT=3001
NODE_ENV=production
Frontend:
NEXT_PUBLIC_API_URL_INTERNAL=http://importer-api:3001
Integration med Recipe App
Recipe App kan anropa denna microservice som extern API:
const IMPORTER_URL = process.env.MICROSERVICE_IMPORTER_URL || 'http://localhost:3001';
const response = await fetch(`${IMPORTER_URL}/api/quick-import`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: url })
});
const { markdown, source } = await response.json();
// Sedan kan recipe-app:s parse-markdown-endpoint
// använda Markdown för DB-matchning mot produkter
Fil-struktur & Koddelning
Följande filer är identiska mellan recipe-app och microservice-importer:
backend/src/quick-import/parsers/base.parser.tsbackend/src/quick-import/parsers/ica.parser.tsbackend/src/quick-import/parsers/generic.parser.tsbackend/src/common/filters/global-exception.filter.tsbackend/src/common/utils/normalize-name.ts
Framtida improvement: Dessa kan förpackas som separat npm-paket för bättre koddelning och versionering.
Tekniska detaljer
Backend Stack
- NestJS 10.3 — REST API & modular architecture
- TypeScript 5.4.5 — Type safety
- Node.js 22.x — Runtime
- class-validator — DTO validation (svenska felmeddelanden)
- Ingen databas — Stateless service
Frontend Stack
- Next.js 16.2 — React framework (App Router)
- React 19.2 — UI components
- TypeScript 5.4.5
- Inline CSS — Minimal styling, no framework dependencies
Error Handling
- Centraliserad
GlobalExceptionFilter(svenska meddelanden) - Konsistent JSON-responsformat
- HTTP status codes: 200, 400, 503
Framtida utbyggnader
- PDF-import (stubben redan på plats)
- Stöd för fler webbplatser (mat.se, kokaihop.se, etc.)
- Caching av parsed recept
- Rate limiting för scraping
- WebSocket support för real-time parsing
- GraphQL endpoint
Licens
Samma som Recipe App — se main repo
Support
Relaterade projekt:
- Recipe App —
recipe-app(Full platform med databas + quick-import integrerad) - Git Repo — Gitea på
192.168.50.2:2222/nilsjohan/microservice-importer