Modern Development Environment: Docker, Express, SQLite3, and Sequelize
Understanding Our Development Stack
Think of our development environment like building a modern kitchen:
- Docker is like having a portable kitchen that's identical everywhere
- Express is like your set of cooking tools and techniques
- SQLite3 is like your pantry where you store ingredients
- Sequelize is like having a smart assistant that helps organize your pantry
Why This Stack?
This combination provides several benefits:
- Docker ensures consistent environments across all developers
- Express provides a robust framework for building web applications
- SQLite3 offers a lightweight, file-based database perfect for development
- Sequelize simplifies database operations with a powerful ORM
Project Structure
project_root/
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
├── src/
│ ├── config/
│ │ └── database.js
│ ├── models/
│ │ └── index.js
│ ├── migrations/
│ ├── seeders/
│ └── app.js
├── .sequelizerc
├── package.json
└── README.md
This structure is like organizing your kitchen where everything has its place:
- docker/ holds our environment configuration
- src/ contains our application code
- config/ manages our settings
- models/ defines our data structures
Setting Up Docker
Dockerfile
# docker/Dockerfile
FROM node:16-alpine
WORKDIR /app
# Install system dependencies
RUN apk add --no-cache python3 make g++ sqlite
# Create directory for SQLite database
RUN mkdir -p /app/data
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Docker Compose
# docker/docker-compose.yml
version: '3.8'
services:
app:
build:
context: ..
dockerfile: docker/Dockerfile
ports:
- "3000:3000"
volumes:
- ..:/app
- /app/node_modules
- ../data:/app/data
environment:
- NODE_ENV=development
command: npm run dev
Express Application Setup
Basic Express Configuration
// src/app.js
const express = require('express');
const morgan = require('morgan');
const { sequelize } = require('./models');
const app = express();
// Middleware
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Basic error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
// Database connection and server start
const PORT = process.env.PORT || 3000;
async function bootServer() {
try {
await sequelize.authenticate();
console.log('Database connection successful');
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
} catch (error) {
console.error('Unable to start server:', error);
process.exit(1);
}
}
bootServer();
Sequelize Configuration
Sequelize RC File
// .sequelizerc
const path = require('path');
module.exports = {
'config': path.resolve('src/config', 'database.js'),
'models-path': path.resolve('src', 'models'),
'seeders-path': path.resolve('src', 'seeders'),
'migrations-path': path.resolve('src', 'migrations')
};
Database Configuration
// src/config/database.js
module.exports = {
development: {
dialect: 'sqlite',
storage: './data/database.sqlite',
logging: console.log,
},
test: {
dialect: 'sqlite',
storage: ':memory:',
logging: false,
},
production: {
dialect: 'sqlite',
storage: './data/database.sqlite',
logging: false,
}
};
Model Setup
// src/models/index.js
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const config = require('../config/database.js');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const db = {};
const sequelize = new Sequelize(config[env]);
fs.readdirSync(__dirname)
.filter(file => {
return (
file.indexOf('.') !== 0 &&
file !== basename &&
file.slice(-3) === '.js'
);
})
.forEach(file => {
const model = require(path.join(__dirname, file))(
sequelize,
Sequelize.DataTypes
);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
Sample Model and Migration
User Model
// src/models/user.js
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
len: [3, 30]
}
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
}
}, {
timestamps: true
});
return User;
};
User Migration
// src/migrations/XXXXXXXXXXXXXX-create-user.js
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
username: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
password: {
type: Sequelize.STRING,
allowNull: false
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Users');
}
};
Running the Application
Package.json Scripts
{
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"migrate": "sequelize-cli db:migrate",
"migrate:undo": "sequelize-cli db:migrate:undo",
"seed": "sequelize-cli db:seed:all"
}
}
Starting the Development Environment
# Build and start containers
docker-compose -f docker/docker-compose.yml up --build
# Run migrations
docker-compose -f docker/docker-compose.yml exec app npm run migrate
# Run seeders (if any)
docker-compose -f docker/docker-compose.yml exec app npm run seed
Testing the Setup
Sample Route
// src/app.js (add to existing file)
const { User } = require('./models');
app.post('/users', async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.get('/users', async (req, res) => {
try {
const users = await User.findAll();
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Testing with curl
# Create a new user
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"username":"testuser","email":"test@example.com","password":"password123"}'
# Get all users
curl http://localhost:3000/users
Common Issues and Solutions
Database Connection Issues
# Check if SQLite file exists
docker-compose -f docker/docker-compose.yml exec app ls -la /app/data
# Check SQLite permissions
docker-compose -f docker/docker-compose.yml exec app chmod 666 /app/data/database.sqlite
# View logs
docker-compose -f docker/docker-compose.yml logs app
Migration Issues
# Reset database
docker-compose -f docker/docker-compose.yml exec app npm run migrate:undo:all
docker-compose -f docker/docker-compose.yml exec app npm run migrate
# Check migration status
docker-compose -f docker/docker-compose.yml exec app npx sequelize-cli db:migrate:status
Best Practices and Tips
Remember these key points when working with this stack:
1. Always use migrations for database changes
# Generate a new migration
npx sequelize-cli migration:generate --name add-role-to-users
# Generate a model with migration
npx sequelize-cli model:generate --name Post --attributes title:string,content:text
2. Keep your Docker containers ephemeral
# Remove all containers and volumes
docker-compose -f docker/docker-compose.yml down -v
# Rebuild from scratch
docker-compose -f docker/docker-compose.yml up --build
3. Use environment variables for configuration
# .env
PORT=3000
NODE_ENV=development
DB_PATH=/app/data/database.sqlite
4. Implement proper error handling
// Example error middleware
app.use((err, req, res, next) => {
console.error(err.stack);
if (err instanceof Sequelize.ValidationError) {
return res.status(400).json({
error: 'Validation Error',
details: err.errors.map(e => ({
field: e.path,
message: e.message
}))
});
}
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'
});
});