Skip to content
Chapter 1Lesson 5

Backticks and tagged templates

JavaScript template literals and tagged templates, the mechanic behind the sql and dedent tags used throughout the course.

Two strings, each behind a production bug. The first opens the door to SQL injection ; the second turns a five-line system prompt into a tangle of + '\n' calls no one wants to edit. They look nothing alike, yet share one habit: each is assembled by concatenation.

const email = req.body.email;
const query = 'SELECT * FROM users WHERE email = \'' + email + '\'';
const rows = await db.execute(query);

Send email as x' OR '1'='1 and the attacker’s text becomes part of the query, returning every user in the table.

The fix for both is the same: reach for backticks by default, and add a tag when the string is structured. Two are worth knowing on sight: Drizzle’s sql`...` makes hand-written queries safe by default, and npm’s dedent`...` fixes multi-line indentation with one import.

A template literal is a string written with backticks instead of quotes, adding two things: ${expression} interpolation, where the expression is anything that evaluates to a value, and newlines that come through as newlines.

The shapes you’ll reach for most:

const path = `/invoices/${invoiceId}`;
const heading = `${count} active invoices`;
const className = `rounded-md ${variant === 'primary' ? 'bg-blue-600' : 'bg-zinc-200'}`;
const log = `User ${user.id} requested invoice ${invoice.id}`;

Build any string from variables with a template literal. Gluing values in with + reads worse, loses the placeholder structure that shows the intent, and is the first step toward the two bugs the lesson opened with. Keep single quotes for plain strings, but the moment a ${} or a newline appears, reach for backticks.

Backticks preserve newlines too, which makes them the default for anything spanning more than one line: log messages, system prompts, fixture text, email bodies. The catch is that they preserve every character between them, including whitespace you never meant to ship.

const buildPrompt = (customerName: string): string => {
return `
You are an invoicing assistant.
Customer ${customerName} has unpaid invoices.
Reply in fewer than 200 words.
`;
};

The string keeps the leading newline after the opening backtick, the indentation aligning each line with the function body, and the trailing newline before the closing backtick.

const buildPrompt = (customerName: string): string => {
return `
You are an invoicing assistant.
Customer ${customerName} has unpaid invoices.
Reply in fewer than 200 words.
`;
};

That indentation lands in the output: mildly annoying for a model reading the prompt, and outright broken for whitespace-sensitive formats like YAML or Markdown code fences, where leading spaces change what the parser sees.

1 / 1

Pulling the lines to the left margin cleans the output but leaves the source out of step with the function body, and any reformatting editor pushes it back. The better fix keeps the source aligned and strips the indentation at runtime, which is a job for a tag.

String.raw is a built-in tag that returns the template’s raw text without processing escape sequences, useful for Windows paths or regex sources where \n shouldn’t become a newline.

A tagged template is a function call with its arguments rearranged. tag`Hello, ${name}!` has the same shape as tag(['Hello, ', '!'], name): the static segments arrive as one array, the interpolated values as separate arguments.

A small currency tag makes the shape concrete: it takes integer-cents numbers, the same 1995 from the “Store cents, not dollars” lesson, and interpolates them as formatted dollar strings.

const currency = (
strings: TemplateStringsArray,
...values: number[]
): string => {
return strings.reduce((acc, str, i) => {
const value = values[i];
const formatted = value === undefined ? '' : `$${(value / 100).toFixed(2)}`;
return acc + str + formatted;
}, '');
};
const cents = 1995;
const message = currency`Your total is ${cents} including tax.`;

The static segments (the text between the ${'${}'} placeholders) arrive as an array, the resolved ${'${}'} values as rest parameters. The strings array always holds one more element than values.

const currency = (
strings: TemplateStringsArray,
...values: number[]
): string => {
return strings.reduce((acc, str, i) => {
const value = values[i];
const formatted = value === undefined ? '' : `$${(value / 100).toFixed(2)}`;
return acc + str + formatted;
}, '');
};
const cents = 1995;
const message = currency`Your total is ${cents} including tax.`;

A plain number, the integer-cents value from the “Store cents, not dollars” lesson.

const currency = (
strings: TemplateStringsArray,
...values: number[]
): string => {
return strings.reduce((acc, str, i) => {
const value = values[i];
const formatted = value === undefined ? '' : `$${(value / 100).toFixed(2)}`;
return acc + str + formatted;
}, '');
};
const cents = 1995;
const message = currency`Your total is ${cents} including tax.`;

