Lesson 50-Koa Project Architecture

Koa Project Architecture

Directory Structure Design

A typical Koa2 project directory structure is as follows:

project/
├── app.js
├── config/
│   └── index.js
├── controllers/
│   ├── index.js
│   └── userController.js
├── middleware/
│   ├── errorMiddleware.js
│   └── loggerMiddleware.js
├── models/
│   ├── index.js
│   └── userModel.js
├── routes/
│   ├── index.js
│   └── userRoutes.js
├── services/
│   ├── index.js
│   └── userService.js
└── utils/
    └── responseUtil.js
  • app.js: Main entry file, initializes the Koa application.
  • config/: Configuration files, such as database settings and environment variables.
  • controllers/: Controller layer, handles business logic and route responses.
  • middleware/: Middleware, processes logic before and after requests, such as error handling and logging.
  • models/: Data models, interacts with the database.
  • routes/: Route definitions.
  • services/: Service layer, encapsulates business logic.
  • utils/: Utility functions, such as response formatting.

Initializing the Koa Application

Initialize the Koa application in the app.js file:

const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const cors = require('@koa/cors');
const router = require('./routes');

const app = new Koa();

// Use middleware
app.use(bodyParser());
app.use(cors());

// Register routes
app.use(router.routes()).use(router.allowedMethods());

// Start the application
app.listen(3000, () => {
    console.log('Server is running on http://localhost:3000');
});

Configuration Management

Manage configuration information in the config/index.js file:

module.exports = {
    database: {
        host: 'localhost',
        user: 'root',
        password: 'password',
        database: 'testdb'
    },
    port: 3000
};

Database Integration

Use Sequelize as the ORM in models/userModel.js:

const { Model, DataTypes } = require('sequelize');
const sequelize = require('../config/db');

class User extends Model {}

User.init({
    name: DataTypes.STRING,
    email: DataTypes.STRING,
}, { sequelize, modelName: 'user' });

module.exports = User;

Routes and Controllers

Define routes and controllers in routes/userRoutes.js and controllers/userController.js:

// userRoutes.js
const Router = require('koa-router');
const UserController = require('../controllers/userController');

const router = new Router();

router.get('/', UserController.getAllUsers);
router.post('/', UserController.createUser);

module.exports = router;

// userController.js
const UserService = require('../services/userService');

const getAllUsers = async (ctx) => {
    const users = await UserService.getAllUsers();
    ctx.body = users;
};

const createUser = async (ctx) => {
    const newUser = await UserService.createUser(ctx.request.body);
    ctx.body = newUser;
};

module.exports = {
    getAllUsers,
    createUser
};

Service Layer

Encapsulate business logic in services/userService.js:

const User = require('../models/userModel');

const getAllUsers = async () => {
    return await User.findAll();
};

const createUser = async (data) => {
    return await User.create(data);
};

module.exports = {
    getAllUsers,
    createUser
};

Middleware

Implement error handling and logging in middleware/errorMiddleware.js and middleware/loggerMiddleware.js:

// errorMiddleware.js
module.exports = async (ctx, next) => {
    try {
        await next();
    } catch (err) {
        ctx.status = err.status || 500;
        ctx.body = {
            message: err.message
        };
    }
};

// loggerMiddleware.js
module.exports = async (ctx, next) => {
    console.log(`${ctx.method} ${ctx.url}`);
    await next();
};

Utility Functions

Define response formatting functions in utils/responseUtil.js:

module.exports = {
    successResponse: (data) => ({
        status: 'success',
        data
    }),
    errorResponse: (message) => ({
        status: 'error',
        message
    })
};

Environment Variable Management

Use environment variables to manage sensitive information and configurations, avoiding hardcoding to enhance security and flexibility. Libraries like dotenv or cross-env can be used to load and set environment variables.

# .env file
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=password
DB_NAME=testdb
PORT=3000

Read environment variables in config/index.js:

require('dotenv').config();

module.exports = {
    database: {
        host: process.env.DB_HOST,
        user: process.env.DB_USER,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_NAME
    },
    port: process.env.PORT
};

Unit Testing

Writing unit tests ensures code quality and stability. Testing frameworks like Mocha and assertion libraries like Chai can be used for testing.

// test/userService.test.js
const chai = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
const expect = chai.expect;
chai.use(sinonChai);

const UserService = require('../../services/userService');
const User = require('../../models/userModel');

describe('UserService', () => {
    let findStub;
    let createStub;

    beforeEach(() => {
        findStub = sinon.stub(User, 'findAll');
        createStub = sinon.stub(User, 'create');
    });

    afterEach(() => {
        findStub.restore();
        createStub.restore();
    });

    describe('getAllUsers', () => {
        it('should return all users', async () => {
            findStub.resolves([{ name: 'John Doe' }]);
            const users = await UserService.getAllUsers();
            expect(users).to.be.an('array');
            expect(users[0].name).to.equal('John Doe');
        });
    });

    describe('createUser', () => {
        it('should create a new user', async () => {
            createStub.resolves({ name: 'Jane Doe' });
            const newUser = await UserService.createUser({ name: 'Jane Doe' });
            expect(newUser.name).to.equal('Jane Doe');
        });
    });
});
Share your love