Query Builder

Knex.js

Knex.js é um query builder de SQL, não um ORM completo. Ele compõe queries como JavaScript, vincula cada valor de forma segura, suporta diversos dialetos e oferece migrações e seeds junto com as queries.

beginner14 min readUpdated 16 de set. de 2026
knexfile.ts
ts
// knexfile.ts
import type { Knex } from "knex";

const config: { [key: string]: Knex.Config } = {
  development: {
    client: "pg",
    connection: process.env.DATABASE_URL,
    migrations: { directory: "./migrations" },
    seeds: { directory: "./seeds" },
    pool: { min: 2, max: 10 },
  },
  production: {
    client: "pg",
    connection: process.env.DATABASE_URL,
    pool: { min: 2, max: 10 },
  },
};

export default config;
Lançado
2013
Tipo
SQL query builder
Dialetos
Postgres, MySQL, SQLite, MSSQL, Oracle
Linguagem
JavaScript / TypeScript
Migrações
Schema builder integrado
Versão atual
3.x

Por que importa

O que um query builder oferece

SQL Componível

Encadeie select, where, join e orderBy para construir uma instrução peça por peça, e então use await para executar a query.

Schema builder e migrações

Crie e altere tabelas em JavaScript, versione cada mudança como uma migração e popule dados locais com a mesma ferramenta.

Vários dialetos, uma única API

O mesmo builder funciona com Postgres, MySQL e SQLite, bastando trocar o driver e usar ocasionalmente códigos específicos de cada dialeto.

O panorama completo

Três ideias por trás do Knex

Componha SQL como JavaScript, vincule valores com segurança e use o mesmo builder em diversos dialetos de banco de dados.

O builder

Compor

knex('users') inicia uma query, e cada método encadeado adiciona uma cláusula até que você aguarde o resultado.

O dialeto

Traduzir

O Knex transforma a cadeia do builder em SQL parametrizado para o cliente configurado, aplicando as aspas nos identificadores conforme o dialeto.

O pool

Conectar

Um pool de conexões sustenta cada query, e a instância do knex gerencia seu ciclo de vida.

Modelo de dados

A tabela de usuários, definida uma única vez

Definida em uma migração com o schema builder, para que o mesmo JavaScript crie a tabela em qualquer ambiente.

A tabela de usuáriosKnex migration
  • idincrementsChave primária inteira com auto-incremento
  • emailstring(255)Não nulo e único, imposto pelo banco de dados
  • display_namestring(120)O nome público exibido na interface
  • created_attimestampAssume o now() do banco de dados no insert

Definida em uma migração com o schema builder, para que o mesmo JavaScript crie a tabela em qualquer ambiente.

Uma breve historia

Um builder que sobreviveu ao seu ORM

  1. 2013

    Knex é lançado

    Um query builder para Node.js surge, reunindo migrações e um schema builder com a API de query.

    13
  2. 2016

    Objection.js baseia-se no Knex

    Uma camada de ORM aparece sobre o builder, provando que os dois podem ser combinados em vez de substituídos.

    16
  3. 2018

    Async/await em todo lugar

    As queries do Knex tornam-se thenables, fazendo com que o builder se encaixe naturalmente no JavaScript moderno.

    18
  4. 2020

    Knex 1.0 e TypeScript

    Uma linha principal estável chega com tipagens aprimoradas e cobertura contínua de dialetos.

    20
  5. 2023

    Knex 3.x

    O builder mantém uma superfície pequena e estável, permanecendo como dependência de ORMs maiores.

    23

O guia completo

Knex.js: Tudo que voce precisa saber

O que é Knex.js?

Knex.js é um SQL query builder para Node.js. Ele deliberadamente não é um ORM. Ele não mapeia linhas para classes, não rastreia o estado de objetos e não carrega relações por conta própria. O que ele faz é permitir que você construa instruções SQL como objetos JavaScript compostos, vincule cada valor de forma segura e as execute no Postgres, MySQL, SQLite, MSSQL ou Oracle.

Essa limitação é a razão de sua longevidade. O Knex surgiu em 2013 e tornou-se a base sobre a qual o Objection.js e diversas outras bibliotecas foram construídos. Se você deseja a ergonomia de um ORM, pode adicionar um por cima; se prefere manter-se próximo ao SQL, o Knex já está no nível certo.

O modelo mental é simples: comece com o nome de uma tabela, encadeie métodos para descrever a instrução e, então, await para executar. Cada encadeamento eventualmente se torna uma query parametrizada.

Por que usar um query builder?

