Lesson 46-Koa Advanced Applications

Template Engine Integration

Installing a Template Engine

First, you need to install the desired template engine. Here, we use Nunjucks as an example:

npm install nunjucks

For EJS and Pug, use the following commands:

npm install ejs
npm install pug

Configuring the Template Engine

In Koa2, you can use the koa-views middleware to integrate a template engine. koa-views allows you to specify the template engine and provides a render method to render views.

const Koa = require('koa');
const views = require('koa-views');
const path = require('path');

const app = new Koa();

// Configure Nunjucks template engine
app.use(views(path.join(__dirname, 'views'), {
    extension: 'njk',
    map: { njk: 'nunjucks' }
}));

// For EJS
// app.use(views(path.join(__dirname, 'views'), {
//     extension: 'ejs',
//     map: { ejs: 'ejs' }
// }));

// For Pug
// app.use(views(path.join(__dirname, 'views'), {
//     extension: 'pug',
//     map: { pug: 'pug' }
// }));

Rendering Views and Passing Data

Once the template engine is configured, you can use the ctx.render method in middleware or route handlers to render views and pass data to the template.

const router = require('koa-router')();
router.get('/', async (ctx) => {
    const data = {
        title: 'Welcome to My Site',
        message: 'Hello, world!'
    };
    await ctx.render('index.njk', data);
});

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

In the above code, the ctx.render method renders the index.njk file from the views directory and passes the data object to the template.

Template Syntax

Below are basic syntax examples for Nunjucks, EJS, and Pug:

Nunjucks

<!-- views/index.njk -->
<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ message }}</h1>
</body>
</html>

EJS

<!-- views/index.ejs -->
<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= message %></h1>
</body>
</html>

Pug

doctype html
html
    head
        title= title
    body
        h1= message

Error Handling

Middleware for Error Handling

In Koa2, you can use middleware to capture and handle errors. Typically, error-handling middleware should be placed after all other middleware to ensure it captures errors from the entire request-handling chain.

const Koa = require('koa');
const app = new Koa();

// Middleware that may throw an error
app.use(async (ctx, next) => {
    try {
        await next();
    } catch (err) {
        ctx.status = err.status || 500;
        ctx.body = 'An error occurred.';
    }
});

// Another middleware that might throw an error
app.use(async (ctx, next) => {
    throw new Error('Something went wrong.');
});

In this example, the first middleware captures and handles errors thrown by the second middleware.

Custom Error Pages

You can use a template engine to create custom error pages. When an error occurs, you can render these pages and return them to the client.

const views = require('koa-views');
const path = require('path');

app.use(views(path.join(__dirname, 'views'), {
    extension: 'njk',
    map: { njk: 'nunjucks' }
}));

app.use(async (ctx, next) => {
    try {
        await next();
    } catch (err) {
        ctx.status = err.status || 500;
        await ctx.render('error', { message: err.message });
    }
});

JSONP

Native Koa2 JSONP Implementation

Implementing JSONP

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx) => {
    // If the JSONP request is GET
    if (ctx.method === 'GET' && ctx.url.split('?')[0] === '/getData.jsonp') {
        // Get the JSONP callback
        let callbackName = ctx.query.callback || 'callback';
        let returnData = {
            success: true,
            data: {
                text: 'this is a jsonp api',
                time: new Date().getTime(),
            }
        };

        // JSONP script string
        let jsonpStr = `;${callbackName}(${JSON.stringify(returnData)})`;

        // Set response type to support cross-origin requests
        ctx.type = 'text/javascript';

        // Output JSONP string
        ctx.body = jsonpStr;
    } else {
        ctx.body = 'hello jsonp';
    }
});

app.listen(3000, () => {
    console.log('[demo] jsonp is starting at port 3000');
});

Parsing Principle

  • JSONP cross-origin output is executable JavaScript code.
  • The ctx output type should be 'text/javascript'.
  • The ctx output content is a string of executable JavaScript code containing the returned data.
  • A callback function name (callbackName) is required, which the frontend dynamically executes to retrieve the data.

