Node.js je JavaScript izvršno okruženje van veb pregledača, izgrađeno na V8 endžinu i prošireno API-jima za rad sa mrežom, fajlovima, procesima, tokovima podataka (streams) i operativnim sistemom. Najčešće se koristi za REST API (Application Programming Interface) servise, GraphQL servise, WebSocket servere u realnom vremenu, mikroservise, CLI alate, proksije i API mrežne prolaze (API gateways). Njegova glavna prednost je neblokirajući I/O: jedan proces može efikasno da opslužuje veliki broj mrežnih konekcija. Kompromis je u tome što dugotrajna, računarski zahtevna izračunavanja mogu blokirati petlju događaja ukoliko se ne prebace na radne niti (workers), zasebne procese ili drugi servis.
JavaScript se izvršava u V8 motoru, dok Node.js API-ji delegiraju asinhroni rad operativnom sistemu ili libuv skupu niti. Glavna JavaScript nit ne čeka završetak I/O operacije; ona nastavlja sa izvršavanjem drugog koda. Kada se operacija završi, njen povratni poziv ili nastavak Promise objekta postaje spreman za petlju događaja. Na taj način Node.js postiže visoku konkurentnost iako se korisnički JavaScript obično izvršava na jednoj glavnoj niti.
V8 kompajlira i izvršava JavaScript i upravlja JavaScript objektima i memorijom. `libuv` obezbeđuje međuplatformsku petlju događaja, integraciju asinhronog U/I (I/O) i skup radnih niti za operacije kao što su pojedini poslovi sa fajl sistemom, DNS i kriptografija. Petlja događaja odlučuje kada treba da se izvrše povratni pozivi završenih operacija. Sam Node.js povezuje JavaScript API-je sa izvornim mogućnostima sistema; V8 sam po sebi ne obezbeđuje HTTP server, pristup fajl sistemu niti procesne API-je.
4Zašto se za Node.js često kaže da je jedno-nitni (single-threaded)?
Jedan Node.js proces obično izvršava korisnički JavaScript na jednoj glavnoj niti, tako da dve uobičajene funkcije za obradu zahteva ne izvršavaju JavaScript u potpuno isto vreme na toj niti. Međutim, platforma se doslovno ne sastoji od samo jedne niti: libuv ima skup niti, operativni sistem obavlja asinhroni I/O (Input/Output), V8 ima zadatke sakupljanja smeća, a Node.js takođe podržava `worker_threads` i potprocese. Ova fraza uglavnom opisuje podrazumevani model izvršavanja JavaScript-a.
5Kako Node.js može konkurentno da obrađuje veliki broj zahteva?
Node.js pokreće U/I operacije i zatim se vraća u petlju događaja umesto da blokira po jednu nit za svaki zahtev. Na primer, 1.000 poziva ka bazi podataka može biti u toku dok glavna nit nastavlja da prihvata nove konekcije i obrađuje prispele povratne pozive. Ovo odlično funkcioniše za poslove ograničene U/I operacijama kao što su HTTP pozivi, upiti ka bazi podataka, rad sa datotekama i soketima. Međutim, funkcioniše loše kada funkcije za obradu zahteva obavljaju težak sinhroni rad na procesoru, jer to blokira petlju događaja i odlaže obradu svih ostalih zahteva.
Node.js koristi faze biblioteke libuv, uključujući pending callbacks, idle/prepare, poll, check i close callbacks, zajedno sa obradom tajmera. poll obrađuje mnoge I/O povratne pozive (ulaz/izlaz), check izvršava setImmediate, a tajmeri izvršavaju setTimeout/setInterval kada prođe minimalno odlaganje i raspoređivanje to dozvoli. Od libuv 1.45 i Node.js 20, tajmeri se obrađuju posle poll u svakoj iteraciji, uz početni prolaz pre ulaska u petlju radi kompatibilnosti. process.nextTick koristi poseban Node red, dok reakcije Promise i queueMicrotask koriste V8 red mikrozadataka; nijedan nije faza libuv-a. Ti redovi se obrađuju na kontrolnim tačkama između povratnih poziva. Rekurzivno dopunjavanje može uskratiti drugim poslovima vreme za izvršavanje.
7Kako se razlikuju sinhrone, asinhrone i neblokirajuće operacije?
Sinhronost opisuje tok kontrole: poziv daje rezultat ili baca izuzetak pre nego što pozivalac nastavi. Asinhronost opisuje rezultat dostavljen kasnije preko povratnog poziva, Promise ili drugog obaveštenja. Blokiranje znači čekanje na posao ili resurs dok trenutna nit ne može da radi drugo. Neblokirajući interfejsi izbegavaju čekanje, na primer vraćanjem rezultata „operacija bi blokirala“ ili organizovanjem obaveštenja. To su različite osobine: sinhrono samo po sebi ne znači čekanje na I/O spremnost (ulaz/izlaz), a vraćanje Promise ne čini teške sinhrone proračune neblokirajućim. fs.readFileSync je blokirajući sinhroni interfejs; fs.promises.readFile dostavlja rezultat asinhrono dok glavna nit može obrađivati druge poslove. U obradi zahteva dajte prednost odgovarajućim asinhronim I/O interfejsima.
Nastaje kada sinhroni JavaScript ili stalno dopunjavani redovi kontrolnih tačaka sprečavaju blagovremenu obradu I/O povratnih poziva (ulaz/izlaz), tajmera i zahteva. Uzroci uključuju teške proračune, velike JSON operacije, rekurzivno zakazane mikrozadatke i prekomerni process.nextTick. Ograničite veličinu ulaza, profilirajte skupe operacije i premestite zahtevne proračune u worker_threads ili drugi servis. Pri deljenju posla prepustite kontrolu kasnijem krugu petlje, na primer pomoću setImmediate; ponovljeni await nad već razrešenim Promise ili zakazivanje queueMicrotask i dalje mogu izgladnjivati I/O. Izbegavajte blokirajuće sinhrone interfejse u često izvršavanim putanjama zahteva.
9Šta je povratni poziv i šta znači pakao povratnih poziva?
Povratni poziv je funkcija prosleđena drugoj operaciji da je ta operacija pozove. Može se izvršavati sinhrono, kao kod Array.map, ili asinhrono, kao kod fs.readFile. Klasični asinhroni Node.js povratni pozivi često stavljaju grešku kao prvi argument: callback(error, result). Pakao povratnih poziva znači duboko ugnežđene zavisne pozive koji otežavaju razumevanje toka kontrole i obrade grešaka. Imenovane funkcije, manje operacije, Promise i async/await mogu poboljšati čitljivost. Nezavisne operacije pokrećite zajedno kada je prikladno, na primer preko Promise.all, uz poštovanje ograničenja istovremenosti i obradu grešaka.
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
Promise predstavlja budući rezultat asinhrone operacije i može biti u stanju čekanja (pending), ispunjen (fulfilled) ili odbijen (rejected). `async` funkcija uvek vraća Promise. `await` pauzira samo trenutnu `async` funkciju dok se Promise ne razreši; on ne blokira ceo Node.js proces. Dobar odgovor treba da obuhvati obradu grešaka pomoću `try/catch`, izbegavanje neobrađenih (floating) Promise objekata i korišćenje `Promise.all` za nezavisne zadatke kako se operacije ne bi izvršavale nepotrebno sekvencijalno.
11Kako se razlikuju process.nextTick, queueMicrotask, setImmediate i setTimeout?
process.nextTick zakazuje posao u Node next-tick red; queueMicrotask koristi V8 red mikrozadataka, koji koriste i reakcije Promise. Na uobičajenoj kontrolnoj tački najvišeg nivoa CommonJS-a, nextTick pozivi rade pre čekajućih Promise/queueMicrotask poziva. Na najvišem nivou ES modula evaluacija je već u kontekstu mikrozadatka, pa zakazani mikrozadaci mogu prethoditi nextTick-u. To zavisi od konteksta i nije univerzalno pravilo prioriteta koje prekida tekući JavaScript. setImmediate radi u check. setTimeout(fn, 0) čeka minimalni vremenski prag i nije trenutan. Njihov redosled zavisi od mesta zakazivanja; ako su oba zakazana u jednom I/O povratnom pozivu (ulaz/izlaz), setImmediate radi prvi. Za prenosivo odlaganje kao mikrozadatak prednost dajte queueMicrotask, osim kada je potrebno specifično ponašanje nextTick-a.
// CommonJS file (.cjs): nextTick precedes promise at this top level.
// ES module file (.mjs): promise precedes nextTick at this top level.
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// timeout vs immediate is context-dependent here.
Koristite try/catch za sinhrone izuzetke. Za Promise operacije koristite await unutar try/catch ili dodajte obrađivač odbijanja; spoljašnji sinhroni try/catch ne hvata izuzetak kasnijeg povratnog poziva. Pozivi sa greškom kao prvim argumentom moraju je proveriti, a EventEmitter error događaji zahtevaju odgovarajuće obrađivače. Serveri treba da centralizuju obradu grešaka zahteva, beleže koristan kontekst i ne otkrivaju interne detalje klijentima. Posle uncaughtException ne pretpostavljajte da proces može bezbedno nastaviti: izvršite samo neophodno sinhrono hitno čišćenje, završite proces i prepustite ponovno pokretanje nadzornom procesu. Planirano uredno gašenje korisno je pri normalnom završavanju, ali proizvoljan asinhroni oporavak posle neuhvaćenog izuzetka nije pouzdana strategija.
Modul je izolovana jedinica koda sa eksplicitnim izvezenim interfejsom. Node.js podržava ugrađene module kao što su node:fs i node:http, lokalne projektne module, npm pakete, CommonJS module i ECMAScript module. Moduli čine granice odgovornosti jasnijim i omogućavaju ponovno korišćenje koda. Kompromis u produkciji jeste međusobna zavisnost: ako moduli previše duboko uvoze jedan drugog ili skrivaju propratne efekte u trenutku uvoza, ponašanje pri pokretanju i testiranje postaju znatno teži za razumevanje.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14Kako se razlikuju CommonJS require i import u ES modulima?
CommonJS koristi require i module.exports, a ES moduli import i export. require obično učitava sinhrono i može se pozivati uslovno. Statičke import deklaracije omogućavaju analizu grafa zavisnosti; dinamički import() vraća Promise. Savremeni Node.js može pomoću require učitati i graf ES modula bez await na najvišem nivou, prema podržanim pravilima interoperabilnosti, pa ES moduli ne znače uvek asinhronu evaluaciju. .cjs/.mjs, type u package.json i primenljiva pravila prepoznavanja sintakse određuju režim modula. ES moduli podržavaju await na najvišem nivou. CommonJS pruža __dirname/__filename; aktuelni Node.js pruža import.meta.dirname/filename za ES module zasnovane na datotekama. import.meta.url je koristan za razrešavanje preko URL-a i pristupe kompatibilne sa starijim verzijama.
// File: math.cjs (CommonJS)
function calculate(a, b) { return a + b; }
module.exports = { calculate };
// Separate file: math.mjs (ES module)
export function calculate(a, b) { return a + b; }
// These are two separate files, not one combined script.
npm je interfejs komandne linije za upravljanje paketima. package.json opisuje metapodatke, skripte i zahteve direktnih zavisnosti, često kroz opsege verzija. package-lock.json beleži razrešeno stablo zavisnosti sa verzijama paketa i podacima o integritetu. Datoteku zaključanih zavisnosti aplikacije uključite u kontrolu verzija. npm ci je koristan u kontinuiranoj integraciji: instalira iz zaključane datoteke, uklanja postojeći node_modules i odbija rad ako se package.json i zaključavanje ne slažu. Zaključavanje smanjuje neželjene promene zavisnosti, ali ne izjednačava operativne sisteme, nativne kompilacije, skripte životnog ciklusa, Node verzije ili opcije instalacije. Ponovljive kompilacije zahtevaju i kontrolisane alate i okruženje.
16Kako treba upravljati konfiguracijom i promenljivama okruženja?
Čitajte promenljive preko process.env: prisutne vrednosti su stringovi, a nedostajuće svojstvo je undefined. Proverite obavezne vrednosti i namerno ih konvertujte pri pokretanju; sama konverzija PORT preko Number ne odbacuje prazan string, NaN ili port izvan dozvoljenog opsega. Prekinite sa jasnom greškom ako je obavezna konfiguracija neispravna. Prave tajne držite van kontrole verzija, a produkcione dostavljajte preko platforme za postavljanje ili upravljača tajnama; primer konfiguracije bez tajni sme se verzionisati. NODE_ENV je aplikaciona konvencija, a ne pouzdana bezbednosna granica.
const rawPort = process.env.PORT ?? '3000';
if (!/^\d+$/.test(rawPort)) throw new Error('PORT must be an integer');
const port = Number(rawPort);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
throw new Error('PORT must be between 1 and 65535');
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl?.trim()) throw new Error('DATABASE_URL is required');
EventEmitter implementira obrazac objavljivanja i pretplate (publish-subscribe) unutar Node.js procesa. Metoda on registruje osluškivač, once registruje osluškivač za jedan događaj, off uklanja osluškivač, a emit sinhrono poziva osluškivače po redosledu registracije. Sinhrona priroda je važna: spor osluškivač blokira emiter. Događaj error je poseban jer neobrađeni error događaj može srušiti proces. Dugotrajni sistemi takođe treba da uklanjaju nekorišćene osluškivače kako bi se izbeglo curenje memorije.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('orderCreated', order => {
console.log(`Order ${order.id} created`);
});
emitter.emit('orderCreated', { id: 42 });
Buffer predstavlja sekvencu bajtova fiksne veličine. Koristi se za rad sa fajlovima, TCP soketima, telima HTTP zahteva i odgovora, slikama, kriptografijom i binarnim protokolima. Buffer.from kreira bafer iz postojećih podataka, Buffer.alloc stvara memoriju popunjenu nulama, a Buffer.allocUnsafe je brži, ali može sadržati stare memorijske podatke dok se ne prepiše. To čini allocUnsafe pogodnim samo kada kod odmah popunjava bafer i ne izlaže zaostale podatke.
const buffer = Buffer.from('Hello', 'utf8');
console.log(buffer.toString('utf8'));
const safe = Buffer.alloc(1024);
// Prefer alloc for general use. Never expose allocUnsafe bytes before filling them.
const fast = Buffer.allocUnsafe(1024);
fast.fill(0); // All bytes initialized before use.
19Šta su tokovi podataka (streams) i koje vrste postoje?
Tokovi podataka obrađuju podatke u delovima umesto učitavanja celokupnog sadržaja u memoriju. Glavni tipovi su `Readable`, `Writable`, `Duplex` i `Transform`. Datoteke, objekti HTTP zahteva i odgovora, TCP utičnice i gzip tokovi su česti primeri. Tokovi su dragoceni za velike datoteke i mrežni saobraćaj jer smanjuju upotrebu memorije i kašnjenje. Odgovor na nivou produkcije treba da napomene da greške u tokovima moraju biti ispravno obrađene duž celog lanca.
20Šta je povratni pritisak (backpressure) i zašto je pipeline koristan?
Povratni pritisak nastaje kada izvor podataka proizvodi delove brže nego što odredište može da ih obradi. Bez kontrole povratnog pritiska, potrošnja memorije može nekontrolisano rasti sve dok proces ne uspori ili se ne sruši. Tokovi podataka (streams) i funkcija `pipe` mogu koordinisati protok, ali je funkcija `pipeline` iz modula `node:stream/promises` bezbednija za višestepene lance jer propagira greške, zatvara povezane tokove i vraća `Promise`. Ona predstavlja odličan podrazumevani izbor za kompresiju datoteka, otpremanje, preuzimanje i transformacije tokova podataka.
import fs from 'node:fs';
import zlib from 'node:zlib';
import { pipeline } from 'node:stream/promises';
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz')
);