Escrever SQL como template strings parece tranquilo até que uma query precise mudar de formato. Um endpoint de busca com filtros opcionais, uma ordenação que depende da entrada do usuário e uma cláusula de paginação são difíceis de montar via concatenação, e perigosos se qualquer valor for interpolado diretamente.

Um builder resolve ambos os problemas. As condições são adicionadas de forma limpa, os valores tornam-se parâmetros vinculados (bound parameters) e os identificadores são escapados de acordo com o dialeto de destino.

const query = db("users").select("id", "email");

if (search) {
  query.where("email", "ilike", `%${search}%`);
}
if (verifiedOnly) {
  query.whereNotNull("email_verified_at");
}

const users = await query.orderBy("created_at", "desc").limit(20);

Observe que search é passado como um argumento para where, e não concatenado em uma string. O Knex o envia para o driver como um placeholder, portanto, ele jamais poderá alterar a estrutura da instrução.

Outro benefício é a portabilidade. A mesma cadeia de métodos do builder funciona no PostgreSQL e MySQL apenas com uma mudança de configuração, o que é útil para desenvolvimento local com SQLite e para a execução de testes.

Instalação e o knexfile

Instale o Knex e o driver para o seu banco de dados. O driver é um pacote separado, pois o Knex não traz os clientes de banco de dados embutidos.

pnpm add knex pg
pnpm add -D @types/pg

A configuração fica em um knexfile, com uma entrada por ambiente. A CLI o lê automaticamente, e sua aplicação importa esse mesmo objeto.

import type { Knex } from "knex";

const config: { [key: string]: Knex.Config } = {
  development: {
    client: "pg",
    connection: process.env.DATABASE_URL,
    migrations: { directory: "./migrations" },
    seeds: { directory: "./seeds" },
    pool: { min: 2, max: 10 },
  },
  production: {
    client: "pg",
    connection: process.env.DATABASE_URL,
    pool: { min: 2, max: 10 },
  },
};

export default config;

Crie uma única instância compartilhada e importe-a em todos os lugares, em vez de chamar knex() em cada módulo. Uma instância detém um pool de conexões, e instâncias extras multiplicam as conexões com o banco de dados.

import knex from "knex";
import config from "../knexfile";

export const db = knex(config.development);

Use satisfies do TypeScript ou um objeto de configuração tipado para que o editor detecte opções escritas incorretamente, e mantenha os segredos no ambiente em vez de no arquivo.

Migrations com o schema builder

Migrations são a forma como o schema evolui ao longo do tempo. Cada arquivo possui um up que aplica a alteração e um down que a reverte.

npx knex migrate:make create_users
npx knex migrate:latest
npx knex migrate:rollback
npx knex migrate:list

O arquivo gerado é TypeScript comum. O schema builder descreve as tabelas através de um callback que recebe um objeto table.

import type { Knex } from "knex";

export async function up(knex: Knex): Promise<void> {
  await knex.schema.createTable("users", (table) => {
    table.increments("id").primary();
    table.string("email", 255).notNullable().unique();
    table.string("display_name", 120).notNullable();
    table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
  });
}

export async function down(knex: Knex): Promise<void> {
  await knex.schema.dropTableIfExists("users");
}

O Knex registra as migrations aplicadas em uma tabela knex_migrations, para que cada ambiente saiba exatamente quais arquivos foram executados. Sempre escreva um down real; mesmo que você raramente faça rollbacks, isso documenta como desfazer a alteração e torna possível o teste das migrations.

alterTable altera uma tabela existente. Algumas alterações, como adicionar uma coluna notNullable a uma tabela que já possui linhas, exigem um valor padrão ou uma etapa de backfill. Divida esses casos em duas migrations: uma para adicionar a coluna e realizar o backfill, e outra para adicionar a constraint.

Seeds para dados locais

Seeds populam um banco de dados com linhas conhecidas para desenvolvimento e testes. Eles são separados das migrations porque representam dados, não a estrutura.

import type { Knex } from "knex";

export async function seed(knex: Knex): Promise<void> {
  await knex("users").del();
  await knex("users").insert([
    { email: "[email protected]", display_name: "Ada Lovelace" },
    { email: "[email protected]", display_name: "Linus Torvalds" },
  ]);
}

Execute-os com npx knex seed:run. Seeds devem ser idempotentes sempre que possível, o que geralmente significa limpar a tabela ou usar um upsert antes de inserir, para que a execução duplicada não falhe nem duplique os dados.

Construindo queries

Uma query começa com o nome de uma tabela. A partir daí, os métodos são encadeados até que a instrução seja aguardada (awaited).