koa-jsonp Middleware

Installation

npm install --save koa-jsonp

Simple Example

const Koa = require('koa');
const jsonp = require('koa-jsonp');
const app = new Koa();

// Use middleware
app.use(jsonp());

app.use(async (ctx) => {
    let returnData = {
        success: true,
        data: {
            text: 'this is a jsonp api',
            time: new Date().getTime(),
        }
    };

    // Directly output JSON
    ctx.body = returnData;
});

app.listen(3000, () => {
    console.log('[demo] jsonp is starting at port 3000');
});

Middleware Chain

Middleware Chain and Execution Order

In Koa2, middleware executes in the order it is added to the application. Each middleware receives a next parameter, and calling this parameter passes control to the next middleware.

app.use(async (ctx, next) => {
    console.log('Middleware 1');
    await next();
});

app.use(async (ctx, next) => {
    console.log('Middleware 2');
    await next();
});

app.use(async (ctx) => {
    console.log('Middleware 3');
});

In this example, the request passes through Middleware 1, Middleware 2, and Middleware 3 in sequence.

Controlling Middleware Execution Flow

You can control the middleware execution flow by calling or not calling next. If next is not called, subsequent middleware will not be executed.

app.use(async (ctx, next) => {
    if (ctx.path === '/') {
        ctx.body = 'Home page';
    } else {
        await next();
    }
});

app.use(async (ctx) => {
    ctx.body = 'Default page';
});

In this example, if the request path is '/', only the first middleware is executed; otherwise, both middleware are executed.

Static File Serving

Installing koa-static

First, install the koa-static middleware using npm:

npm install koa-static

Configuring the Static File Directory

koa-static makes it easy to serve static files from a specific directory. Assume the following directory structure:

project/
|-- server.js
|-- views/
|-- public/
    |-- css/
    |-- js/
    |-- images/

Where the public directory contains all static assets. You can configure koa-static in your Koa2 application as follows:

const Koa = require('koa');
const staticFiles = require('koa-static');
const app = new Koa();

// Configure static file directory
// Path to the static assets directory relative to the entry file index.js
const staticPath = './static';

app.use(staticFiles(path.join(__dirname, staticPath)));

app.listen(3000);

In this configuration, ./public points to the directory containing static files. You can now access static files like http://localhost:3000/css/style.css, http://localhost:3000/js/app.js, or http://localhost:3000/images/logo.png.

Application Example

Code Directory

├── static # Static assets directory
   ├── css/
   ├── image/
   ├── js/
   └── index.html
├── util # Utility code
   ├── content.js # Read request content
   ├── dir.js # Read directory content
   ├── file.js # Read file content
   ├── mimes.js # File type list
   └── walk.js # Traverse directory content
└── index.js # Entry file

Code Analysis

index.js
const Koa = require('koa');
const path = require('path');
const content = require('./util/content');
const mimes = require('./util/mimes');

const app = new Koa();

// Path to the static assets directory relative to the entry file index.js
const staticPath = './static';

// Parse resource type
function parseMime(url) {
    let extName = path.extname(url);
    extName = extName ? extName.slice(1) : 'unknown';
    return mimes[extName];
}

app.use(async (ctx) => {
    // Absolute path to the static assets directory
    let fullStaticPath = path.join(__dirname, staticPath);

    // Get static resource content, which could be file content, directory, or 404
    let _content = await content(ctx, fullStaticPath);

    // Parse the type of requested content
    let _mime = parseMime(ctx.url);

    // Set the context type if a corresponding file type exists
    if (_mime) {
        ctx.type = _mime;
    }

    // Output static resource content
    if (_mime && _mime.indexOf('image/') >= 0) {
        // If it's an image, use Node's native res to output binary data
        ctx.res.writeHead(200);
        ctx.res.write(_content, 'binary');
        ctx.res.end();
    } else {
        // Otherwise, output text
        ctx.body = _content;
    }
});

app.listen(3000);
console.log('[demo] static-server is starting at port 3000');
util/content.js
const path = require('path');
const fs = require('fs');

