npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nestbolt/excel

v0.3.0

Published

Supercharged Excel and CSV exports and imports for NestJS applications. Effortlessly create, download, and import spreadsheets with powerful features and seamless integration.

Downloads

294

Readme

This package provides a clean, decorator-based export API for NestJS that makes generating XLSX and CSV files effortless.

Once installed, using it is as simple as:

@Exportable({ title: "Users" })
class UserEntity {
  @ExportColumn({ order: 1, header: "ID" })
  id!: number;

  @ExportColumn({ order: 2 })
  firstName!: string;

  @ExportColumn({ order: 3 })
  email!: string;

  @ExportIgnore()
  password!: string;
}

// In your controller
return this.excelService.downloadFromEntityAsStream(UserEntity, users, "users.xlsx");

Table of Contents

Installation

Install the package via npm:

npm install @nestbolt/excel

Or via yarn:

yarn add @nestbolt/excel

Or via pnpm:

pnpm add @nestbolt/excel

Peer Dependencies

This package requires the following peer dependencies, which you likely already have in a NestJS project:

@nestjs/common   ^10.0.0 || ^11.0.0
@nestjs/core     ^10.0.0 || ^11.0.0
reflect-metadata ^0.1.13 || ^0.2.0

Quick Start

1. Register the module

import { ExcelModule } from "@nestbolt/excel";

@Module({
  imports: [ExcelModule.forRoot()],
})
export class AppModule {}

2. Decorate your entity or DTO

import { Exportable, ExportColumn, ExportIgnore } from "@nestbolt/excel";

@Exportable({ title: "Users" })
export class UserEntity {
  @ExportColumn({ order: 1, header: "ID" })
  id!: number;

  @ExportColumn({ order: 2 })
  firstName!: string;

  @ExportColumn({ order: 3 })
  email!: string;

  @ExportIgnore()
  password!: string;
}

3. Use it in your controller

import { Controller, Get } from "@nestjs/common";
import { ExcelService } from "@nestbolt/excel";
import { UserEntity } from "./user.entity";

@Controller("users")
export class UsersController {
  constructor(private readonly excelService: ExcelService) {}

  @Get("export")
  async export() {
    const users: UserEntity[] = [
      { id: 1, firstName: "Alice", email: "[email protected]", password: "s" },
      { id: 2, firstName: "Bob", email: "[email protected]", password: "s" },
    ];
    return this.excelService.downloadFromEntityAsStream(
      UserEntity,
      users,
      "users.xlsx",
    );
  }
}

The password field is automatically excluded from the export thanks to @ExportIgnore().

Module Configuration

Static Configuration (forRoot)

ExcelModule.forRoot({
  defaultType: "xlsx",
  csv: {
    delimiter: ",",
    useBom: false,
  },
});

Async Configuration (forRootAsync)

ExcelModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    defaultType: config.get("EXCEL_DEFAULT_TYPE", "xlsx"),
  }),
});

The module is registered as global — import it once in your root module.

Exports — Decorator API

The recommended way to define exports. Decorate your existing entities or DTOs — no separate export class needed.

@Exportable

Mark a class as exportable. Accepts optional configuration:

@Exportable({
  title: "Users",           // worksheet tab name
  autoFilter: "auto",       // add auto-filter to headings
  autoSize: true,           // auto-size columns to fit content
  frozenRows: 1,            // freeze heading row
  frozenColumns: 1,         // freeze first column
  columnWidths: { A: 10 },  // explicit column widths
})
class UserEntity { /* ... */ }

@ExportColumn

Mark a property for export. Without options, the column header is derived from the property name (camelCase → Title Case).

@Exportable()
class ProductEntity {
  @ExportColumn({ order: 1, header: "SKU", width: 15 })
  sku!: string;

  @ExportColumn({ order: 2, format: "#,##0.00" })
  price!: number;

  @ExportColumn({
    order: 3,
    header: "In Stock",
    map: (val) => (val ? "Yes" : "No"),
  })
  inStock!: boolean;
}

Options:

| Option | Type | Description | | -------- | ---------------------------- | -------------------------------------------------- | | order | number | Column position (lower = further left) | | header | string | Column heading text | | format | string | Excel number format (e.g. '#,##0.00') | | map | (value, row) => any | Transform the value before writing | | width | number | Column width in character units |

