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.
const prompt = 'You are an invoicing assistant.\n' + 'Customer ' + customerName + ' has ' + unpaidCount + ' unpaid invoices.\n' + 'Their plan is ' + planName + '.\n' + 'Reply in fewer than 200 words.';The source no longer looks like the string it produces, and one missing \n collapses the prompt into a run-on line the model misreads.
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.
Backticks as the default
Section titled “Backticks as the default”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.
Multi-line strings and leaked indentation
Section titled “Multi-line strings and leaked indentation”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.
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 tag is a function call
Section titled “A tag is a function call”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.'.
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`);strings is ['', ' ', 's online']: the leading segment is empty because the literal starts with ${count}, not text. The reduce interleaves segments and values into '' + '' + '3' + ' ' + 'admin' + 's online'. The final segment 's online' has no matching value (one more segment than values), so the ternary skips that append.Two tags worth importing: sql and dedent
Section titled “Two tags worth importing: sql and dedent”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.
import dedent from 'dedent';
const buildPrompt = (customerName: string, unpaidCount: number): string => { return dedent` You are an invoicing assistant. Customer ${customerName} has ${unpaidCount} unpaid invoices. Reply in fewer than 200 words. `;};Same source, clean output. The tag strips the common leading indentation and trims the outer newlines, so the source stays aligned with the function body while the runtime string comes out indentation-free.
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.
Check your understanding
Section titled “Check your understanding”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.
`/invoices/${id}`.${id}.dedent`...` — strips the function-body indentation at runtime.sql`...` — binds ${orgId} as a parameter, not inlined text.${user.id} and ${action}.External resources
Section titled “External resources”The full reference for backtick syntax, interpolation, and the tagged-template form, with the edge cases not covered here.
The Drizzle escape hatch this lesson previews, including the sql.raw, sql.join, and sql.identifier helpers the data layer chapters cover in depth.
The package the course uses for every multi-line template literal. One import, one tag, zero indentation noise.