// Encapsulated method for reading directory content
const dir = require('./dir');

// Encapsulated method for reading file content
const file = require('./file');

/**
 * Get static resource content
 * @param {object} ctx - Koa context
 * @param {string} fullStaticPath - Absolute path to the static assets directory
 * @return {string} - Requested local content
 */
async function content(ctx, fullStaticPath) {
    // Construct the absolute path of the requested resource
    let reqPath = path.join(fullStaticPath, ctx.url);

    // Check if the requested path exists as a directory or file
    let exist = fs.existsSync(reqPath);

    // Default return content is empty
    let content = '';

    if (!exist) {
        // If the requested path does not exist, return 404
        content = '404 Not Found! o(╯□╰)o!';
    } else {
        // Determine if the path is a directory or file
        let stat = fs.statSync(reqPath);

        if (stat.isDirectory()) {
            // If it's a directory, read directory content
            content = dir(ctx.url, reqPath);
        } else {
            // If it's a file, read file content
            content = await file(reqPath);
        }
    }

    return content;
}

module.exports = content;
util/dir.js
const url = require('url');
const fs = require('fs');
const path = require('path');

// Method for traversing directory content
const walk = require('./walk');

/**
 * Encapsulate directory content
 * @param {string} url - URL from the request context, i.e., ctx.url
 * @param {string} reqPath - Full local path of the requested static resource
 * @return {string} - Directory content encapsulated as HTML
 */
function dir(url, reqPath) {
    // Read files and subdirectories in the current directory
    let contentList = walk(reqPath);

    let html = `<ul>`;
    for (let [index, item] of contentList.entries()) {
        html = `${html}<li><a href="${url === '/' ? '' : url}/${item}">${item}</a>`;
    }
    html = `${html}</ul>`;

    return html;
}

module.exports = dir;
util/file.js
const fs = require('fs');

/**
 * Read file method
 * @param {string} filePath - Absolute local path of the file
 * @return {string|binary}
 */
function file(filePath) {
    let content = fs.readFileSync(filePath, 'binary');
    return content;
}

module.exports = file;
util/walk.js
const fs = require('fs');
const mimes = require('./mimes');

/**
 * Traverse and read directory content (subdirectories, filenames)
 * @param {string} reqPath - Absolute path of the requested resource
 * @return {array} - List of directory contents
 */
function walk(reqPath) {
    let files = fs.readdirSync(reqPath);

    let dirList = [], fileList = [];
    for (let i = 0, len = files.length; i < len; i++) {
        let item = files[i];
        let itemArr = item.split('.');
        let itemMime = (itemArr.length > 1) ? itemArr[itemArr.length - 1] : 'undefined';

        if (typeof mimes[itemMime] === 'undefined') {
            dirList.push(files[i]);
        } else {
            fileList.push(files[i]);
        }
    }

    let result = dirList.concat(fileList);

    return result;
}

module.exports = walk;
util/mimes.js
let mimes = {
    'css': 'text/css',
    'less': 'text/css',
    'gif': 'image/gif',
    'html': 'text/html',
    'ico': 'image/x-icon',
    'jpeg': 'image/jpeg',
    'jpg': 'image/jpeg',
    'js': 'text/javascript',
    'json': 'application/json',
    'pdf': 'application/pdf',
    'png': 'image/png',
    'svg': 'image/svg+xml',
    'swf': 'application/x-shockwave-flash',
    'tiff': 'image/tiff',
    'txt': 'text/plain',
    'wav': 'audio/x-wav',
    'wma': 'audio/x-ms-wma',
    'wmv': 'video/x-ms-wmv',
    'xml': 'text/xml'
};

module.exports = mimes;

Serving CSS, JavaScript, Images, and Other Static Assets

Once koa-static is configured, Koa2 automatically handles requests for static files. For example, if you reference CSS or JavaScript files in an HTML file:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <script src="/js/app.js"></script>
</body>
</html>