@ExportIgnore

Exclude a property from the export.

@Exportable()
class UserEntity {
  @ExportColumn() name!: string;
  @ExportColumn() email!: string;
  @ExportIgnore() password!: string;  // excluded
}

Decorator Options

All @Exportable() options map to the same concern-based features:

| Option | Equivalent Concern | | --------------- | -------------------- | | title | WithTitle | | columnWidths | WithColumnWidths | | autoFilter | WithAutoFilter | | autoSize | ShouldAutoSize | | frozenRows | WithFrozenRows | | frozenColumns | WithFrozenColumns |

Inheritance

Decorators support class inheritance. Child classes inherit parent columns and can override or ignore them:

@Exportable({ title: "Base" })
class BaseEntity {
  @ExportColumn({ order: 1 }) id!: number;
  @ExportColumn({ order: 2 }) name!: string;
}

@Exportable({ title: "Employees" })
class EmployeeEntity extends BaseEntity {
  @ExportColumn({ order: 3 }) department!: string;
  @ExportIgnore() name!: string;  // remove name from export
}
// Columns: ID, Department

Service Methods (Decorator API)

| Method | Returns | Description | | ------------------------------------------------------------------- | --------------------- | ------------------------------------ | | downloadFromEntity(entityClass, data, filename, type?) | ExcelDownloadResult | Buffer + filename + content type | | downloadFromEntityAsStream(entityClass, data, filename, type?) | StreamableFile | NestJS StreamableFile for controllers | | storeFromEntity(entityClass, data, filePath, type?, disk?) | void | Write to storage (default or named disk) | | rawFromEntity(entityClass, data, type) | Buffer | Raw file buffer |


Exports — Concern-based API

For advanced use cases (multiple sheets, templates, events, custom start cells, CSV settings), use the concern-based pattern. Implement one or more interfaces to opt in to features.

FromCollection

Provide data as an array of objects or arrays. Supports async.

class UsersExport implements FromCollection {
  async collection() {
    return await this.usersService.findAll();
  }
}

FromArray

Provide data as a two-dimensional array.

class ReportExport implements FromArray {
  array() {
    return [
      [1, "Alice", 100],
      [2, "Bob", 200],
    ];
  }
}

WithHeadings

Add a heading row (or multiple rows).

class UsersExport implements FromCollection, WithHeadings {
  collection() {
    return this.users;
  }

  headings() {
    return ["ID", "Name", "Email"];
  }
}

WithMapping

Transform each row before writing.

class UsersExport implements FromCollection, WithMapping {
  collection() {
    return this.users;
  }

  map(user: User) {
    return [user.id, `${user.firstName} ${user.lastName}`, user.email];
  }
}

WithTitle

Set the worksheet tab name.

class UsersExport implements FromCollection, WithTitle {
  collection() {
    return this.users;
  }
  title() {
    return "Active Users";
  }
}

WithMultipleSheets

Export multiple sheets in one workbook.

class MonthlyReport implements WithMultipleSheets {
  sheets() {
    return [new JanuarySheet(), new FebruarySheet(), new MarchSheet()];
  }
}

WithColumnWidths

Set explicit column widths (in character units).

columnWidths() {
  return { A: 10, B: 30, C: 20 };
}

WithColumnFormatting

Apply Excel number formats.

columnFormats() {
  return { A: '#,##0.00', B: 'yyyy-mm-dd' };
}

WithStyles

Apply styles to rows, columns, or individual cells.

styles() {
  return {
    1:    { font: { bold: true, size: 14 } },         // row 1
    'A':  { alignment: { horizontal: 'center' } },    // column A
    'B2': { fill: { fgColor: 'FFD700' } },            // cell B2
  };
}

ShouldAutoSize

Auto-size all columns to fit their content.

class UsersExport implements FromCollection, ShouldAutoSize {
  readonly shouldAutoSize = true as const;
  collection() {
    return this.users;
  }
}

WithProperties

Set workbook document properties.

properties() {
  return { creator: 'MyApp', title: 'User Report' };
}

WithCustomStartCell

Start writing at a specific cell instead of A1.

startCell() { return 'C3'; }

WithCsvSettings

Override CSV options per export.

csvSettings() {
  return { delimiter: ';', useBom: true };
}

