Ultimamente tenho utilizado knex.js, para fazer consultas e gerenciar o banco de dados de um backend em NodeJS. Trata-se de uma ferramenta bem simplificada e agradável para gerenciamento de banco de dados, apesar de enfrentar alguns obstáculos com relação a integração com typescript. Por isso, trago aqui a maneira como configurei o Knex no meu projeto, que utiliza ESM e Typescript, também trabalhando com migrations e seeds.
Para usar o Knex, teremos que configurar o driver do banco de dados que vamos usar, neste exemplo utilizei o postgresql, portanto:
npm install knex --save
# Instalando o driver para usar postgresql
npm install pg --save
Caso não esteja utilizado postgresql, descubra o driver que você deve instalar aqui: https://knexjs.org/guide/
Primeiro, com seu banco criado e rodando, aconselho criar um arquivo .env na raiz do seu projeto para armazenar as credenciais do seu banco de dados, exemplo:
# NODE_ENV define o ambiente em que estamos rodando o app
# Vamos usar isso depois para rodar as seeds
NODE_ENV=development
# Configuração do banco de dados
DATABASE_URL=jdbc:postgresql://localhost:5432/nome_do_banco
DB_HOST=localhost
DB_PORT=5432
DB_USER=nome_do_usuario
DB_NAME=nome_do_banco
DB_PASSWORD=sua_senha
Feito isso, crie um arquivo knexfile.ts na raiz do projeto, nesse arquivo iremos configurar a conexão do knex com o banco:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { Knex } from 'knex';
import { config } from 'dotenv';
// Carrega o .env para usarmos abaixo
config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const development: Knex.Config = {
client: 'pg', // O driver do postgresql, substitua pelo driver que você instalou
connection: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
charset: 'utf8',
},
pool: {
min: 2,
max: 10,
},
migrations: {
tableName: 'knex_migrations',
directory: __dirname + '/knex/migrations',
extension: 'ts',
loadExtensions: ['.js', '.ts']
},
seeds: {
directory: __dirname + '/knex/seeds',
extension: 'ts',
loadExtensions: ['.js', '.ts']
},
};
// Obs: Você pode configurar outros ambientes também, por exemplo:
const production = {
client: 'pg',
connection: {},
pool: {},
migrations: {}
};
export default { development, production };
Pronto, agora os dados do .env serão utilizado pelo knex e também definimos o local onde as migrations e as seeds serão criadas.
Agora, vamos finalizar criando um arquivo para colocar algumas funções para testar o banco, fazer o setup do banco de dados, etc.
Crie o arquivo src/database/index.ts:
import config from '../../knexfile';
import knex from 'knex';
// No meu caso, eu defini três ambientes no knexfile.ts, porém você pode colocar development para testar
type KnexEnv = 'development' | 'staging' | 'production';
const environment = (process.env.NODE_ENV as KnexEnv) || 'development';
// Use essa instância toda vez que for interagir com o banco de dados
export const db = knex(config[environment]);
const testConnection = async () => {
try {
await db.raw('SELECT 1+1 AS result');
console.log('Database connection successful');
return null;
} catch (error) {
console.error('Database connection failed:', error);
throw error;
}
}
export const setupDatabase = async () => {
try {
await testConnection();
// Se o ambiente for development, o banco será recriado e vamos rodar os dados mockados
if (process.env.NODE_ENV === 'development') {
await db.migrate.rollback({}, true);
await db.migrate.latest();
await db.seed.run();
} else {
await db.migrate.latest();
}
} catch (error) {
console.log('Database setup failed');
}
}
Pronto, agora você irá usar essa instância db em todo seu app para consultar o banco de dados. Também criamos a função setupDatabase, você deve chamar esta função ao rodar seu servidor. Por exemplo:
server.listen(3000, async () => await setupDatabase());
Lembre-se que seu tsconfig.json deve estar configurado para lidar com ESM, veja o exemplo:
{
"compilerOptions": {
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ESNext",
"strict": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"outDir": "dist",
"lib": [ "esnext" ],
"types": [ "node" ],
},
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
},
"exclude": [ "node_modules" ],
"include": [
"src/**/*.ts",
"**/*.ts",
"bin/*.ts",
"knexfile.ts"
]
}
Para criar migrations e seeds já com extensão .ts e em ESM, precisamos antes configurar alguns scripts no package.json:
"scripts": {
"knex": "NODE_OPTIONS='--loader ts-node/esm --disable-warning=ExperimentalWarning' knex",
"migrate": "npm run knex migrate:latest",
"migrate:make": "npm run knex migrate:make",
"seed": "npm run knex seed:run",
"seed:make": "npm run knex seed:make"
},
Agora vamos criar nossa primeira migration:
npm run migrate:make create_table_users
Se tudo deu certo, um arquivo xxxxxx_create_table_users.ts foi criado no /knex/migrations (definimos isso no knexfile.ts anteriormente), agora vamos criar a tabela users, devemos definir duas funções, uma função up e outra down, uma serve para fazermos a alteração no banco e a outra desfaz a alteração feita, respectivamente:
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').notNullable().unique();
table.string('password').notNullable();
table.string('name').notNullable();
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable('users');
}
Agora vamos criar a seed para users:
npm run seed:make 01_users
O arquivo 01_users.ts será criado, dentro dele teremos uma única função seed para inserir dados no banco users:
import { Knex } from "knex";
export async function seed(knex: Knex): Promise<void> {
await knex('users').del();
return await knex('users').insert([
{ id: 'eba7c689-708b-4d6d-a15e-0bb72c5bc089', name: 'John Doe', email: 'john@email.com', password: '12345' },
{ id: 'b3c9627f-f665-4237-8731-7a88aba9fc8b', name: 'Test', email: 'test@email.com', password: '12345' }
]);
};
Agora, se NODE_ENV estiver definido como "development" no .env, você pode apenar rodar a aplicação que as migrations e as seeds serão executadas, ou então rode no terminal:
npm run migrate && npm run seed
Agora ao fazer uma consulta na tabela users, dois usuários mockados serão retornados.
Aqui está um exemplo de um use case que retorna um usuário pelo seu id:
export async function getUser(req: Request, res: Response) {
try {
const { id } = req.params;
const user = await db<IUser>('users')
.where('id', id)
.first();
if (!user) {
res.status(404).json(null);
}
res.status(200).json(user);
} catch (error) {
console.error('Error fetching user:', error);
}
}
Por fim, temos um projeto typescript completamente configurado com Knex, com um bom versionamento do banco de dados e seeds para ambiente de desenvolvimento.
Para mais informações, consulte a documentação oficial do Knex: https://knexjs.org/guide
Veja também o projeto completo utilizando Knex: https://github.com/HarlonGarcia/express-api-controle-de-gastos