const rows = await db("users")
  .select("id", "email", "display_name")
  .where("created_at", ">", since)
  .whereIn("role", ["admin", "editor"])
  .orderBy("created_at", "desc")
  .limit(50)
  .offset(0);

where aceita diversas formas: uma coluna e um valor, uma coluna, operador e valor, ou um objeto de igualdades. whereIn, whereNot, whereNull, whereBetween e whereExists cobrem o restante dos predicados comuns. Condições agrupadas utilizam um callback para que os parênteses fiquem no lugar correto.

db("posts")
  .where("published", true)
  .andWhere((qb) => {
    qb.where("title", "ilike", `%${term}%`).orWhere("body", "ilike", `%${term}%`);
  });

Use .first() quando esperar uma única linha, e pluck quando quiser um array simples de apenas uma coluna.

const user = await db("users").where({ email }).first();
const emails = await db("users").pluck("email");

Joins, agregados e group by

Os joins são lidos da mesma forma que no SQL: uma tabela, seguida pelo par de colunas que a vincula.

const rows = await db("users as u")
  .join("posts as p", "p.author_id", "u.id")
  .whereNot("p.status", "draft")
  .groupBy("u.id", "u.email")
  .select("u.email")
  .count("p.id as post_count")
  .max("p.created_at as last_post_at")
  .orderBy("post_count", "desc")
  .limit(10);

join é um inner join, leftJoin mantém as linhas não correspondentes da primeira tabela, e rightJoin e fullOuterJoin estão disponíveis em dialetos que os suportam. A condição do join também pode ser um callback quando for necessária mais de uma cláusula.

Agregados utilizam .count(), .sum(), .avg(), .min() e .max(). Dois detalhes costumam confundir os desenvolvedores. Primeiro, qualquer coluna não agregada no select deve aparecer no groupBy. Segundo, no Postgres, os valores de count e sum retornam como strings para evitar overflow de inteiros, portanto, faça o cast ou parse quando precisar de números.

const { count } = await db("users").count("* as count").first();
const total = Number(count);

A cláusula returning

Inserir uma linha geralmente significa que você deseja a chave primária gerada ou os valores padrão do servidor. No Postgres e MSSQL, .returning() solicita que o banco de dados retorne a linha.

const [user] = await db("users")
  .insert({ email, display_name: displayName })
  .returning(["id", "email", "created_at"]);

Updates e deletes também podem retornar linhas:

const updated = await db("posts")
  .where({ id })
  .update({ title, updated_at: db.fn.now() })
  .returning("*");

MySQL e versões mais antigas do SQLite não suportam RETURNING. Nesses casos, a chamada de insert resolve para o novo id, e você deve emitir um select subsequente. Saber qual dialeto você está utilizando é fundamental, pois este é um dos pontos onde a promessa de portabilidade tem limitações.

Upserts e tratamento de conflitos

Um padrão comum é “insira esta linha, mas atualize-a se ela já existir”. O Knex expressa isso com onConflict, que mapeia para ON CONFLICT no Postgres e SQLite e para ON DUPLICATE KEY UPDATE no MySQL.

await db("users")
  .insert({ email, display_name: displayName, last_seen_at: db.fn.now() })
  .onConflict("email")
  .merge({
    display_name: displayName,
    last_seen_at: db.fn.now(),
  });

O onConflict recebe a coluna ou colunas únicas que definem o conflito. O merge atualiza as colunas listadas a partir da nova linha, enquanto o ignore ignora a inserção completamente. Esta é a maneira segura de tornar um sincronizador ou manipulador de webhook idempotente, pois o banco de dados resolve a condição de corrida entre dois escritores simultâneos em vez do código da sua aplicação.

Transações

Uma transação agrupa instruções para que elas sejam confirmadas (commit) ou revertidas (rollback) juntas. db.transaction passa um objeto de transação para o callback, e cada instrução interna deve utilizá-lo.

await db.transaction(async (trx) => {
  const account = await trx("accounts").where({ id: fromId }).first();

  if (!account || account.balance_cents < 5000) {
    throw new Error("insufficient_funds");
  }

  await trx("accounts").where({ id: fromId }).decrement("balance_cents", 5000);
  await trx("accounts").where({ id: toId }).increment("balance_cents", 5000);
});

Lançar um erro (throw) reverte a transação e rejeita a promise. Retornar um valor a confirma. O erro mais comum é misturar trx e db dentro do mesmo bloco: queries executadas em db utilizam uma conexão de pool diferente e rodam fora da transação, portanto, não são revertidas.

Para controle manual, db.transaction() sem um callback retorna um objeto de transação com os métodos commit e rollback. Prefira a forma de callback; é mais difícil causar vazamento de conexão por esquecer de realizar o commit.