When a user visits http://localhost:3000, Koa2 automatically locates and serves public/css/style.css and public/js/app.js.

Database Integration

MySQL Database Integration

Install Dependencies

Use mysql2 or sequelize (an ORM) as the MySQL driver.

npm install mysql2
# Or use Sequelize
npm install sequelize

Using mysql

Install the Node.js mysql module:

npm install --save mysql

The mysql module is a Node.js engine for interacting with MySQL databases, enabling operations like table creation, insertion, deletion, updating, and querying.

Creating a Database Session:

const mysql = require('mysql');
const connection = mysql.createConnection({
    host: '127.0.0.1', // Database host
    user: 'root', // Database user
    password: '123456', // Database password
    database: 'my_database' // Selected database
});

// Execute SQL query to read/write to the database
connection.query('SELECT * FROM my_table', (error, results, fields) => {
    if (error) throw error;
    // connected!

    // End the session
    connection.release();
});

Note: Each event has a start-to-end process, and database sessions must be closed after execution to avoid occupying connection resources.

Creating a Database Connection Pool:

Database operations are often complex and involve multiple sessions. Instead of configuring connection parameters for each session, a connection pool can manage sessions efficiently.

const mysql = require('mysql');

// Create a connection pool
const pool = mysql.createPool({
    host: '127.0.0.1', // Database host
    user: 'root', // Database user
    password: '123456', // Database password
    database: 'my_database' // Selected database
});

// Perform session operations in the connection pool
pool.getConnection(function(err, connection) {
    connection.query('SELECT * FROM my_table', (error, results, fields) => {
        // End the session
        connection.release();

        // Throw error if any
        if (error) throw error;
    });
});

Using mysql2

const mysql = require('mysql2/promise');
const pool = mysql.createPool({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'testdb'
});

app.use(async (ctx) => {
    const [rows] = await pool.query('SELECT * FROM users');
    ctx.body = rows;
});

Using Sequelize

const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize('database', 'username', 'password', {
    host: 'localhost',
    dialect: /* one of 'mysql' | 'mariadb' | 'postgres' | 'mssql' */
});

const User = sequelize.define('user', {
    firstName: DataTypes.STRING,
    lastName: DataTypes.STRING
}, {
    // Other model options go here
});

(async () => {
    await sequelize.authenticate();
    console.log('Connection has been established successfully.');
})();

MongoDB Database Integration

Install Dependencies

Use mongoose as the MongoDB ORM.

npm install mongoose

Using Mongoose

const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test', { useNewUrlParser: true, useUnifiedTopology: true });

const userSchema = new mongoose.Schema({
    name: String,
    age: Number
});

const User = mongoose.model('User', userSchema);

app.use(async (ctx) => {
    const users = await User.find({});
    ctx.body = users;
});

PostgreSQL Database Integration

Install Dependencies

Use pg or sequelize (PostgreSQL ORM).

npm install pg
# Or use Sequelize
npm install sequelize

Using pg

const { Pool } = require('pg');
const pool = new Pool({
    user: 'dbuser',
    host: 'database.server.com',
    database: 'mydatabase',
    password: 'secretpassword',
    port: 5432,
});

app.use(async (ctx) => {
    const res = await pool.query('SELECT * FROM users');
    ctx.body = res.rows;
});

Sequelize Integration

Sequelize is a Promise-based Node.js ORM that supports multiple databases, including MySQL, PostgreSQL, SQLite, and Microsoft SQL Server.

Install Sequelize

npm install --save sequelize
npm install --save mysql2 # Or install the appropriate driver for your database

Configure Sequelize

const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('database', 'username', 'password', {
    host: 'localhost',
    dialect: /* one of 'mysql' | 'mariadb' | 'postgres' | 'mssql' */
});

Define Models

const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize(/* your database config */);

const User = sequelize.define('User', {
    username: {
        type: DataTypes.STRING,
        allowNull: false
    },
    email: {
        type: DataTypes.STRING,
        unique: true
    }
}, {
    timestamps: true,
    underscored: true
});

Perform Database Operations

