client.search() maps directly onto the four search parameters covered in Searching: base DN, scope, filter, and attributes.
ldapjs search results arrive as an event-emitting stream, not a single returned array — this matters because directories can return a large number of entries, and streaming avoids buffering all of them in memory at once.
const opts: ldap.SearchOptions = {
filter: "(&(objectClass=user)(mail=jane.doe@example.com))",
scope: "sub",
attributes: ["cn", "mail", "memberOf"],
};
client.search("OU=Users,DC=example,DC=com", opts, (err, res) => {
if (err) {
console.error("Search failed:", err);
return;
}
res.on("searchEntry", (entry) => {
console.log(entry.pojo.attributes);
});
res.on("error", (err) => {
console.error("Search error:", err);
});
res.on("end", (result) => {
console.log("Search finished with status", result?.status);
});
});
Note
entry.pojo is ldapjs's plain-object representation of the entry: { objectName, attributes, ... }. Older ldapjs versions exposed a similar shape via entry.object; check your installed version's changelog if that field is missing.
Most application code wants a plain array of results rather than working with events directly. A small wrapper handles that:
function searchAll(
client: ldap.Client,
base: string,
opts: ldap.SearchOptions,
): Promise<Record<string, unknown>[]> {
return new Promise((resolve, reject) => {
const entries: Record<string, unknown>[] = [];
client.search(base, opts, (err, res) => {
if (err) return reject(err);
res.on("searchEntry", (entry) => entries.push(entry.pojo.attributes));
res.on("error", reject);
res.on("end", () => resolve(entries));
});
});
}
Never build the filter string with plain interpolation when any part of it comes from user input — escape values first. See LDAP injection for why.
LDAP Filter Builder
Prototype and validate filters before hardcoding them.
For searches that might return thousands of entries, pass a paging control so both the client and server handle the volume gracefully:
const opts: ldap.SearchOptions = {
filter: "(objectClass=user)",
scope: "sub",
paged: { pageSize: 200 },
};
ldapjs handles requesting subsequent pages internally when paged is set, still delivering entries through the same searchEntry event.
Searching for users is only half of the login story — see Authentication with ldapjs for the full bind-then-search-then-bind pattern.