Supercharged Excel and CSV exports and imports for NestJS applications. Effortlessly create, download, and import spreadsheets with powerful features and seamless integration.
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");- 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
Install the package via npm:
npm install @nestbolt/excelOr via yarn:
yarn add @nestbolt/excelOr via pnpm:
pnpm add @nestbolt/excelThis 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
import { ExcelModule } from "@nestbolt/excel";
@Module({
imports: [ExcelModule.forRoot()],
})
export class AppModule {}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;
}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().
ExcelModule.forRoot({
defaultType: "xlsx",
csv: {
delimiter: ",",
useBom: false,
},
});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.
The recommended way to define exports. Decorate your existing entities or DTOs — no separate export class needed.
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 { /* ... */ }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 |
Exclude a property from the export.
@Exportable()
class UserEntity {
@ExportColumn() name!: string;
@ExportColumn() email!: string;
@ExportIgnore() password!: string; // excluded
}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 |
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| 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 |
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.
Provide data as an array of objects or arrays. Supports async.
class UsersExport implements FromCollection {
async collection() {
return await this.usersService.findAll();
}
}Provide data as a two-dimensional array.
class ReportExport implements FromArray {
array() {
return [
[1, "Alice", 100],
[2, "Bob", 200],
];
}
}Add a heading row (or multiple rows).
class UsersExport implements FromCollection, WithHeadings {
collection() {
return this.users;
}
headings() {
return ["ID", "Name", "Email"];
}
}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];
}
}Set the worksheet tab name.
class UsersExport implements FromCollection, WithTitle {
collection() {
return this.users;
}
title() {
return "Active Users";
}
}Export multiple sheets in one workbook.
class MonthlyReport implements WithMultipleSheets {
sheets() {
return [new JanuarySheet(), new FebruarySheet(), new MarchSheet()];
}
}Set explicit column widths (in character units).
columnWidths() {
return { A: 10, B: 30, C: 20 };
}Apply Excel number formats.
columnFormats() {
return { A: '#,##0.00', B: 'yyyy-mm-dd' };
}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
};
}Auto-size all columns to fit their content.
class UsersExport implements FromCollection, ShouldAutoSize {
readonly shouldAutoSize = true as const;
collection() {
return this.users;
}
}Set workbook document properties.
properties() {
return { creator: 'MyApp', title: 'User Report' };
}Start writing at a specific cell instead of A1.
startCell() { return 'C3'; }Override CSV options per export.
csvSettings() {
return { delimiter: ';', useBom: true };
}Hook into the export lifecycle.
import { ExcelExportEvent } from '@nestbolt/excel';
registerEvents() {
return {
[ExcelExportEvent.BEFORE_EXPORT]: ({ workbook }) => { /* ... */ },
[ExcelExportEvent.AFTER_SHEET]: ({ worksheet }) => { /* ... */ },
};
}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";
}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
}
}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.
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.
Import classes use the same concern-based pattern as exports. Implement one or more interfaces to configure how data is read, transformed, and validated.
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.skippedReceive imported data as a two-dimensional array.
class DataImport implements ToArray {
handleArray(rows: any[][]) {
console.log(rows); // [[1, "Alice"], [2, "Bob"]]
}
}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" }, ...]
}
}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
}
}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(),
};
}
}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 };
}
}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
}Insert imported rows in configurable batch sizes.
class BatchImport implements WithBatchInserts {
batchSize() {
return 100;
}
async handleBatch(batch: any[]) {
await this.userRepo.save(batch);
}
}Skip rows before a given row number.
startRow() {
return 3; // start reading from row 3
}Limit the number of data rows read.
limit() {
return 1000; // only read first 1000 data rows
}Ignore blank rows during import.
class CleanImport implements SkipsEmptyRows {
readonly skipsEmptyRows = true as const;
}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() { /* ... */ }
}By default, store() and import() work with the local filesystem. You can configure named storage backends ("disks") to read/write files from cloud storage.
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",
});// 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).
Default driver. No additional packages needed.
{ driver: "local", root: "./storage" }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 }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 }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 }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",
}),
});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);
}
}
}Inject ExcelService and call its methods:
| 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 |
| 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 |
| 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 |
| 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 |
npm testRun tests in watch mode:
npm run test:watchGenerate coverage report:
npm run test:covPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.
- Built on top of ExcelJS
The MIT License (MIT). Please see License File for more information.