app.use(async (ctx) => {
    const user = await User.create({
        username: 'john_doe',
        email: 'john@example.com'
    });

    const foundUser = await User.findOne({
        where: {
            email: 'john@example.com'
        }
    });

    ctx.body = foundUser;
});

TypeORM Integration

TypeORM is an ORM that supports TypeScript and JavaScript and works with most mainstream relational databases.

Install TypeORM

npm install typeorm reflect-metadata mysql2 # Or install the appropriate driver for your database

Configure TypeORM

import { createConnection } from 'typeorm';

createConnection({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'user',
    password: 'password',
    database: 'mydb',
    entities: [
        'dist/entities/*.js'
    ],
    synchronize: true,
});

Define Entities

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    username: string;

    @Column()
    email: string;
}

Perform Database Operations

import { getRepository } from 'typeorm';

app.use(async (ctx) => {
    const userRepository = getRepository(User);
    const user = userRepository.create({
        username: 'john_doe',
        email: 'john@example.com'
    });

    await userRepository.save(user);

    const foundUser = await userRepository.findOne({
        where: { email: 'john@example.com' }
    });

    ctx.body = foundUser;
});

Prisma Integration

Install Prisma CLI and Prisma Client

First, install the Prisma CLI globally:

npm install -g prisma

Then, install the Prisma Client in your project:

npx prisma generate

This generates the Prisma Client, the primary way to interact with the database.

Configure Prisma Schema

Create a prisma/schema.prisma file in your project root to define your database models:

datasource db {
    provider = 'postgresql'
    url      = env('DATABASE_URL')
}

generator client {
    provider = 'prisma-client-js'
}

model User {
    id        Int      @id @default(autoincrement())
    email     String   @unique
    createdAt DateTime @default(now())
    posts     Post[]
}

model Post {
    id        Int      @id @default(autoincrement())
    title     String
    content   String?
    published Boolean  @default(false)
    author    User     @relation(fields: [authorId], references: [id])
    authorId  Int
}

Generate Prisma Client

Run the following command to generate the Prisma Client:

npx prisma generate

This generates the Prisma Client in the node_modules/.prisma/client directory.

Using Prisma in Koa2

In your Koa2 application, import the Prisma Client and use it for database operations:

const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

const app = new Koa();

app.use(async (ctx) => {
    const user = await prisma.user.findUnique({
        where: Juvenile@id: john.doe@example.com' }
    });

    ctx.body = user;
});

Database Migrations

Prisma provides a database migration tool to safely modify the database schema:

npx prisma migrate dev --name init

This creates a new migration file and applies it to your database.

Exception Handling and Connection Cleanup

In production, properly handle Prisma exceptions and gracefully close the Prisma Client connection when the application shuts down:

process.on('SIGTERM', async () => {
    await prisma.$disconnect();
    process.exit(0);
});

File Upload

Creating the Basic Application Structure

  • app.js: Main application file.
  • routes/upload.js: Handles file uploads.
// app.js
const Koa = require('koa');
const router = require('koa-router')();
const bodyParser = require('koa-bodyparser');
const uploadRouter = require('./routes/upload');

const app = new Koa();

// Use middleware
app.use(bodyParser());
app.use(uploadRouter.routes()).use(uploadRouter.allowedMethods());

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

File Upload Route

Use multer to handle file uploads. Set the storage path and file type restrictions.

// routes/upload.js
const Router = require('koa-router');
const multer = require('multer');
const path = require('path');

const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, 'uploads/');
    },
    filename: function (req, file, cb) {
        cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname));
    }
});

const upload = multer({ storage: storage });

const router = new Router();

router.post('/upload', upload.single('file'), async (ctx) => {
    ctx.body = {
        success: true,
        message: 'File uploaded successfully',
        fileName: ctx.req.file.filename
    };
});

module.exports = router;

Testing File Upload Functionality

Use Postman or curl to send a POST request. Configure the request URL and file field name.

curl -X POST -F "file=@/path/to/file.jpg" http://localhost:3000/upload

