<link rel="stylesheet" href="/assets/fonts/inter/inter.css" />
All posts

Angular with Looker Embedded: how to eliminate boilerplate and TypeScript interfaces for your data tables

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

AspectTraditional table in AngularAngular with Looker Embedded
Frontend code for a new datasetDozens or hundreds of lines: service, mapping, component, templateOne line of template, with a shared component
TypeScript interfaces per datasetAt least two: DTO and view modelNone
Adding a columnBackend and frontend development, review and deployA LookML model change, no frontend deploy
Pagination, sorting, filters, exportTo implement and testIncluded
Per-user data securityDedicated backend logicUser attributes and access filters in LookML
MaintenanceGrows with the number of tablesConcentrated 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.

💬 Reader notes

0 notes

Write a note

Share your opinion, a suggestion or a compliment

Latest notes

No notes yet. Be the first to comment!