@nestbolt/excel
Concern Exports
Build advanced exports using concern interfaces -- composable features for data sources, headings, mapping, styling, multiple sheets, events, and more.
The concern-based API gives you full control over every aspect of an export. Instead of decorating entities, you create a dedicated export class and implement one or more interfaces. Each interface opts in to a single feature, making exports composable and easy to test.
Use this approach when you need:
- Multiple sheets in one workbook
- Cell-level styling, borders, or fills
- Lifecycle event hooks
- Custom start cells
- Per-export CSV overrides
- Workbook-level document properties
Data Sources
Every export needs a data source. Implement one of the following interfaces.
FromCollection
Provide data as an array of objects or arrays. The method can be async.
import { FromCollection } from "@nestbolt/excel";
class UsersExport implements FromCollection {
constructor(private readonly usersService: UsersService) {}
async collection() {
return await this.usersService.findAll();
}
}When combined with WithMapping, each item from collection() is passed to the map() method before being written.
When used without WithMapping or WithHeadings, each item is written as-is. If items are objects, each object value becomes a cell. If items are arrays, each array element becomes a cell.
FromArray
Provide data as a two-dimensional array. Each inner array is one row of cell values.
import { FromArray } from "@nestbolt/excel";
class ReportExport implements FromArray {
array() {
return [
[1, "Alice", "alice@example.com", 100],
[2, "Bob", "bob@example.com", 200],
[3, "Charlie", "charlie@example.com", 150],
];
}
}FromArray is useful for tabular data that is already in row/column form, such as query results or aggregated reports.
Headings and Mapping
WithHeadings
Add a heading row (or multiple heading rows) to the top of the sheet.
import { FromCollection, WithHeadings } from "@nestbolt/excel";
class UsersExport implements FromCollection, WithHeadings {
collection() {
return this.users;
}
headings() {
return ["ID", "Name", "Email", "Department"];
}
}For multiple heading rows (e.g., a title row plus column headers), return a nested array:
headings() {
return [
["User Report - Q1 2026"], // title row
["ID", "Name", "Email", "Department"], // column headers
];
}WithMapping
Transform each row before it is written to the sheet. The map() method receives one raw item from collection() and must return a flat array of cell values.
import { FromCollection, WithHeadings, WithMapping } from "@nestbolt/excel";
class UsersExport implements FromCollection, WithHeadings, WithMapping<User> {
collection() {
return this.users;
}
headings() {
return ["ID", "Full Name", "Email", "Active"];
}
map(user: User) {
return [
user.id,
`${user.firstName} ${user.lastName}`,
user.email,
user.isActive ? "Yes" : "No",
];
}
}Without WithMapping, each object from the collection is written using its values in insertion order. With WithMapping, you have complete control over which fields appear, their order, and how they are formatted.
Sheet Structure
WithTitle
Set the worksheet tab name.
import { FromCollection, WithTitle } from "@nestbolt/excel";
class UsersExport implements FromCollection, WithTitle {
collection() {
return this.users;
}
title() {
return "Active Users";
}
}The tab name appears at the bottom of the spreadsheet in Excel. Without this concern, ExcelJS assigns a default name like "Sheet1".
WithMultipleSheets
Export multiple sheets in a single workbook. Each element returned by sheets() is an independent exportable object that can implement its own set of concerns.
import { WithMultipleSheets, FromCollection, WithHeadings, WithTitle } from "@nestbolt/excel";
class JanuarySheet implements FromCollection, WithHeadings, WithTitle {
collection() { return this.januaryData; }
headings() { return ["Date", "Revenue", "Expenses"]; }
title() { return "January"; }
}
class FebruarySheet implements FromCollection, WithHeadings, WithTitle {
collection() { return this.februaryData; }
headings() { return ["Date", "Revenue", "Expenses"]; }
title() { return "February"; }
}
class QuarterlyReport implements WithMultipleSheets {
sheets() {
return [
new JanuarySheet(),
new FebruarySheet(),
new SummarySheet(),
];
}
}
// Usage:
return this.excelService.downloadAsStream(new QuarterlyReport(), "q1-report.xlsx");Each sheet in the array is processed independently. A sheet can use FromCollection, FromArray, or even FromTemplate -- they do not all need to use the same data source.
WithCustomStartCell
Start writing data at a specific cell instead of A1. This is useful when you want to leave room for a logo, title, or other content above or to the left of the data.
import { FromCollection, WithCustomStartCell } from "@nestbolt/excel";
class ReportExport implements FromCollection, WithCustomStartCell {
collection() {
return this.data;
}
startCell() {
return "C3";
}
}Data (including headings, if present) begins at cell C3 instead of A1.
Formatting and Styling
WithColumnWidths
Set explicit column widths in character units. Keys are column letters.
import { FromCollection, WithColumnWidths } from "@nestbolt/excel";
class UsersExport implements FromCollection, WithColumnWidths {
collection() {
return this.users;
}
columnWidths() {
return {
A: 8, // ID column -- narrow
B: 30, // Name column -- wide
C: 35, // Email column -- wider
D: 15, // Department
};
}
}WithColumnFormatting
Apply Excel number format strings to columns. Keys are column letters, values are format strings.
import { FromCollection, WithColumnFormatting } from "@nestbolt/excel";
class FinancialExport implements FromCollection, WithColumnFormatting {
collection() {
return this.transactions;
}
columnFormats() {
return {
A: "yyyy-mm-dd", // date column
B: "#,##0.00", // amount with two decimals
C: "$#,##0.00", // currency
D: "0.00%", // percentage
};
}
}Common Excel format strings:
| Format | Example Output | Description |
|---|---|---|
#,##0 | 1,234 | Integer with commas |
#,##0.00 | 1,234.56 | Two decimal places |
$#,##0.00 | $1,234.56 | Currency |
0% | 85% | Percentage |
0.00% | 85.50% | Percentage (2 dec) |
yyyy-mm-dd | 2026-01-15 | ISO date |
mm/dd/yyyy | 01/15/2026 | US date |
@ | (as-is) | Text |
WithStyles
Apply styles to rows, columns, or individual cells. Keys can be a row number, a column letter, or a cell reference.
import { FromCollection, WithHeadings, WithStyles } from "@nestbolt/excel";
class StyledExport implements FromCollection, WithHeadings, WithStyles {
collection() {
return this.data;
}
headings() {
return ["ID", "Name", "Revenue"];
}
styles() {
return {
// Style an entire row (row 1 = heading row)
1: {
font: { bold: true, size: 12, color: "FFFFFF" },
fill: { type: "pattern", pattern: "solid", fgColor: "4472C4" },
alignment: { horizontal: "center" },
},
// Style an entire column
C: {
numFmt: "$#,##0.00",
alignment: { horizontal: "right" },
},
// Style a specific cell
A1: {
font: { bold: true, size: 14 },
border: {
bottom: { style: "medium", color: "000000" },
},
},
};
}
}Style Type Reference
CellStyle -- the top-level style object:
| Property | Type | Description |
|---|---|---|
font | FontStyle | Font settings |
alignment | AlignmentStyle | Cell alignment |
fill | FillStyle | Background fill |
border | BorderStyles | Cell borders |
numFmt | string | Number format string |
FontStyle:
| Property | Type | Description |
|---|---|---|
name | string | Font family name |
size | number | Font size in points |
bold | boolean | Bold text |
italic | boolean | Italic text |
underline | boolean | "single" | "double" | Underline style |
strike | boolean | Strikethrough |
color | string | Font color (hex) |
AlignmentStyle:
| Property | Type | Description |
|---|---|---|
horizontal | "left" | "center" | "right" | "fill" | "justify" | Horizontal alignment |
vertical | "top" | "middle" | "bottom" | Vertical alignment |
wrapText | boolean | Wrap text in cell |
textRotation | number | Text angle (degrees) |
FillStyle:
| Property | Type | Description |
|---|---|---|
type | "pattern" | Fill type |
pattern | "solid" | "none" | Fill pattern |
fgColor | string | Foreground color (hex) |
bgColor | string | Background color (hex) |
BorderStyle (for top, bottom, left, right):
| Property | Type | Description |
|---|---|---|
style | "thin" | "medium" | "thick" | "dotted" | "dashed" | "double" | "hair" | "mediumDashed" | "dashDot" | ... | Border line style |
color | string | Border color (hex) |
ShouldAutoSize
Auto-size all columns to fit their content. This is a marker concern -- implement the interface and set the property to true:
import { FromCollection, ShouldAutoSize } from "@nestbolt/excel";
class UsersExport implements FromCollection, ShouldAutoSize {
readonly shouldAutoSize = true as const;
collection() {
return this.users;
}
}Auto-sizing examines the content of each cell (including headings) and sets the column width to fit the longest value. This is convenient but can be overridden on specific columns by also implementing WithColumnWidths.
Auto-Filter and Freeze Panes
WithAutoFilter
Add an auto-filter dropdown to your heading row. Users can click the dropdown arrow in Excel to filter and sort data.
import { FromCollection, WithHeadings, WithAutoFilter } from "@nestbolt/excel";
class UsersExport implements FromCollection, WithHeadings, WithAutoFilter {
collection() {
return this.users;
}
headings() {
return ["ID", "Name", "Email", "Department"];
}
autoFilter() {
return "auto"; // automatically covers A1:D1
}
}Use "auto" to automatically detect the filter range from your headings. Or specify an explicit cell range:
autoFilter() {
return "A1:D1"; // explicit range
}WithFrozenRows
Freeze rows at the top of the sheet so they remain visible when scrolling down.
import { FromCollection, WithHeadings, WithFrozenRows } from "@nestbolt/excel";
class UsersExport implements FromCollection, WithHeadings, WithFrozenRows {
collection() {
return this.users;
}
headings() {
return ["ID", "Name", "Email"];
}
frozenRows() {
return 1; // freeze the heading row
}
}WithFrozenColumns
Freeze columns at the left of the sheet so they remain visible when scrolling horizontally.
import { FromCollection, WithFrozenColumns } from "@nestbolt/excel";
class WideReport implements FromCollection, WithFrozenColumns {
collection() {
return this.data;
}
frozenColumns() {
return 2; // freeze columns A and B
}
}You can combine WithFrozenRows and WithFrozenColumns on the same export:
class DataExport
implements FromCollection, WithHeadings, WithFrozenRows, WithFrozenColumns
{
collection() { return this.data; }
headings() { return ["ID", "Category", "Jan", "Feb", "Mar", "Apr"]; }
frozenRows() { return 1; }
frozenColumns() { return 2; }
}This freezes the heading row and the first two columns (ID and Category), creating a fixed header and row-label area while the numeric columns scroll.
Document Properties
WithProperties
Set workbook-level document metadata. These properties appear in the file's "Properties" or "Info" panel in Excel.
import { FromCollection, WithProperties } from "@nestbolt/excel";
class AuditExport implements FromCollection, WithProperties {
collection() {
return this.auditRecords;
}
properties() {
return {
creator: "MyApp v2.1",
lastModifiedBy: "Export Service",
title: "Audit Log Export",
subject: "Security Audit Records",
description: "Exported on " + new Date().toISOString(),
keywords: "audit security logs",
category: "Reports",
company: "Acme Corp",
manager: "Jane Smith",
};
}
}All properties are optional. The ExcelProperties interface:
| Property | Type | Description |
|---|---|---|
creator | string | Name of the creator |
lastModifiedBy | string | Name of the last modifier |
title | string | Document title |
subject | string | Document subject |
description | string | Document description |
keywords | string | Search keywords |
category | string | Document category |
company | string | Company name |
manager | string | Manager name |
CSV Settings
WithCsvSettings
Override global CSV settings for a specific export. This only takes effect when the export is written as CSV.
import { FromCollection, WithCsvSettings } from "@nestbolt/excel";
class EuropeanExport implements FromCollection, WithCsvSettings {
collection() {
return this.data;
}
csvSettings() {
return {
delimiter: ";", // semicolon for European locales
quoteChar: '"',
lineEnding: "\r\n", // Windows line endings
useBom: true, // BOM for Excel compatibility
encoding: "utf-8",
};
}
}See the CsvSettings reference for all available options.
Lifecycle Events
WithEvents
Hook into the export lifecycle to execute custom logic at specific points during the export process. This is useful for adding charts, images, data validation, conditional formatting, or any other ExcelJS feature not covered by a concern interface.
import {
FromCollection,
WithHeadings,
WithEvents,
ExcelExportEvent,
} from "@nestbolt/excel";
import type {
BeforeExportEventPayload,
BeforeSheetEventPayload,
AfterSheetEventPayload,
} from "@nestbolt/excel";
class UsersExport implements FromCollection, WithHeadings, WithEvents {
collection() {
return this.users;
}
headings() {
return ["ID", "Name", "Email"];
}
registerEvents() {
return {
[ExcelExportEvent.BEFORE_EXPORT]: (event: BeforeExportEventPayload) => {
// Called once, before any sheets are created
// event.workbook -- the ExcelJS Workbook instance
// event.exportable -- the export class instance
console.log("Starting export...");
},
[ExcelExportEvent.BEFORE_SHEET]: (event: BeforeSheetEventPayload) => {
// Called before each sheet is populated
// event.worksheet -- the ExcelJS Worksheet instance
// event.exportable -- the export class instance
},
[ExcelExportEvent.AFTER_SHEET]: (event: AfterSheetEventPayload) => {
// Called after each sheet is fully populated
// event.worksheet -- the ExcelJS Worksheet instance
const ws = event.worksheet;
// Add data validation to a column
ws.getColumn("C").eachCell((cell, rowNumber) => {
if (rowNumber > 1) {
cell.dataValidation = {
type: "textLength",
operator: "greaterThan",
formulae: [0],
showErrorMessage: true,
errorTitle: "Invalid",
error: "Email cannot be empty",
};
}
});
},
[ExcelExportEvent.BEFORE_WRITING]: (event) => {
// Called once, after all sheets are populated but before the
// workbook is serialized to a buffer
// event.workbook -- the finalized ExcelJS Workbook
},
};
}
}Event Reference
| Event | Payload | When |
|---|---|---|
ExcelExportEvent.BEFORE_EXPORT | BeforeExportEventPayload | Once, before any sheets are created |
ExcelExportEvent.BEFORE_SHEET | BeforeSheetEventPayload | Before each sheet is populated with data |
ExcelExportEvent.AFTER_SHEET | AfterSheetEventPayload | After each sheet is fully populated |
ExcelExportEvent.BEFORE_WRITING | BeforeWritingEventPayload | Once, after all sheets are done, before serialization |
Each payload includes:
exportable-- the export class instance (or the current sheet's exportable for sheet-level events)workbook(forBEFORE_EXPORTandBEFORE_WRITING) -- the ExcelJSWorkbookinstanceworksheet(forBEFORE_SHEETandAFTER_SHEET) -- the ExcelJSWorksheetinstance
ExcelService Concern Methods
Use these methods when working with concern-based export classes:
download
Returns an ExcelDownloadResult with the buffer, filename, and content type:
const result = await this.excelService.download(
new UsersExport(users),
"users.xlsx",
);downloadAsStream
Returns a StreamableFile for direct controller return:
@Get("export")
async exportUsers() {
const users = await this.usersService.findAll();
return this.excelService.downloadAsStream(new UsersExport(users), "users.xlsx");
}store
Writes the export to a storage backend:
await this.excelService.store(
new UsersExport(users),
"reports/users.xlsx",
);
// Write to a named disk
await this.excelService.store(
new UsersExport(users),
"reports/users.xlsx",
undefined, // auto-detect type from extension
"s3",
);raw
Returns the raw file buffer:
import { ExcelType } from "@nestbolt/excel";
const buffer = await this.excelService.raw(new UsersExport(users), ExcelType.XLSX);Method Signatures
| Method | Returns | Description |
|---|---|---|
download(exportable, filename, type?) | ExcelDownloadResult | Buffer + filename + content type |
downloadAsStream(exportable, filename, type?) | StreamableFile | NestJS StreamableFile for direct controller return |
store(exportable, filePath, type?, disk?) | void | Write to storage (default or named disk) |
raw(exportable, type) | Buffer | Raw file buffer |
Combining Multiple Concerns
Concerns compose freely. Here is a full-featured export combining many concerns:
class FinancialReport
implements
FromCollection,
WithHeadings,
WithMapping,
WithTitle,
WithColumnWidths,
WithColumnFormatting,
WithStyles,
WithAutoFilter,
WithFrozenRows,
WithProperties,
ShouldAutoSize,
WithEvents
{
readonly shouldAutoSize = true as const;
constructor(private readonly transactions: Transaction[]) {}
collection() {
return this.transactions;
}
headings() {
return ["Date", "Description", "Category", "Amount", "Balance"];
}
map(tx: Transaction) {
return [tx.date, tx.description, tx.category, tx.amount, tx.runningBalance];
}
title() {
return "Transactions";
}
columnWidths() {
return { A: 12, B: 40, C: 18, D: 14, E: 14 };
}
columnFormats() {
return { A: "yyyy-mm-dd", D: "#,##0.00", E: "#,##0.00" };
}
styles() {
return {
1: {
font: { bold: true, color: "FFFFFF" },
fill: { type: "pattern" as const, pattern: "solid" as const, fgColor: "2F5496" },
},
};
}
autoFilter() {
return "auto";
}
frozenRows() {
return 1;
}
properties() {
return { creator: "Finance App", title: "Monthly Transactions" };
}
registerEvents() {
return {
[ExcelExportEvent.AFTER_SHEET]: ({ worksheet }) => {
// Add a summary row at the bottom
const lastRow = worksheet.lastRow?.number ?? 1;
const summaryRow = worksheet.getRow(lastRow + 2);
summaryRow.getCell(3).value = "Total:";
summaryRow.getCell(3).font = { bold: true };
summaryRow.getCell(4).value = { formula: `SUM(D2:D${lastRow})` };
summaryRow.getCell(4).numFmt = "#,##0.00";
},
};
}
}