Error Handling and Validation

Error Handling:

  • File size restrictions.
  • File type restrictions.

Validation:

  • Ensure files are saved correctly.
  • Return appropriate response information.
// routes/upload.js
const upload = multer({
    storage: storage,
    limits: { fileSize: 1024 * 1024 * 5 }, // 5MB
    fileFilter: (req, file, cb) => {
        if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
            return cb(new Error('Only image files are allowed!'));
        }
        cb(null, true);
    }
});

router.post('/upload', upload.single('file'), async (ctx) => {
    try {
        ctx.body = {
            success: true,
            message: 'File uploaded successfully',
            fileName: ctx.req.file.filename
        };
    } catch (err) {
        ctx.status = 400;
        ctx.body = { error: err.message };
    }
});

Cookies and Sessions

Installing Required Packages

To use Cookies and Sessions, install the koa-session package, the official session middleware for Koa.

npm install koa-session --save

Configuring Sessions

To configure sessions in Koa2, create a session configuration object and pass it to the koa-session middleware.

// app.js
const Koa = require('koa');
const session = require('koa-session');

const app = new Koa();

// Configure Session
const CONFIG = {
    key: 'koa:sess', // Cookie key (default is koa:sess)
    maxAge: 86400000, // Cookie expiration time in ms (default is 1 day)
    autoCommit: true, // Auto-commit headers (default: true)
    overwrite: true, // Allow overwriting (default: true)
    httpOnly: true, // Cookie accessible only by the server (default: true)
    signed: false, // Signature default (default: true)
    rolling: false, // Force-set cookie on every request to reset expiration (default: false)
    renew: false, // Renew session when about to expire (default: false)
};

app.keys = ['some secret hurr']; // Key for signing
app.use(session(CONFIG, app));

Using Sessions

Once sessions are configured, you can use them in middleware or route handlers to store and retrieve data.

// app.js
app.use(async (ctx) => {
    if (ctx.request.method === 'POST') {
        // Set session
        ctx.session.username = ctx.request.body.username;
        ctx.body = { message: 'Logged in!' };
    } else {
        // Get session
        const username = ctx.session.username;
        ctx.body = { username };
    }
});

Using Cookies

Cookies can be accessed directly via ctx.cookies. Use ctx.cookies.get(name[, options]) to retrieve a cookie and ctx.cookies.set(name, value[, options]) to set a cookie.

// Set Cookie
ctx.cookies.set('username', 'john_doe', { maxAge: 600000 });
ctx.cookies.set(
    'cid',
    'hello world',
    {
        domain: 'localhost', // Domain for the cookie
        path: '/index', // Path for the cookie
        maxAge: 10 * 60 * 1000, // Cookie duration
        expires: new Date('2017-02-15'), // Cookie expiration date
        httpOnly: false, // Accessible only via HTTP
        overwrite: false // Allow overwriting
    }
);
ctx.body = 'cookie is ok';

// Get Cookie
const username = ctx.cookies.get('username');

Example: Login Authentication

Below is a simple login example using sessions to track a user’s login status.

// app.js
const Koa = require('koa');
const session = require('koa-session');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

// Configure Session
const CONFIG = {
    key: 'koa:sess',
    maxAge: 86400000,
    autoCommit: true,
    overwrite: true,
    httpOnly: true,
    signed: false,
    rolling: false,
    renew: false
};

app.keys = ['some secret hurr'];
app.use(session(CONFIG, app));

// Login route
router.post('/login', async (ctx) => {
    const { username, password } = ctx.request.body;

    // Simple user validation logic
    if (username === 'admin' && password === '123456') {
        ctx.session.user = { id: 1, username };
        ctx.body = { message: 'Login successful' };
    } else {
        ctx.status = 401;
        ctx.body = { message: 'Invalid credentials' };
    }
});

// Protected route
router.get('/protected', async (ctx) => {
    const user = ctx.session.user;
    if (user) {
        ctx.body = { message: `Welcome, ${user.username}!` };
    } else {
        ctx.status = 401;
        ctx.body = { message: 'You need to login first' };
    }
});

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

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

