Database and Servers
For more advanced bots, connecting to databases or making your bot's statistics public using a server are commonly seen.
Connecting to Databases
Since there are tons of different databases and ways to connect to them, this guide won't be able to cover all the different methods of connecting.
As long as the driver supports connecting to the database separately, this guide should be close enough.
For simplicity's sake, we'll try implementing a database connection with Sequelize following Discord.JS's guide.
To not make this section overly long, we'll only implement 2 of 6 of the commands from the guide.
import { defineOnLoad } from 'chooksie'
import type { CreationOptional } from 'sequelize'
import { DataTypes, Model, Sequelize } from 'sequelize'
// Reference: https://discordjs.guide/sequelize/#alpha-connection-information
const sequelize = new Sequelize('database', 'user', 'password', {
host: 'localhost',
dialect: 'sqlite',
logging: false,
// SQLite only
storage: 'database.sqlite',
})
// We can expose the Tags model for use in our commands.
// Reference: https://sequelize.org/master/manual/typescript.html
export class Tag extends Model {
declare public name: string
declare public description: string
declare public username: string
declare public usageCount: CreationOptional<number>
}
// Reference: https://discordjs.guide/sequelize/#beta-creating-the-model
Tag.init({
name: {
type: DataTypes.STRING,
unique: true,
},
description: DataTypes.TEXT,
username: DataTypes.STRING,
usageCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
allowNull: false,
},
}, { sequelize, modelName: 'tags' })
export const chooksOnLoad = defineOnLoad(async () => {
// Sync changes every time we make changes to the file.
await sequelize.sync({
// Optionally, we can choose to always clear the database during development.
force: process.env.NODE_ENV !== 'production',
})
})
import { defineOnLoad } from 'chooksie'
import { DataTypes, Model, Sequelize } from 'sequelize'
// Reference: https://discordjs.guide/sequelize/#alpha-connection-information
const sequelize = new Sequelize('database', 'user', 'password', {
host: 'localhost',
dialect: 'sqlite',
logging: false,
// SQLite only
storage: 'database.sqlite',
})
// We can expose the Tags model for use in our commands.
export class Tag extends Model {}
// Reference: https://discordjs.guide/sequelize/#beta-creating-the-model
Tag.init({
name: {
type: DataTypes.STRING,
unique: true,
},
description: DataTypes.TEXT,
username: DataTypes.STRING,
usageCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
allowNull: false,
},
}, { sequelize, modelName: 'tags' })
export const chooksOnLoad = defineOnLoad(async () => {
// Sync changes every time we make changes to the file.
await sequelize.sync({
// Optionally, we can choose to always clear the database during development.
force: process.env.NODE_ENV !== 'production',
})
})
const { defineOnLoad } = require('chooksie')
const { DataTypes, Model, Sequelize } = require('sequelize')
// Reference: https://discordjs.guide/sequelize/#alpha-connection-information
const sequelize = new Sequelize('database', 'user', 'password', {
host: 'localhost',
dialect: 'sqlite',
logging: false,
// SQLite only
storage: 'database.sqlite',
})
// We can expose the Tags model for use in our commands.
class Tag extends Model {}
exports.Tag = Tag
// Reference: https://discordjs.guide/sequelize/#beta-creating-the-model
Tag.init({
name: {
type: DataTypes.STRING,
unique: true,
},
description: DataTypes.TEXT,
username: DataTypes.STRING,
usageCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
allowNull: false,
},
}, { sequelize, modelName: 'tags' })
exports.chooksOnLoad = defineOnLoad(async () => {
// Sync changes every time we make changes to the file.
await sequelize.sync({
// Optionally, we can choose to always clear the database during development.
force: process.env.NODE_ENV !== 'production',
})
})
Next, we can define a function that exposes our Tag
model that we can pass in our setup
methods:
async function db() {
// Only expose the Tag model from our database
const { Tag } = await import('../db')
return Tag
}
async function db() {
// Only expose the Tag model from our database
const { Tag } = await import('../db')
return Tag
}
function db() {
// Only expose the Tag model from our database
const { Tag } = require('../db')
return Tag
}
Using the function we created above, we can define our first subcommand that creates tags for us:
// Reference: https://discordjs.guide/sequelize/#delta-adding-a-tag
const addTag = defineSubcommand({
name: 'add',
description: 'Add a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
const tagDescription = ctx.interaction.options.getString('description', true)
try {
// equivalent to: INSERT INTO tags (name, description, username) values (?, ?, ?);
const tag = await this.create({
name: tagName,
description: tagDescription,
username: ctx.interaction.user.username,
})
await ctx.interaction.reply(`Tag ${tag.name} added.`)
} catch (error) {
if ((error as Error).name === 'SequelizeUniqueConstraintError') {
await ctx.interaction.reply('That tag already exists.')
return
}
await ctx.interaction.reply('Something went wrong with adding a tag.')
}
},
options: [
{
name: 'name',
description: 'Name of the tag.',
type: 'STRING',
required: true,
},
{
name: 'description',
description: 'Description of the tag.',
type: 'STRING',
required: true,
},
],
})
// Reference: https://discordjs.guide/sequelize/#delta-adding-a-tag
const addTag = defineSubcommand({
name: 'add',
description: 'Add a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
const tagDescription = ctx.interaction.options.getString('description', true)
try {
// equivalent to: INSERT INTO tags (name, description, username) values (?, ?, ?);
const tag = await this.create({
name: tagName,
description: tagDescription,
username: ctx.interaction.user.username,
})
await ctx.interaction.reply(`Tag ${tag.name} added.`)
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
await ctx.interaction.reply('That tag already exists.')
return
}
await ctx.interaction.reply('Something went wrong with adding a tag.')
}
},
options: [
{
name: 'name',
description: 'Name of the tag.',
type: 'STRING',
required: true,
},
{
name: 'description',
description: 'Description of the tag.',
type: 'STRING',
required: true,
},
],
})
// Reference: https://discordjs.guide/sequelize/#delta-adding-a-tag
const addTag = defineSubcommand({
name: 'add',
description: 'Add a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
const tagDescription = ctx.interaction.options.getString('description', true)
try {
// equivalent to: INSERT INTO tags (name, description, username) values (?, ?, ?);
const tag = await this.create({
name: tagName,
description: tagDescription,
username: ctx.interaction.user.username,
})
await ctx.interaction.reply(`Tag ${tag.name} added.`)
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
await ctx.interaction.reply('That tag already exists.')
return
}
await ctx.interaction.reply('Something went wrong with adding a tag.')
}
},
options: [
{
name: 'name',
description: 'Name of the tag.',
type: 'STRING',
required: true,
},
{
name: 'description',
description: 'Description of the tag.',
type: 'STRING',
required: true,
},
],
})
Then, we create the second command that fetches our tags, just so we can see in our Discord client that saving and searching for tags work:
// Reference: https://discordjs.guide/sequelize/#epsilon-fetching-a-tag
const fetchTag = defineSubcommand({
name: 'fetch',
description: 'Fetch a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await this.findOne({ where: { name: tagName } })
if (tag) {
// equivalent to: UPDATE tags SET usage_count = usage_count + 1 WHERE name = 'tagName';
await tag.increment('usageCount')
await ctx.interaction.reply(tag.get('description'))
return
}
await ctx.interaction.reply(`Could not find tag: ${tagName}`)
},
options: [
defineOption({
name: 'name',
description: 'The name of the tag to fetch.',
type: 'STRING',
required: true,
setup: db,
async autocomplete(ctx) {
const tagName = ctx.interaction.options.getFocused()
// equivalent to: SELECT * FROM tags WHERE name LIKE '%tagName%'
const tags = await this.findAll({ where: { name: { [Op.like]: `%${tagName}%` } } })
// Map similar tags we found into something Discord can use
const tagList = tags.map(tag => ({
name: `${tag.name} - ${tag.description}`,
value: tag.name,
}))
await ctx.interaction.respond(tagList)
},
}),
],
})
// Reference: https://discordjs.guide/sequelize/#epsilon-fetching-a-tag
const fetchTag = defineSubcommand({
name: 'fetch',
description: 'Fetch a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await this.findOne({ where: { name: tagName } })
if (tag) {
// equivalent to: UPDATE tags SET usage_count = usage_count + 1 WHERE name = 'tagName';
await tag.increment('usageCount')
await ctx.interaction.reply(tag.get('description'))
return
}
await ctx.interaction.reply(`Could not find tag: ${tagName}`)
},
options: [
defineOption({
name: 'name',
description: 'The name of the tag.',
type: 'STRING',
required: true,
setup: db,
async autocomplete(ctx) {
const tagName = ctx.interaction.options.getFocused()
// equivalent to: SELECT * FROM tags WHERE name LIKE '%tagName%'
const tags = await this.findAll({ where: { name: { [Op.like]: `%${tagName}%` } } })
// Map similar tags we found into something Discord can use
const tagList = tags.map(tag => ({
name: `${tag.name} - ${tag.description}`,
value: tag.name,
}))
await ctx.interaction.respond(tagList)
},
}),
],
})
// Reference: https://discordjs.guide/sequelize/#epsilon-fetching-a-tag
const fetchTag = defineSubcommand({
name: 'fetch',
description: 'Fetch a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await this.findOne({ where: { name: tagName } })
if (tag) {
// equivalent to: UPDATE tags SET usage_count = usage_count + 1 WHERE name = 'tagName';
await tag.increment('usageCount')
await ctx.interaction.reply(tag.get('description'))
return
}
await ctx.interaction.reply(`Could not find tag: ${tagName}`)
},
options: [
defineOption({
name: 'name',
description: 'The name of the tag.',
type: 'STRING',
required: true,
setup: db,
async autocomplete(ctx) {
const tagName = ctx.interaction.options.getFocused()
// equivalent to: SELECT * FROM tags WHERE name LIKE '%tagName%'
const tags = await this.findAll({ where: { name: { [Op.like]: `%${tagName}%` } } })
// Map similar tags we found into something Discord can use
const tagList = tags.map(tag => ({
name: `${tag.name} - ${tag.description}`,
value: tag.name,
}))
await ctx.interaction.respond(tagList)
},
}),
],
})
Once we connect all these up, we should have the following command file:
import { defineOption, defineSlashSubcommand, defineSubcommand } from 'chooksie'
import { Op } from 'sequelize'
async function db() {
// Only expose the Tag model from our database
const { Tag } = await import('../db')
return Tag
}
// Reference: https://discordjs.guide/sequelize/#delta-adding-a-tag
const addTag = defineSubcommand({
name: 'add',
description: 'Add a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
const tagDescription = ctx.interaction.options.getString('description', true)
try {
// equivalent to: INSERT INTO tags (name, description, username) values (?, ?, ?);
const tag = await this.create({
name: tagName,
description: tagDescription,
username: ctx.interaction.user.username,
})
await ctx.interaction.reply(`Tag ${tag.name} added.`)
} catch (error) {
if ((error as Error).name === 'SequelizeUniqueConstraintError') {
await ctx.interaction.reply('That tag already exists.')
return
}
await ctx.interaction.reply('Something went wrong with adding a tag.')
}
},
options: [
{
name: 'name',
description: 'Name of the tag.',
type: 'STRING',
required: true,
},
{
name: 'description',
description: 'Description of the tag.',
type: 'STRING',
required: true,
},
],
})
// Reference: https://discordjs.guide/sequelize/#epsilon-fetching-a-tag
const fetchTag = defineSubcommand({
name: 'fetch',
description: 'Fetch a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await this.findOne({ where: { name: tagName } })
if (tag) {
// equivalent to: UPDATE tags SET usage_count = usage_count + 1 WHERE name = 'tagName';
await tag.increment('usageCount')
await ctx.interaction.reply(tag.get('description'))
return
}
await ctx.interaction.reply(`Could not find tag: ${tagName}`)
},
options: [
defineOption({
name: 'name',
description: 'The name of the tag to fetch.',
type: 'STRING',
required: true,
setup: db,
async autocomplete(ctx) {
const tagName = ctx.interaction.options.getFocused()
// equivalent to: SELECT * FROM tags WHERE name LIKE '%tagName%'
const tags = await this.findAll({ where: { name: { [Op.like]: `%${tagName}%` } } })
// Map similar tags we found into something Discord can use
const tagList = tags.map(tag => ({
name: `${tag.name} - ${tag.description}`,
value: tag.name,
}))
await ctx.interaction.respond(tagList)
},
}),
],
})
export default defineSlashSubcommand({
name: 'tag',
description: 'Manage tags.',
options: [
addTag,
fetchTag,
],
})
import { defineOption, defineSlashSubcommand, defineSubcommand } from 'chooksie'
import { Op } from 'sequelize'
async function db() {
// Only expose the Tag model from our database
const { Tag } = await import('../db')
return Tag
}
// Reference: https://discordjs.guide/sequelize/#delta-adding-a-tag
const addTag = defineSubcommand({
name: 'add',
description: 'Add a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
const tagDescription = ctx.interaction.options.getString('description', true)
try {
// equivalent to: INSERT INTO tags (name, description, username) values (?, ?, ?);
const tag = await this.create({
name: tagName,
description: tagDescription,
username: ctx.interaction.user.username,
})
await ctx.interaction.reply(`Tag ${tag.name} added.`)
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
await ctx.interaction.reply('That tag already exists.')
return
}
await ctx.interaction.reply('Something went wrong with adding a tag.')
}
},
options: [
{
name: 'name',
description: 'Name of the tag.',
type: 'STRING',
required: true,
},
{
name: 'description',
description: 'Description of the tag.',
type: 'STRING',
required: true,
},
],
})
// Reference: https://discordjs.guide/sequelize/#epsilon-fetching-a-tag
const fetchTag = defineSubcommand({
name: 'fetch',
description: 'Fetch a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await this.findOne({ where: { name: tagName } })
if (tag) {
// equivalent to: UPDATE tags SET usage_count = usage_count + 1 WHERE name = 'tagName';
await tag.increment('usageCount')
await ctx.interaction.reply(tag.get('description'))
return
}
await ctx.interaction.reply(`Could not find tag: ${tagName}`)
},
options: [
defineOption({
name: 'name',
description: 'The name of the tag.',
type: 'STRING',
required: true,
setup: db,
async autocomplete(ctx) {
const tagName = ctx.interaction.options.getFocused()
// equivalent to: SELECT * FROM tags WHERE name LIKE '%tagName%'
const tags = await this.findAll({ where: { name: { [Op.like]: `%${tagName}%` } } })
// Map similar tags we found into something Discord can use
const tagList = tags.map(tag => ({
name: `${tag.name} - ${tag.description}`,
value: tag.name,
}))
await ctx.interaction.respond(tagList)
},
}),
],
})
export default defineSlashSubcommand({
name: 'tag',
description: 'Manage tags.',
options: [
addTag,
fetchTag,
],
})
const { defineOption, defineSlashSubcommand, defineSubcommand } = require('chooksie')
const { Op } = require('sequelize')
function db() {
// Only expose the Tag model from our database
const { Tag } = require('../db')
return Tag
}
// Reference: https://discordjs.guide/sequelize/#delta-adding-a-tag
const addTag = defineSubcommand({
name: 'add',
description: 'Add a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
const tagDescription = ctx.interaction.options.getString('description', true)
try {
// equivalent to: INSERT INTO tags (name, description, username) values (?, ?, ?);
const tag = await this.create({
name: tagName,
description: tagDescription,
username: ctx.interaction.user.username,
})
await ctx.interaction.reply(`Tag ${tag.name} added.`)
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
await ctx.interaction.reply('That tag already exists.')
return
}
await ctx.interaction.reply('Something went wrong with adding a tag.')
}
},
options: [
{
name: 'name',
description: 'Name of the tag.',
type: 'STRING',
required: true,
},
{
name: 'description',
description: 'Description of the tag.',
type: 'STRING',
required: true,
},
],
})
// Reference: https://discordjs.guide/sequelize/#epsilon-fetching-a-tag
const fetchTag = defineSubcommand({
name: 'fetch',
description: 'Fetch a tag.',
type: 'SUB_COMMAND',
setup: db,
async execute(ctx) {
const tagName = ctx.interaction.options.getString('name', true)
// equivalent to: SELECT * FROM tags WHERE name = 'tagName' LIMIT 1;
const tag = await this.findOne({ where: { name: tagName } })
if (tag) {
// equivalent to: UPDATE tags SET usage_count = usage_count + 1 WHERE name = 'tagName';
await tag.increment('usageCount')
await ctx.interaction.reply(tag.get('description'))
return
}
await ctx.interaction.reply(`Could not find tag: ${tagName}`)
},
options: [
defineOption({
name: 'name',
description: 'The name of the tag.',
type: 'STRING',
required: true,
setup: db,
async autocomplete(ctx) {
const tagName = ctx.interaction.options.getFocused()
// equivalent to: SELECT * FROM tags WHERE name LIKE '%tagName%'
const tags = await this.findAll({ where: { name: { [Op.like]: `%${tagName}%` } } })
// Map similar tags we found into something Discord can use
const tagList = tags.map(tag => ({
name: `${tag.name} - ${tag.description}`,
value: tag.name,
}))
await ctx.interaction.respond(tagList)
},
}),
],
})
module.exports = defineSlashSubcommand({
name: 'tag',
description: 'Manage tags.',
options: [
addTag,
fetchTag,
],
})
If all things went right, you should now have two new commands available in the Discord client, and searching for tags should also give you autocomplete suggestions on tags you've previously created!
Creating Servers
While we are using a specific web framework for our examples, feel free to bring any web framework you like, as long as the framework is flexible enough to not interfere with our existing project.
For this guide, we will be using Fastify to make our web server. If you've worked with or are familiar with Express, then working with Fastify should feel familiar.
To start off, we create our web server inside the chooksOnLoad
script so we can safely initialize and destroy the server:
import { defineOnLoad } from 'chooksie'
import { fastify } from 'fastify'
// Set our port as an env variable, and fallback to port 3000 if none was set.
const PORT = process.env.PORT ?? 3000
export const chooksOnLoad = defineOnLoad(async ctx => {
const app = fastify({
// Since Fastify actually uses Pino under the hood for logging, we can
// pass "type": "fastify" to enable logging integration with Chooksie!
logger: ctx.logger.child({ type: 'fastify' }),
})
// Start listening on our specified port.
await app.listen(PORT)
// Stop our server when it gets updated.
return async () => {
ctx.logger.info('Stopping server...')
await app.close()
}
})
import { defineOnLoad } from 'chooksie'
import { fastify } from 'fastify'
// Set our port as an env variable, and fallback to port 3000 if none was set.
const PORT = process.env.PORT ?? 3000
export const chooksOnLoad = defineOnLoad(async ctx => {
const app = fastify({
// Since Fastify actually uses Pino under the hood for logging, we can
// pass "type": "fastify" to enable logging integration with Chooksie!
logger: ctx.logger.child({ type: 'fastify' }),
})
// Start listening on our specified port.
await app.listen(PORT)
// Stop our server when it gets updated.
return async () => {
ctx.logger.info('Stopping server...')
await app.close()
}
})
const { defineOnLoad } = require('chooksie')
const { fastify } = require('fastify')
// Set our port as an env variable, and fallback to port 3000 if none was set.
const PORT = process.env.PORT ?? 3000
exports.chooksOnLoad = defineOnLoad(async ctx => {
const app = fastify({
// Since Fastify actually uses Pino under the hood for logging, we can
// pass "type": "fastify" to enable logging integration with Chooksie!
logger: ctx.logger.child({ type: 'fastify' }),
})
// Start listening on our specified port.
await app.listen(PORT)
// Stop our server when it gets updated.
return async () => {
ctx.logger.info('Stopping server...')
await app.close()
}
})
Then to keep things clean, we can define our routes in a separate file:
import type { Context } from 'chooksie'
import type { FastifyPluginAsync } from 'fastify'
// We define our routes inside another function so we can pass our context
function register(ctx: Context): FastifyPluginAsync {
const routes: FastifyPluginAsync = async app => {
// Some route just to check if our server works.
app.get('/', async (req, reply) => {
await reply.send({ env: process.env.NODE_ENV, time: Date.now() })
})
// A route to check our bot's ping.
app.get('/ping', async (req, reply) => {
await reply.send({ ping: ctx.client.ws.ping })
})
}
return routes
}
export default register
function register(ctx) {
const routes = async app => {
// Some route just to check if our server works.
app.get('/', async (req, reply) => {
await reply.send({ env: process.env.NODE_ENV, time: Date.now() })
})
// A route to check our bot's ping.
app.get('/ping', async (req, reply) => {
await reply.send({ ping: ctx.client.ws.ping })
})
}
return routes
}
export default register
function register(ctx) {
const routes = async app => {
// Some route just to check if our server works.
app.get('/', async (req, reply) => {
await reply.send({ env: process.env.NODE_ENV, time: Date.now() })
})
// A route to check our bot's ping.
app.get('/ping', async (req, reply) => {
await reply.send({ ping: ctx.client.ws.ping })
})
}
return routes
}
module.exports = register
Then back in our main file, we can import our routes and register it into our server:
import { defineOnLoad } from 'chooksie'
import { fastify } from 'fastify'
import register from './routes'
// Set our port as an env variable, and fallback to port 3000 if none was set.
const PORT = process.env.PORT ?? 3000
export const chooksOnLoad = defineOnLoad(async ctx => {
const app = fastify({
// Since Fastify actually uses Pino under the hood for logging, we can
// pass "type": "fastify" to enable logging integration with Chooksie!
logger: ctx.logger.child({ type: 'fastify' }),
})
// Here we register all our routes before starting the server.
await app.register(register(ctx))
// Start listening on our specified port.
await app.listen(PORT)
// Stop our server when it gets updated.
return async () => {
ctx.logger.info('Stopping server...')
await app.close()
}
})
import { defineOnLoad } from 'chooksie'
import { fastify } from 'fastify'
import register from './routes'
// Set our port as an env variable, and fallback to port 3000 if none was set.
const PORT = process.env.PORT ?? 3000
export const chooksOnLoad = defineOnLoad(async ctx => {
const app = fastify({
// Since Fastify actually uses Pino under the hood for logging, we can
// pass "type": "fastify" to enable logging integration with Chooksie!
logger: ctx.logger.child({ type: 'fastify' }),
})
// Here we register all our routes before starting the server.
await app.register(register(ctx))
// Start listening on our specified port.
await app.listen(PORT)
// Stop our server when it gets updated.
return async () => {
ctx.logger.info('Stopping server...')
await app.close()
}
})
const { defineOnLoad } = require('chooksie')
const { fastify } = require('fastify')
const register = require('./register')
// Set our port as an env variable, and fallback to port 3000 if none was set.
const PORT = process.env.PORT ?? 3000
exports.chooksOnLoad = defineOnLoad(async ctx => {
const app = fastify({
// Since Fastify actually uses Pino under the hood for logging, we can
// pass "type": "fastify" to enable logging integration with Chooksie!
logger: ctx.logger.child({ type: 'fastify' }),
})
// Here we register all our routes before starting the server.
await app.register(register(ctx))
// Start listening on our specified port.
await app.listen(PORT)
// Stop our server when it gets updated.
return async () => {
ctx.logger.info('Stopping server...')
await app.close()
}
})
If your application is running, you should already be able to open your browser and make a request to your server and it should respond.
Great! But now we want to add another route in our routes file. While we know our file gets updated each time we save it, how about those that depend on the file, like our server file?
Well why don't we add another route and see:
import type { Context } from 'chooksie'
import type { FastifyPluginAsync } from 'fastify'
// We define our routes inside another function so we can pass our context
function register(ctx: Context): FastifyPluginAsync {
const routes: FastifyPluginAsync = async app => {
// Some route just to check if our server works.
app.get('/', async (req, reply) => {
await reply.send({ env: process.env.NODE_ENV, time: Date.now() })
})
// A route to check our bot's ping.
app.get('/ping', async (req, reply) => {
await reply.send({ ping: ctx.client.ws.ping })
})
// A route to get how many servers our bot is on.
app.get('/servers', async (req, reply) => {
await reply.send({ count: ctx.client.guilds.cache.size })
})
}
return routes
}
export default register
function register(ctx) {
const routes = async app => {
// Some route just to check if our server works.
app.get('/', async (req, reply) => {
await reply.send({ env: process.env.NODE_ENV, time: Date.now() })
})
// A route to check our bot's ping.
app.get('/ping', async (req, reply) => {
await reply.send({ ping: ctx.client.ws.ping })
})
// A route to get how many servers our bot is on.
app.get('/servers', async (req, reply) => {
await reply.send({ count: ctx.client.guilds.cache.size })
})
}
return routes
}
export default register
function register(ctx) {
const routes = async app => {
// Some route just to check if our server works.
app.get('/', async (req, reply) => {
await reply.send({ env: process.env.NODE_ENV, time: Date.now() })
})
// A route to check our bot's ping.
app.get('/ping', async (req, reply) => {
await reply.send({ ping: ctx.client.ws.ping })
})
// A route to get how many servers our bot is on.
app.get('/servers', async (req, reply) => {
await reply.send({ count: ctx.client.guilds.cache.size })
})
}
return routes
}
module.exports = register
We save the file and then... our server restarted on its own?
Making a request to the new route, we can properly see the response we just added. How?
Reloading the Dependency Chain
That's because just like what we explained in External Scripts, the framework has full control over Node's Module Cache, and so the framework can intelligently reload dependent modules, while leaving untouched dependencies alone.
What does that mean? Well take this for example:
We have 4 modules, A
, B
, C
, and D
. Module A
depends on B
, module B
on C
, and C
on D
.
A -> B -> C -> D
Say we made changes to module C
. Module D
shouldn't be affected since it's not dependent on module C
. Meanwhile, modules A
and B
does rely on module C
, and so should be updated.
The framework then goes to the cache and removes modules A
, B
, and C
, while leaving module D
alone while also loading their scripts again, effectively reloading the modules A
B
and C
and letting module C
load the cached version of module D
.
A -> B -> C -> D
^
updated
new A -> new B -> new C -> old D
This results in a very clean and efficient way to write modular scripts without having to fully restart your application, which if you're paying attention is a recurring theme in our framework.
It is also quite a fun experience to work with!