The tag call. The runtime splits the literal into ['Your total is ', ' including tax.'] and the value 1995, then calls currency(['Your total is ', ' including tax.'], 1995).

const currency = (
strings: TemplateStringsArray,
...values: number[]
): string => {
return strings.reduce((acc, str, i) => {
const value = values[i];
const formatted = value === undefined ? '' : `$${(value / 100).toFixed(2)}`;
return acc + str + formatted;
}, '');
};
const cents = 1995;
const message = currency`Your total is ${cents} including tax.`;

The tag interleaves each segment with the formatted value, returning a regular string: 'Your total is $19.95 including tax.'.

1 / 1

The shape is fixed: a tag receives a TemplateStringsArray first, then the interpolated values. What it does with them is open: escape HTML, build a parameterized SQL query that never inlines the values, strip leading whitespace, validate and throw, even return something other than a string.

You won’t write tags in this course; the two you’ll use are imported. The walkthrough is here so you know what happens at the call site when you meet sql`...` or dedent`...` later.

To check the shape has landed, predict what the tag below prints.

Predict what this program prints, then press Check.

const join = (
strings: TemplateStringsArray,
...values: unknown[]
): string => {
return strings.reduce((acc, str, i) => {
return acc + str + (i < values.length ? String(values[i]) : '');
}, '');
};
const role = 'admin';
const count = 3;
console.log(join`${count} ${role}s online`);

Two tagged templates earn their place in a real web app. sql keeps your queries injection-proof; dedent keeps multi-line strings readable. Both are called like the currency tag you just saw, only the work differs.

sql`...`: parameterized queries by default

Section titled “sql`...`: parameterized queries by default”

Every SQL client and ORM ships a sql tag that parameterizes the interpolated values automatically. Drizzle exposes one as the escape hatch for queries its builder can’t express: each value inside ${} is sent to the database as a separately bound parameter, never inlined into the query text.

Focus on the sql-tagged literal below and ignore the surrounding db.execute call; you’ll meet this shape again in the Drizzle chapter.

import { sql } from 'drizzle-orm';
const status = 'paid';
const orgId = '019385f0-1234-7000-a000-000000000001';
const rows = await db.execute(
sql`SELECT id, total_cents FROM invoices
WHERE org_id = ${orgId} AND status = ${status}
ORDER BY created_at DESC LIMIT 50`,
);

The tagged form is what makes the query safe, not the developer’s discipline or a linter rule. Where + inlines orgId straight into the SQL, the sql tag binds each value to a numbered placeholder ($1, $2, and so on) and lets the driver handle the rest. Writing an injectable query means deliberately leaving the tagged form, a red flag in code review.

dedent`...`: multi-line strings without indentation noise

Section titled “dedent`...`: multi-line strings without indentation noise”

The leaked indentation from the backticks section is exactly the kind of problem a tag can fix. The dedent package does just that: the source stays aligned with its surroundings while the runtime string comes out clean.

const buildPrompt = (customerName: string, unpaidCount: number): string => {
return `
You are an invoicing assistant.
Customer ${customerName} has ${unpaidCount} unpaid invoices.
Reply in fewer than 200 words.
`;
};

Indentation leaks into the output. Every line carries the function-body indentation, and the string opens and closes with stray newlines. Fine if the consumer ignores whitespace, broken for YAML, Markdown code fences, or any LLM that reads literal whitespace as part of the prompt.

Every alternative has a cost: un-indenting the string misaligns the source, concatenation reintroduces the bug from the start of the lesson, and a hand-rolled .replace(/^ {4}/gm, '') is just a worse version of the same thing. One import gives you a consistent shape and a source that stays readable, the same way every time the course composes prompts, email bodies, or test fixtures.

Match each string on the left to the form you’d reach for to write it.

Match each string to the right shape. Click an item on the left, then its match on the right. Press Check when done.

A short interpolated URL path like `/invoices/${id}`.
Plain template literal — no tag, just ${id}.
A 200-line system prompt the team edits inside the source file.
dedent`...` — strips the function-body indentation at runtime.
A hand-written SQL query selecting rows by a user-submitted org ID.
sql`...` — binds ${orgId} as a parameter, not inlined text.
A one-line log message built from a user ID and an action name.
Plain template literal — no tag, just ${user.id} and ${action}.