Example 2: MySQL Session Storage

const Koa = require('koa');
const session = require('koa-session-minimal');
const MysqlSession = require('koa-mysql-session');

const app = new Koa();

// Configure MySQL for session storage
let store = new MysqlSession({
    user: 'root',
    password: 'abc123',
    database: 'koa_demo',
    host: '127.0.0.1',
});

// Cookie configuration for storing session ID
let cookie = {
    maxAge: '', // Cookie duration
    expires: '', // Cookie expiration date
    path: '', // Path for the cookie
    domain: '', // Domain for the cookie
    httpOnly: '', // Accessible only via HTTP
    overwrite: '', // Allow overwriting
    secure: '',
    sameSite: '',
    signed: '',
};

// Use session middleware
app.use(session({
    key: 'SESSION_ID',
    store: store,
    cookie: cookie
}));

app.use(async (ctx) => {
    // Set session
    if (ctx.url === '/set') {
        ctx.session = {
            user_id: Math.random().toString(36).substr(2),
            count: 0
        };
        ctx.body = ctx.session;
    } else if (ctx.url === '/') {
        // Read session information
        ctx.session.count = ctx.session.count + 1;
        ctx.body = ctx.session;
    }
});

app.listen(3000, () => {
    console.log('[demo] session is starting at port 3000');
});

Custom Middleware

Installing Koa

Ensure Koa2 is installed:

npm install koa --save

Creating a Basic Application

Create a basic Koa2 application:

// app.js
const Koa = require('koa');
const app = new Koa();

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

Writing Middleware

Middleware is a function that takes the context object ctx and a next function as parameters. Middleware can perform any operation and call next() to pass control to the next middleware.

Example: Logging Middleware

Below is a simple logging middleware that records the timestamp and method of requests.

function loggerMiddleware() {
    return async (ctx, next) => {
        const start = Date.now();
        await next();
        const end = Date.now();
        console.log(`${ctx.method} ${ctx.url} - ${end - start}ms`);
    };
}

app.use(loggerMiddleware());

Example: Error Handling Middleware

An error-handling middleware to catch unhandled errors and return appropriate HTTP status codes and error messages.

function errorHandlerMiddleware() {
    return async (ctx, next) => {
        try {
            await next();
        } catch (err) {
            console.error(err);
            ctx.status = err.status || 500;
            ctx.body = {
                message: err.message,
                error: err
            };
        }
    };
}

app.use(errorHandlerMiddleware());

Example: Authentication Middleware

A simple authentication middleware to check if a user has permission to access a specific resource.

function authMiddleware() {
    return async (ctx, next) => {
        const requiredRole = ctx.params.role || 'user';
        const userRole = ctx.state.user?.role || 'guest';

        if (userRole !== requiredRole) {
            ctx.status = 403;
            ctx.body = {
                message: 'Forbidden'
            };
            return;
        }

        await next();
    };
}

app.use(authMiddleware());

Example: JSON API Response Middleware

A middleware to automatically convert JSON objects into HTTP response bodies.

function jsonApiResponseMiddleware() {
    return async (ctx, next) => {
        await next();

        if (ctx.body && typeof ctx.body === 'object' && !Buffer.isBuffer(ctx.body)) {
            ctx.body = JSON.stringify(ctx.body);
            ctx.type = 'application/json';
        }
    };
}

app.use(jsonApiResponseMiddleware());

Using Middleware

Incorporate these middleware into the application:

// app.js
const Koa = require('koa');
const app = new Koa();

// Logging middleware
app.use(loggerMiddleware());

// Error handling middleware
app.use(errorHandlerMiddleware());

// JSON API response middleware
app.use(jsonApiResponseMiddleware());

// Authentication middleware
app.use(authMiddleware());

// Route handling
app.use(async (ctx) => {
    ctx.body = {
        message: 'Hello, World!'
    };
});

app.listen(3000, () => {
    console.log('Server is running on http://localhost:3000');
});
Share your love