Queries brutas quando você precisar

O builder não consegue expressar tudo, e nem tenta fazer isso. knex.raw executa uma string SQL com valores vinculados.

const result = await db.raw(
  `select date_trunc('day', created_at) as day, count(*) as signups
   from users
   where created_at >= ?
   group by 1
   order by 1`,
  [since],
);

const rows = result.rows;

Sempre use placeholders ? e passe os valores no array. Interpolar esses valores diretamente na string reintroduz exatamente o risco de injection que o builder existe para remover. Você pode embutir um fragmento bruto dentro de uma cadeia do builder com whereRaw ou select(db.raw(...)) quando apenas parte da query precisar de SQL escrito à mão.

Window functions, recursive CTEs, COPY e operadores específicos de dialetos são todos bons motivos para recorrer ao raw. Uma query de ranking, por exemplo, fica mais clara quando escrita por extenso do que quando montada a partir de fragmentos do builder:

const { rows } = await db.raw(
  `select email, score,
          row_number() over (order by score desc) as rank
   from leaderboard
   where season = ?`,
  [season],
);

Mantenha esses fragmentos pequenos e comentados, e prefira o builder para todo o restante ao redor deles.

Connection pooling

Cada query é executada através de um pool de conexões gerenciado pelo tarn.js. O padrão é um mínimo de duas e um máximo de dez, o que é razoável para um único processo, mas precisa de ajustes em produção.

const db = knex({
  client: "pg",
  connection: process.env.DATABASE_URL,
  pool: { min: 2, max: 10, acquireTimeoutMillis: 30_000 },
});

Dimensione o pool com base no max_connections do banco de dados, dividido entre todas as instâncias da aplicação. Um pool excessivamente grande é tão prejudicial quanto um muito pequeno: conexões demais esgotam a memória e a tabela de processos do servidor. Utilize um pooler como o PgBouncer à frente quando estiver executando muitas instâncias ou funções serverless.

Chame await db.destroy() quando o processo for encerrado. Sem isso, as conexões abertas mantêm o event loop ativo e o graceful shutdown trava.

Combinando Knex com Objection.js

O Knex retorna linhas simples, portanto, se você deseja modelos, relações e hooks de ciclo de vida, adicione o Objection.js. Ele é construído diretamente sobre o Knex e reutiliza a mesma conexão.

import { Model } from "objection";
import { db } from "./db";

Model.knex(db);

class User extends Model {
  static tableName = "users";

  static relationMappings = {
    posts: {
      relation: Model.HasManyRelation,
      modelClass: Post,
      join: { from: "users.id", to: "posts.author_id" },
    },
  };
}

const user = await User.query()
  .withGraphFetched("posts")
  .findOne({ email });

Essa camada é a resposta pragmática para equipes que desejam um ORM, mas não gostam de abstrações pesadas: o Knex cuida do SQL e das migrations, o Objection adiciona o modelo de objetos, e você pode retornar ao Knex a qualquer momento para uma query que a camada de modelo não atenda.

Você ainda escreve SQL

A coisa mais importante a internalizar sobre o Knex é que ele não esconde o SQL. Ele o reordena, parametriza e coloca as aspas, mas a instrução que ele produz é exatamente o que você teria escrito.

Isso traz duas consequências. A primeira é boa: ler uma cadeia do Knex revela a query, e debugar significa imprimir .toSQL() e ler o plano, em vez de tentar adivinhar o SQL gerado.

A segunda é uma responsabilidade: um builder não salvará você de um índice ausente, de um SELECT * em uma tabela extensa ou de um cross join acidental. Você ainda projeta o schema, adiciona os índices e verifica EXPLAIN ANALYZE. O Knex remove a “encanamento” de strings, não a necessidade de entender o banco de dados.

Boas práticas

  • Crie apenas uma instância do Knex e importe-a; nunca chame knex() por módulo.
  • Vincule cada valor como um parâmetro e use placeholders ? em queries raw.
  • Versione cada alteração de schema como uma migration com um método down real.
  • Divida alterações arriscadas em migrations separadas: adicione e faça o backfill primeiro, aplique as constraints depois.
  • Use .returning() onde o dialeto suportar e, caso contrário, utilize um select subsequente.
  • Use sempre o objeto de transaction dentro de db.transaction, nunca a instância de nível superior.
  • Dimensione o connection pool com base no limite do banco de dados e chame db.destroy() ao encerrar a aplicação.
  • Faça o cast de resultados count e sum do Postgres para números antes de realizar operações aritméticas.
  • Imprima .toSQL() quando uma query apresentar resultados inesperados e confirme a performance com EXPLAIN ANALYZE.

