Skip to content
Chapter 1Lesson 5

Backticks and tagged templates

How JavaScript template literals and tagged templates let you build strings safely, the foundation behind the sql and dedent tags you will use throughout the course.

This lesson starts with two strings, each responsible for 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 that no one wants to edit. The two look nothing alike, but they share one habit: each string is assembled by concatenation. By the end of this lesson you’ll know the one syntax that fixes both, and the two tagged templates that make the fix safe.

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

If email arrives as x' OR '1'='1, the query reads every user in the table. Concatenation is what lets the attacker’s text become part of the query.

Both bugs trace back to the same root: a string built by gluing pieces together with +. The fix is the same for both. Reach for backticks by default, and add a tag when the string is structured. Two tags are worth knowing on sight in 2026. The first is sql`...` from Drizzle, which makes hand-written queries safe by default. The second is dedent`...` from npm, which fixes the multi-line indentation problem with a single import.

A template literal is a string written with backticks instead of single or double quotes, with two additions: ${expression} interpolation, and newlines that come through as newlines. The expression inside ${...} can be anything that evaluates to a value: a variable, a property access, a function call, a ternary.

Here are four shapes you’ll reach for on a typical day:

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}`;

The rule of thumb is simple: any string built from variables uses a template literal. Reaching for + to glue a value into a string is a warning sign. It reads less clearly, and it loses the placeholder structure that makes the intent obvious. It is also the first step toward the two bugs the lesson opened with. The course defaults to single quotes for plain strings, since they are the simpler form when no interpolation is needed, but the moment a ${} or a newline shows up, backticks are the answer.

Backticks also preserve newlines, which is why they’re the default for anything that spans more than one line: structured log messages, system prompts, fixture text, email bodies. The catch is that backticks preserve every character between them, not just the newlines, and that includes whitespace the author 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 preserves every character between the backticks: the leading newline after the opening backtick, the indentation on each line that aligns with the surrounding 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.
`;
};

Every line carries the function-body indentation into the runtime output. That’s mildly annoying when a model reads the prompt, and outright broken for whitespace-sensitive formats like YAML or Markdown code fences, where the leading spaces change what the parser sees.

1 / 1

The natural reaction is to un-indent the string by dragging the lines back to the left margin, so the content comes out clean. That works, but the source then sits out of step with the surrounding function body, and every editor that reformats code tries to push it back into alignment. The better fix is to keep the source aligned and strip the indentation at runtime instead. That is a job for a tag, which the next section introduces.

One related tool is worth a brief mention. String.raw is a tag function on String that returns the template’s raw text without processing escape sequences. It is useful when authoring Windows paths or regular-expression sources, where \n shouldn’t become a newline. You won’t reach for it often, but if you see String.raw`C:\Users\${name}` in a library, you’ll know what it is doing.

A tagged template looks like a brand-new piece of syntax, but underneath it’s a function call with the arguments rearranged. tag`Hello, ${name}!` is the same shape as tag(['Hello, ', '!'], name). The backticks aren’t a string going into a function; they’re the function-call syntax itself, with the template’s static segments and dynamic values handed in separately.

Once you see past the syntax, the rest is ordinary function-call behavior. A small example makes the shape concrete: a currency tag that 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 tag receives two things: an array of the static string segments (everything between the ${'${}'} placeholders), and the resolved values from each ${'${}'}, spread as rest parameters. The strings array always has exactly one more element than the values array.

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.`;

This is a plain number, the same integer-cents value the “Store cents, not dollars” lesson stored. The tag doesn’t care where it came from; it just receives a number.

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.`;

This line is the tag call. The runtime splits the literal into ['Your total is ', ' including tax.'] and the value 1995, then passes them to currency exactly as if you’d called 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 walks the segments, interleaving each one with the formatted value. The result is a regular string, 'Your total is $19.95 including tax.', that the caller uses like any other.

1 / 1

The shape is fixed: a tag function always receives a TemplateStringsArray first and the interpolated values after. What happens next is entirely up to the tag. It can escape HTML before interleaving. It can build a parameterized SQL query that never inlines the values into the query text. It can strip the common leading whitespace from every line. It can validate the inputs and throw on bad ones. It can even return something other than a string: a SQL fragment object, a React element, a typed query builder. The syntax doesn’t constrain any of this; it just hands over the array and the values.

You won’t write production tag functions in this course. The two tags you’ll actually use are imported, not hand-written. The walkthrough above is there so you can recognize the pattern: when you see sql`...` or dedent`...` in later lessons, you’ll know exactly what’s happening at the call site.

To check that the segments-and-values 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 tags earn their place in a 2026 SaaS codebase. The first keeps your SQL injection-proof, and the second keeps multi-line strings readable. The call-site syntax is identical to the currency tag you just walked through. Only the work each one does is different.

sql`...`: parameterized queries by default

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

Every modern SQL client and ORM in 2026 ships a sql tagged template that automatically parameterizes the interpolated values. Drizzle exposes one as its escape hatch for the rare queries its query builder can’t express, and the values inside the ${} placeholders are sent to the database as separately bound parameters, never as inlined strings.

Here’s the shape you’ll see when the course covers Drizzle properly a few units from now. Don’t worry about the surrounding db.execute call yet; the part to focus on is the sql-tagged literal.

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-template shape is what makes the SQL safe by default, rather than the developer’s discipline or a linter rule. A string built with + would inline orgId straight into the SQL text, leaving the developer to remember to escape it by hand every single time. The sql tag instead intercepts the value and binds it to a numbered placeholder ($1, $2, and so on), and the database driver handles the rest. To write a SQL-injectable Drizzle query, you have to deliberately leave the tagged form. That escape hatch is named, rare, and a red flag in code review when it shows up.

The data layer unit later in the course covers Drizzle in full, including when the sql tag is the right choice over the query builder. The shape above is what you’ll recognize on sight when that unit opens.

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

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

The indentation gotcha from the backticks section is exactly the kind of problem a tag can fix. The dedent package on npm exports a tag that strips the common leading whitespace from every line of a multi-line template literal. The source stays aligned with its surroundings; the runtime output 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. That’s fine if the consumer doesn’t care, and broken for YAML, Markdown code fences, or any LLM that reads literal whitespace as part of the prompt.

dedent earns its place because every alternative has a cost. Un-indenting the string leaves the source misaligned with the surrounding function. Building the string with concatenation reintroduces the bug class the lesson opened with. A hand-rolled .replace(/^ {4}/gm, '') is just a worse version of what dedent already does. One import gives you a consistent shape and a source that stays readable.

You’ll see dedent in several places later in the course: email bodies when the course reaches transactional email, system prompts wherever the course composes them, and fixture text in tests. The usage is identical every time, so once you recognize it here you’ll read it without effort later.

When you sit down to write a string, which shape should you reach for? Match each string described on the left to the form that fits 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}.