Browse docs

Searching

Running an LDAP search with ldapjs, consuming the streamed results, and requesting specific attributes.

On this page

client.search() maps directly onto the four search parameters covered in Searching: base DN, scope, filter, and attributes.

A basic search

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.

ts
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.

Collecting entries into an array

Most application code wants a plain array of results rather than working with events directly. A small wrapper handles that:

ts
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));
    });
  });
}
Building the filter safely

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.

Paging large result sets

For searches that might return thousands of entries, pass a paging control so both the client and server handle the volume gracefully:

ts
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.

What's next

Searching for users is only half of the login story — see Authentication with ldapjs for the full bind-then-search-then-bind pattern.