WithEvents

Hook into the export lifecycle.

import { ExcelExportEvent } from '@nestbolt/excel';

registerEvents() {
  return {
    [ExcelExportEvent.BEFORE_EXPORT]: ({ workbook }) => { /* ... */ },
    [ExcelExportEvent.AFTER_SHEET]:   ({ worksheet }) => { /* ... */ },
  };
}

WithAutoFilter

Add an auto-filter dropdown to your heading row. Use 'auto' to automatically detect the range from your headings, or specify an explicit range.

class UsersExport implements FromCollection, WithHeadings, WithAutoFilter {
  collection() {
    return this.users;
  }
  headings() {
    return ["ID", "Name", "Email"];
  }
  autoFilter() {
    return "auto"; // automatically covers A1:C1
  }
}

Or with an explicit range:

autoFilter() {
  return "A1:D10";
}

WithFrozenRows / WithFrozenColumns

Freeze rows or columns so they stay visible when scrolling.

class UsersExport implements FromCollection, WithHeadings, WithFrozenRows {
  collection() {
    return this.users;
  }
  headings() {
    return ["ID", "Name", "Email"];
  }
  frozenRows() {
    return 1; // freeze the first row (headings)
  }
}

You can freeze columns too, or combine both:

class ReportExport
  implements FromCollection, WithFrozenRows, WithFrozenColumns
{
  collection() {
    return this.data;
  }
  frozenRows() {
    return 2;
  }
  frozenColumns() {
    return 1; // freeze column A
  }
}

FromTemplate

Fill an existing .xlsx template with data. Define placeholder bindings that replace {{placeholder}} patterns in the template.

class InvoiceExport implements FromTemplate {
  templatePath() {
    return "/path/to/invoice-template.xlsx";
  }

  bindings() {
    return {
      "{{company}}": "Acme Corp",
      "{{date}}": "2026-01-15",
      "{{total}}": 1500,
    };
  }
}

When a cell contains exactly one placeholder and nothing else, the binding value is written with its original type (number, date, etc.). When a placeholder is embedded in a longer string, the result is a string concatenation.

WithTemplateData

Extend FromTemplate with repeating row data — ideal for line items in invoices, reports, etc.

class InvoiceExport implements FromTemplate, WithTemplateData {
  templatePath() {
    return "/path/to/invoice-template.xlsx";
  }

  bindings() {
    return {
      "{{company}}": "Acme Corp",
      "{{date}}": "2026-01-15",
      "{{total}}": 4200,
    };
  }

  dataStartCell() {
    return "A6"; // row data starts at A6
  }

  async templateData() {
    return [
      ["Widget", 10, 42],
      ["Gadget", 5, 840],
    ];
  }
}

The dataStartCell() specifies where the first row of data is written. Each subsequent row is placed on the next row below.

Imports

Import classes use the same concern-based pattern as exports. Implement one or more interfaces to configure how data is read, transformed, and validated.

Quick Import Example

class UsersImport implements ToCollection, WithHeadingRow, WithValidation, SkipsOnError {
  readonly hasHeadingRow = true as const;
  readonly skipsOnError = true as const;

  handleCollection(rows: Record<string, any>[]) {
    // Process imported rows
  }

  rules() {
    return {
      name: [{ validate: (v) => v?.length > 0, message: "Name is required" }],
      email: [{ validate: (v) => /^.+@.+\..+$/.test(v), message: "Invalid email" }],
    };
  }
}

// In your controller
const result = await this.excelService.import(new UsersImport(), "users.xlsx");
// result.rows, result.errors, result.skipped

ToArray

Receive imported data as a two-dimensional array.

class DataImport implements ToArray {
  handleArray(rows: any[][]) {
    console.log(rows); // [[1, "Alice"], [2, "Bob"]]
  }
}

ToCollection

Receive imported data as an array of objects. Requires WithHeadingRow or WithColumnMapping to derive object keys.

class UsersImport implements ToCollection, WithHeadingRow {
  readonly hasHeadingRow = true as const;

  handleCollection(rows: Record<string, any>[]) {
    console.log(rows); // [{ ID: 1, Name: "Alice" }, ...]
  }
}

WithHeadingRow

Use a row in the spreadsheet as column headings. Defaults to row 1.

