In every business application there comes the moment of table number twenty: another TypeScript interface, another DTO, another component with pagination, sorting, filters and export. It's useful code, but always the same, and it grows with every request. In this article I show how, for reporting and analytical data, Angular with Looker Embedded lets you remove almost all of that boilerplate.
The problem with traditional boilerplate
The classic approach to showing a dataset in Angular always needs the same pieces:
- Interfaces and DTOs that duplicate the backend data shape on the frontend, to be updated on every change.
- A dedicated table component, with Angular Material or PrimeNG, hand-defined columns and templates for each field.
- Pagination, sorting and filters to wire up and test, often on the server side too.
- Formatting of currencies, dates and statuses, repeated in every table.
- Export to CSV or Excel, hand-written or delegated to yet another library.
Here's what a single orders table looks like, in a reduced form:
// 1. The API contract, duplicated on the frontend
export interface OrderDto {
id: number; customer_name: string; region: string;
total_cents: number; status: 'OPEN' | 'PAID' | 'CANCELLED'; created_at: string;
}
// 2. The view model, because the API shape is not the table shape
export interface OrderRow {
id: number; customer: string; region: string;
total: number; status: string; createdAt: Date;
}
@Component({ /* ... MatTableModule, MatPaginatorModule, MatSortModule ... */ })
export class OrdersTableComponent implements AfterViewInit {
readonly columns = ['id', 'customer', 'region', 'total', 'status', 'createdAt'];
readonly dataSource = new MatTableDataSource<OrderRow>([]);
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
constructor(private api: OrdersApi) {
this.api.list().subscribe(dtos => (this.dataSource.data = dtos.map(toRow)));
}
ngAfterViewInit(): void {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
applyFilter(value: string): void { this.dataSource.filter = value.trim().toLowerCase(); }
exportCsv(): void { /* build the CSV by hand, escape quotes, trigger the download... */ }
}
// 3. Mapping and formatting, also by hand
const toRow = (d: OrderDto): OrderRow => ({
id: d.id, customer: d.customer_name, region: d.region,
total: d.total_cents / 100, status: d.status, createdAt: new Date(d.created_at),
});
// ...plus the HTML template with one <ng-container matColumnDef> per column
And that's just the frontend: every new column means changing the DTO, the mapping, the template, the tests, and releasing backend and frontend together.
The turning point: Angular with Looker Embedded
With Looker, the data contract and its presentation live in the LookML model: dimensions, measures, formats, labels and relationships are defined once, on the analytics side. The frontend no longer needs to know the shape of the data: it only displays ready-made content, which Looker embeds in a secure iframe.
You initialise the SDK once for the application, pointing it at our backend endpoint that signs embed URLs:
// app.config.ts — once for the whole application
import { LookerEmbedSDK } from '@looker/embed-sdk';
export const appConfig: ApplicationConfig = {
providers: [
// The SDK asks our backend for a signed embed URL for each content item
provideAppInitializer(() => LookerEmbedSDK.init('analytics.example.com', '/api/looker/embed-auth')),
],
};
Then all you need is a generic component, <app-looker-table>, which only receives the content ID and knows nothing about columns or types:
@Component({
selector: 'app-looker-table',
template: `<div #host class="looker-host"></div>`,
styles: `.looker-host, .looker-host iframe { width: 100%; height: 100%; border: 0; }`,
})
export class LookerTableComponent {
/** Look ID defined in Looker: the component knows nothing about the data. */
readonly lookId = input.required<string>();
private readonly host = viewChild.required<ElementRef<HTMLDivElement>>('host');
constructor() {
afterNextRender(() => {
LookerEmbedSDK.createLookWithId(this.lookId())
.appendTo(this.host().nativeElement)
.build()
.connect()
.catch(err => console.error('Looker embed failed', err));
});
}
}
// Usage: the same component for any dataset
// <app-looker-table lookId="42" /> orders
// <app-looker-table lookId="57" /> invoices
Notice what's missing: no interface, no DTO, no column definitions, no export function. The same component shows orders, invoices or any other dataset.
The benefits for the team
New columns without a frontend deploy
Adding a column or changing a format or label is a change to the LookML model or the Look: as soon as it's saved, it's visible in the application. The frontend needs neither a rebuild nor a release.
Export, drill-down and pagination included
Downloads to CSV, Excel and PDF, sorting, pagination and drill-down on values are Looker features, already tested and consistent across every table, enabled through the user's permissions.
Row-level security
With signed embedding, our backend generates the signed URL with the logged-in user's attributes. In the LookML model an access_filter uses those attributes to filter every query: a user can't see another region's rows, not even by tampering with the URL.
// NestJS: signs the embed URL with the logged-in user's attributes
@Get('looker/embed-auth')
async embedAuth(@Query('src') src: string, @CurrentUser() user: User) {
const embed = await this.looker.ok(this.looker.create_sso_embed_url({
target_url: `https://analytics.example.com${src}`,
session_length: 3600,
external_user_id: String(user.id),
first_name: user.firstName,
last_name: user.lastName,
permissions: ['access_data', 'see_looks', 'download_without_limit'],
models: ['sales'],
user_attributes: { region: user.region }, // drives row-level security
}));
return { url: embed.url };
}
# LookML: every query on this explore is filtered by the user's region
explore: orders {
access_filter: {
field: customers.region
user_attribute: region
}
}
The comparison
| Aspect | Traditional table in Angular | Angular with Looker Embedded |
|---|---|---|
| Frontend code for a new dataset | Dozens or hundreds of lines: service, mapping, component, template | One line of template, with a shared component |
| TypeScript interfaces per dataset | At least two: DTO and view model | None |
| Adding a column | Backend and frontend development, review and deploy | A LookML model change, no frontend deploy |
| Pagination, sorting, filters, export | To implement and test | Included |
| Per-user data security | Dedicated backend logic | User attributes and access filters in LookML |
| Maintenance | Grows with the number of tables | Concentrated in the data model |
When not to use it
- Editable tables: inserting, inline editing and row actions remain the application's job.
- Highly customised interfaces: the content lives in an iframe, so styling is adapted through Looker themes, not the app's CSS.
- Cost and infrastructure: Looker is a licensed platform with a data model to maintain; it pays off when datasets are many and change often.
- Application logic still needs its types: interfaces disappear for reporting tables, not for forms and business rules.
Conclusions
- Time-to-market: a new report becomes a line of template, not a sprint.
- Maintainability: the Angular codebase stops growing with every dataset; complexity stays in the data model, where it belongs.
- Security: data visibility rules are centralised and applied to every query.
- Clear roles: frontend developers focus on the application experience, data people on the model.
For reporting and analytical data, Angular with Looker Embedded moves the work from repetitive code to the data model. If you want to dig into how to organise the rest of the application, I've written about Angular architecture and design systems with reusable components; if you're considering a similar integration, get in touch.