@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
Maintainers
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
- Quick Start
- Module Configuration
- Exports — Decorator API
- Exports — Concern-based API
- Imports
- Storage Drivers
- Using the Service Directly
- Configuration Options
- Testing
- Changelog
- Contributing
- Security
- Credits
- License
Installation
Install the package via npm:
npm install @nestbolt/excelOr via yarn:
yarn add @nestbolt/excelOr via pnpm:
pnpm add @nestbolt/excelPeer 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.0Quick 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, DepartmentService 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.skippedToArray
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-validatorandclass-transformeras 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 testRun tests in watch mode:
npm run test:watchGenerate coverage report:
npm run test:covChangelog
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
- Built on top of ExcelJS
License
The MIT License (MIT). Please see License File for more information.