class ImportWithCustomHeading implements WithHeadingRow {
  readonly hasHeadingRow = true as const;

  headingRow() {
    return 2; // row 2 contains the headers
  }
}

WithImportMapping

Transform each row after reading.

class MappedImport implements WithHeadingRow, WithImportMapping {
  readonly hasHeadingRow = true as const;

  mapRow(row: Record<string, any>) {
    return {
      fullName: row.first_name + " " + row.last_name,
      email: row.email.toLowerCase(),
    };
  }
}

WithColumnMapping

Map column letters or 1-based indices to named fields, useful for files without headers.

class NoHeaderImport implements WithColumnMapping {
  columnMapping() {
    return { name: "A", email: "C", age: 2 };
  }
}

WithValidation

Validate imported rows using custom rules or class-validator DTOs.

Custom rules:

rules() {
  return {
    name: [
      { validate: (v) => v?.length > 0, message: "Name is required" },
    ],
    email: [
      { validate: (v) => /^.+@.+\..+$/.test(v), message: "Invalid email" },
    ],
  };
}

class-validator DTO:

import { IsString, IsEmail, IsNotEmpty } from "class-validator";

class UserDto {
  @IsString() @IsNotEmpty() name!: string;
  @IsEmail() email!: string;
}

// In your import class
rules() {
  return { dto: UserDto };
}

Note: DTO mode requires class-validator and class-transformer as peer dependencies:

pnpm add class-validator class-transformer

The ImportResult returned from the service contains:

interface ImportResult<T = any> {
  rows: T[];                        // valid rows
  errors: ImportValidationError[];  // per-row validation errors
  skipped: number;                  // count of skipped rows
}

WithBatchInserts

Insert imported rows in configurable batch sizes.

class BatchImport implements WithBatchInserts {
  batchSize() {
    return 100;
  }

  async handleBatch(batch: any[]) {
    await this.userRepo.save(batch);
  }
}

WithStartRow

Skip rows before a given row number.

startRow() {
  return 3; // start reading from row 3
}

WithLimit

Limit the number of data rows read.

limit() {
  return 1000; // only read first 1000 data rows
}

SkipsEmptyRows

Ignore blank rows during import.

class CleanImport implements SkipsEmptyRows {
  readonly skipsEmptyRows = true as const;
}

SkipsOnError

Skip invalid rows instead of throwing. Without this concern, the first validation failure throws an error with all collected errors attached.

class TolerantImport implements WithValidation, SkipsOnError {
  readonly skipsOnError = true as const;
  rules() { /* ... */ }
}

Storage Drivers

By default, store() and import() work with the local filesystem. You can configure named storage backends ("disks") to read/write files from cloud storage.

Configuration

ExcelModule.forRoot({
  disks: {
    local: { driver: "local", root: "./storage" },
    s3: {
      driver: "s3",
      bucket: "my-reports",
      region: "us-east-1",
      prefix: "excel",
    },
    gcs: {
      driver: "gcs",
      bucket: "my-reports",
      keyFilename: "/path/to/service-account.json",
    },
    azure: {
      driver: "azure",
      container: "reports",
      connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
    },
  },
  defaultDisk: "local",
});

Using the disk parameter

// Store to S3
await excelService.store(new UsersExport(), "reports/users.xlsx", undefined, "s3");

// Import from GCS
const result = await excelService.import(new UsersImport(), "uploads/data.xlsx", undefined, "gcs");

// toArray / toCollection also support disk
const rows = await excelService.toArray("data.xlsx", undefined, "s3");

Without the disk parameter, methods use the defaultDisk (falls back to an implicit local driver).

LocalDriver

Default driver. No additional packages needed.

{ driver: "local", root: "./storage" }

S3Driver

Works with AWS S3 and any S3-compatible service (MinIO, R2, DigitalOcean Spaces).

pnpm add @aws-sdk/client-s3
// SDK default credentials (env vars, IAM roles, ~/.aws/credentials)
{ driver: "s3", bucket: "my-bucket", region: "us-east-1" }

// Inline credentials
{ driver: "s3", bucket: "my-bucket", region: "us-east-1",
  credentials: { accessKeyId: "...", secretAccessKey: "..." } }

// S3-compatible endpoint
{ driver: "s3", bucket: "my-bucket", endpoint: "http://localhost:9000" }