Erros comuns

  • Misturar trx e db em uma única transação, fazendo com que parte do trabalho escape do rollback.
  • Interpolar valores em strings knex.raw em vez de usar placeholders.
  • Esquecer o groupBy para colunas não agregadas e receber um erro de SQL.
  • Tratar a string retornada por count como um número e produzir "10" + 1.
  • Assumir que .returning() funciona de forma idêntica no MySQL e no SQLite.
  • Deixar um pool sem limite ou maior do que o banco de dados consegue suportar.
  • Criar uma nova instância do Knex por requisição e esgotar as conexões.
  • Editar um schema de produção manualmente e permitir que os ambientes fiquem dessincronizados.
  • Ignorar o método down até que um rollback seja realmente necessário.

Próximos passos

O Knex é um ótimo ponto de parada se você gosta de trabalhar próximo ao SQL, e serve como uma base sólida caso você queira adicionar um ORM posteriormente. Leia o guia de SQL para aprimorar as instruções que o builder produz, e o guia de PostgreSQL para aprender sobre índices, transações e planos de consulta. Se você prefere um cliente tipado com foco em schema, o Prisma gera um para você, enquanto o Drizzle ORM mantém-se mais próximo do estilo builder com inferência completa de TypeScript.

Na pratica

Migrações, queries, joins, transações

As quatro coisas que você mais faz com Knex, desde a criação da tabela até a alteração atômica.

migrations/20240101_create_users.ts
import type { Knex } from "knex";

export async function up(knex: Knex): Promise<void> {
  await knex.schema.createTable("users", (table) => {
    table.increments("id").primary();
    table.string("email", 255).notNullable().unique();
    table.string("display_name", 120).notNullable();
    table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
  });
}

export async function down(knex: Knex): Promise<void> {
  await knex.schema.dropTableIfExists("users");
}

Builder componível vs concatenação de strings

Construir SQL manualmente com template strings convida a bugs de injection e aspas. O builder vincula valores e escapa identificadores para você.

Preferir
const query = db("users").select("id", "email");

if (search) {
  query.where("email", "ilike", `%${search}%`);
}

const rows = await query.orderBy("created_at", "desc");
Evitar
let sql = "select id, email from users";

if (search) {
  // The value is interpolated straight into the statement.
  sql += ` where email ilike '%${search}%'`;
}

const rows = await db.raw(sql);

Migrações vs alteração manual do schema

Uma migração é um arquivo versionado e reversível que cada ambiente executa na mesma ordem. Schemas editados manualmente divergem e não podem ser reproduzidos.

Preferir
export async function up(knex: Knex) {
  await knex.schema.alterTable("users", (table) => {
    table.boolean("email_verified").notNullable().defaultTo(false);
  });
}

export async function down(knex: Knex) {
  await knex.schema.alterTable("users", (table) => {
    table.dropColumn("email_verified");
  });
}
Evitar
-- Run once in a terminal against production,
-- then forgotten and never applied to staging.
ALTER TABLE users ADD COLUMN email_verified boolean;

Trade-offs

Um query builder é a camada certa para você?

O Knex fica entre o SQL puro e um ORM completo. Essa posição é uma força para algumas equipes e uma cerimônia extra para outras.

Strengths

  • Você mantém o SQL visível

    A cadeia de métodos parece a instrução que ela produz, portanto não há geração de query oculta nem mágica para depurar.

  • Seguro por construção

    Valores são sempre vinculados como parâmetros, identificadores são citados por dialeto e filtros dinâmicos são compostos sem risco de injection.

  • Migrações e seeds inclusos

    O schema builder, migrações e seeds vêm no mesmo pacote, então você não precisa de uma segunda ferramenta para gerenciar o banco de dados.

Trade-offs

  • Sem modelos ou relações

    O Knex retorna linhas simples. Você escreve seu próprio mapeamento e, se quiser que as relações sejam carregadas automaticamente, precisará do Objection.js ou de um ORM por cima.

  • Tipagem manual

    As linhas são fracamente tipadas por padrão. Você anota as formas dos resultados ou adiciona uma camada tipada, o que exige disciplina conforme o schema cresce.

  • Diferenças de dialeto vazam

    A API é portátil, mas o SQL não é. Operadores JSON, upserts e returning comportam-se de forma diferente, portanto, teste no banco de dados onde você fará o deploy.

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender Knex.js?

Nosso tutorial interativo te guia por Knex.js passo a passo — com quizzes e codigo real que voce pode executar no navegador.