# Asynchroniczne przekazywanie leadów z Next.js do 1C Bitrix: wzorzec bez kolejek i workerów
Synchroniczne wysyłanie leadów do CRM przez REST API powoduje krytyczne problemy: użytkownik czeka na odpowiedź do czasu zakończenia zewnętrznego zapytania, a niestabilność API 1C Bitrix wywołuje timeouty i błędy. Proponujemy rozwiązanie oparte na Next.js 16, które eliminuje kolejki i procesy w tle. Implementacja zapisuje leada w lokalnej bazie PostgreSQL, natychmiast odpowiada użytkownikowi, a integrację z Bitriksem realizuje za pomocą after() — mechanizmu przetwarzania w tle po wysłaniu odpowiedzi HTTP.
Architektura bez kolejek: trzy warstwy odpowiedzialności
Kluczowa zasada — podział operacji na część synchroniczną i asynchroniczną. Po otrzymaniu formularza:
- Walidacja danych za pomocą zod
- Zapis do PostgreSQL z bitrix_id=NULL
- Natychmiastowa odpowiedź 200 OK
- Asynchroniczne wysłanie do Bitrikxa za pomocą after()
To gwarantuje:
- Niezależność od stabilności REST API Bitrikxa
- Brak blokady interfejsu użytkownika
- Możliwość ręcznej obróbki niesprawionych leadów za pomocą SELECT * FROM leads WHERE bitrix_id IS NULL
Cechą tego podejścia jest rezygnacja z Redis/BullMQ. Wszystkie operacje mieszczą się w API route Next.js, wykorzystując wbudowany after() do przetwarzania po wysłaniu odpowiedzi. To zmniejsza złożoność infrastruktury i ułatwia wdrożenie na VPS za pomocą systemd/nginx.
after() w Next.js 16: techniczna implementacja
Metoda after() rozwiązuje problem „ogień-i-zapomnij” w środowiskach serverless. W przeciwieństwie do Promise.resolve().then(), gwarantuje wykonanie zadania nawet po wysłaniu odpowiedzi, nie kończąc procesu przed zakończeniem operacji. Przykład obróbki formularza:
export async function POST(request: Request) {
// ... waliacja i antyduplikat
const [lead] = await pgQuery(...);
after(async () => {
try {
const bitrixId = await sendToBitrix24(payload);
await pgQuery(
`UPDATE leads SET bitrix_id = $1 WHERE id = $2`,
[bitrixId, lead.id]
);
} catch (error) {
console.error("[Leads API] Error otpravki in Bitriks:", error);
}
});
return Response.json({ ok: true, id: lead.id });
}
Ważne niuanse:
- Antyduplikat zaimplementowany za pomocą okna 10 minut w PostgreSQL, a nie przez Bitrikx (CRM akceptuje duplikaty bez ostrzeżenia)
- bitrix_id przechowywane jako nullable text — odzwierciedla asynchroniczny charakter operacji
- Błędy integracji logowane w journalctl, bez wpływu na odpowiedź użytkownika
Niezawodny klient HTTP dla 1C Bitrix
Krytyczne parametry dla stabilnej pracy z niestabilnym API:
- MAX_ATTEMPTS = 2 — równowaga między niezawodnością a obciążeniem
- REQUEST_TIMEOUT_MS = 8000 — powyżej p95 latencji Bitrikxa (400-700 ms)
- RETRY_DELAY_MS = 1500 — liniowy backoff bez wykładniczej złożoności
Implementacja klienta obejmuje:
async function bitrixRequest(method: string, payload: Record<string, unknown>) {
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
const response = await fetch(url, {
signal: controller.signal,
// ...
});
const text = await response.text();
const json = text ? JSON.parse(text) : {};
if (!response.ok || json.error) {
throw new Error(json.error_description || `HTTP ${response.status}`);
}
return json;
} catch (error) {
if (attempt >= MAX_ATTEMPTS) throw error;
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
} finally {
clearTimeout(timer);
}
}
}
Trzy kluczowe aspekty:
- Ręczny parsowanie przez response.text() zamiast response.json() — omija błędy Bitrikxa z pustymi odpowiedziami
- Obsługa dwóch typów błędów: statusy HTTP i wewnętrzne błędy CRM
- Gwarantowane czyszczenie timera w finally — zapobiega wyciekom pamięci
Cechy autentykacji i schematu danych
Do integracji wybrano incoming webhook zamiast OAuth:
- Brak zależności od Marketplace i aprobat
- Proste zarządzanie zmienną środowiskową BITRIX_WEBHOOK_URL
- Scope'y konfiguruje się w panelu admin Bitrikxa
Ryzyko wycieku tokena w URL minimalizowane przez zasadę: w logach i kodzie podaje się tylko metodę API, a nie pełny URL.
Schemat tabeli leads:
CREATE TABLE leads (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
phone text NOT NULL,
source text,
bitrix_id text,
created_at timestamptz NOT NULL DEFAULT now()
);
Dlaczego bitrix_id jako text, a nie integer?
- Bitrix zwraca ID jako string w JSON
- Brak potrzeby konwersji typów
- UUID w PostgreSQL zapewnia idempotentność
Co jest ważne
- Źródło prawdy — lokalna baza danych: lead jest zapisywany przed obrascheniem do Bitrikxa, co gwarantuje dane nawet przy awarii CRM
- Przetwarzanie po wysłaniu odpowiedzi: after() zastępuje kolejki, eliminując złożoność infrastruktury
- Zarządzanie błędami: retry z timeoutami i logowanie w journalctl zapewniają obserwowalność
- Antyduplikat na poziomie bazy danych: okno 10 minut po numerze telefonu zapobiega duplikatom niezależnie od CRM
- Bezpieczna autentykacja: webhook wymaga ścisłej kontroli logowania URL
Ten wzorzec nadaje się do projektów o obciążeniu do 50 zapytań na minutę. W systemach o wysokim obciążeniu zaleca się dodać monitoring za pomocą SELECT COUNT(*) FROM leads WHERE bitrix_id IS NULL oraz automatyczne retry za pomocą cron. W obecnej implementacji ręczna obróbka błędów jest preferowana przed automatycznymi ponownymi próbami — zmniejsza ryzyko duplikacji leadów przy tymczasowych awariach.
— Editorial Team
Brak komentarzy.