Skip to content

nestbolt/excel

Repository files navigation

@nestbolt/excel

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

npm version npm downloads tests license


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: "alice@example.com", password: "s" },
      { id: 2, firstName: "Bob", email: "bob@example.com", 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.

About

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

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors