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'
    });
});