// Pre-configured client
{ driver: "s3", bucket: "my-bucket", client: myS3Client }

GCSDriver

pnpm add @google-cloud/storage
// Application Default Credentials
{ driver: "gcs", bucket: "my-bucket" }

// Service account keyfile
{ driver: "gcs", bucket: "my-bucket", keyFilename: "/path/to/key.json" }

// Pre-configured client
{ driver: "gcs", bucket: "my-bucket", client: myStorageInstance }

AzureDriver

pnpm add @azure/storage-blob
// Connection string
{ driver: "azure", container: "reports",
  connectionString: "DefaultEndpointsProtocol=https;..." }

// Account name + key
{ driver: "azure", container: "reports",
  accountName: "myaccount", accountKey: "mykey" }

// Pre-configured ContainerClient
{ driver: "azure", container: "reports", client: myContainerClient }

Pre-configured client via forRootAsync

For full control over authentication (secrets managers, vaults, custom middleware):

ExcelModule.forRootAsync({
  imports: [AwsModule],
  inject: [S3Client],
  useFactory: (s3Client: S3Client) => ({
    disks: {
      s3: { driver: "s3", bucket: "my-bucket", client: s3Client },
    },
    defaultDisk: "s3",
  }),
});

Using DiskManager Directly

Inject DiskManager for direct storage operations:

import { DiskManager } from "@nestbolt/excel";

@Injectable()
export class ReportService {
  constructor(private readonly diskManager: DiskManager) {}

  async cleanup(path: string) {
    const driver = this.diskManager.disk("s3");
    if (await driver.exists(path)) {
      await driver.delete(path);
    }
  }
}

Using the Service Directly

Inject ExcelService and call its methods:

Export Methods (Concern-based)

| Method | Returns | Description | | ----------------------------------------------------- | --------------------- | ------------------------------------------------------------ | | download(exportable, filename, type?) | ExcelDownloadResult | Returns buffer + filename + content type | | downloadAsStream(exportable, filename, type?) | StreamableFile | Returns a NestJS StreamableFile for direct controller return | | store(exportable, filePath, type?, disk?) | void | Writes the export to storage (default or named disk) | | raw(exportable, type) | Buffer | Returns the raw file buffer |

Export Methods (Decorator-based)

| Method | Returns | Description | | ------------------------------------------------------------------- | --------------------- | ------------------------------------ | | downloadFromEntity(entityClass, data, filename, type?) | ExcelDownloadResult | Buffer + filename + content type | | downloadFromEntityAsStream(entityClass, data, filename, type?) | StreamableFile | NestJS StreamableFile for controllers | | storeFromEntity(entityClass, data, filePath, type?, disk?) | void | Write to storage (default or named disk) | | rawFromEntity(entityClass, data, type) | Buffer | Raw file buffer |

Import Methods

| Method | Returns | Description | | --------------------------------------------------- | ---------------------------- | ---------------------------------------------------- | | import(importable, filePath, type?, disk?) | ImportResult | Read and process a file from storage | | importFromBuffer(importable, buffer, type?) | ImportResult | Read and process a buffer | | toArray(filePath, type?, disk?) | any[][] | Shorthand: returns raw 2D array | | toCollection(filePath, type?, disk?) | Record<string, any>[] | Shorthand: returns objects using row 1 as headings |

Configuration Options

| Option | Type | Default | Description | | ---------------- | ----------------------------- | ----------- | ---------------------------------------------- | | defaultType | 'xlsx' \| 'csv' | 'xlsx' | Fallback type when extension is unrecognised | | tempDirectory | string | OS temp dir | Directory for temporary files | | disks | Record<string, DiskConfig> | — | Named storage backends (see Storage Drivers) | | defaultDisk | string | 'local' | Default disk name used when disk is omitted | | csv.delimiter | string | ',' | CSV column delimiter | | csv.quoteChar | string | '"' | CSV quote character | | csv.lineEnding | string | '\n' | CSV line ending | | csv.useBom | boolean | false | Prepend UTF-8 BOM | | csv.encoding | BufferEncoding | 'utf-8' | Output encoding |

Testing

npm test

Run tests in watch mode:

npm run test:watch

Generate coverage report:

npm run test:cov

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.