Understand the template
Before starting to code a bot using this template, you should understand how it works in a more theoretical way (yeah we know, not the most entertaining part :P)
Here is a sample of the architecture of TSCord:
Architecture
TSCord is following a clear separation of concerns and is built on multiple layers, each having its own scope in the codebase.
Presentation layer
The first brick in this layered architecture, and the place where all of the flow starts are the Events listeners triggered by interactions on Discord. Theses interactions are then redirected to the correct Handler, similar to a controller in a MVC pattern.
We can also trigger our own Custom Events at any time from anywhere in the codebase.
Business layer
The business layer includes all the logic of the application. It is represented by the Services and Utility classes. Indeed, the handlers should contain the least amount of logic possible.
Data layer
Last but not least, the data layer have the responsibility of manipulating and persisting data to a database.
We are using Mikro-ORM within TSCord so you don't have to worry a lot about this layer, except to write some custom methods on repositories.
Files structure
Here is a simplified view of the template structure:
tscord-template
├── .env # environments variables
├── package.json # config for npm
├── mikro-orm.config.ts # exports the config of Mikro-ORM for the CLI (/!\ DO NOT TOUCH /!\)
├── pm2.config.json # config for PM2
├── tsconfig.json # config for the typescript transpiler (/!\ DO NOT TOUCH /!\)
├── .typesafe-i18n.json # config for the i18n plugin (/!\ DO NOT TOUCH /!\)
├── docker-compose.yml # docker-compose config file
├── .docker # dockerfiles
│ └─ #...
├── .vscode # contains the configs for vscode (e.g: debug)
│ └─ # ...
├── assets # assets folder
│ └─ # ...
├── cli # plop CLI code base location
│ └─ # ...
├── database
│ ├─ db.sqlite # SQLite database location (if SQLite is configured as database for the bot)
│ └─ migrations # folder where are stored the database migrations
│ └─ #...
├── logs # log files (info, warn, debug, error)
│ └─ # ...
└── src # all the source code of the bot!
├─ commands # commands handlers (can be nested as wanted)
│ └─ #...
│ └─ #...
├── config # bot's config files
│ └─ # ...
├─ entities # entities models definitions (mikro-orm)
│ └─ #...
├─ events # events and custom events handlers
│ ├─ triggers # custom events triggers based on discord events
│ │ └─ # ...
│ └─ #...
├─ guards # guards functions (similar to middlewares)
│ └─ #...
├─ i18n # localization (i18n) system. /!\ YOU SHOULD NOT TOUCH FILES, ONLY LOCALES FOLDERS
│ ├─ en
│ ├─ fr
│ └─ #...
├─ services # services classes exposing most of the features of the bot
│ └─ #...
├─ utils # utilities
│ ├─ functions # utility simple pure functions
│ │ └─ #...
│ ├─ classes # utility classes
│ │ └─ #...
│ ├─ decorators # implementations of decorators
│ │ └─ #...
│ └─ types # typescript types definitions
│ └─ #...
├─ client.ts # discordx client config
└─ main.ts # main file that starts all the bot logic
Typescript
TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.
discordx
Under the hood, TSCord uses discordx that is himself a superset of discord.js.
We HIGHLY recommend to check their documentation here: https://discordx.js.org/.
Decorators
A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression , where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.
This template and discordx are using decorators a lot.
Even if it is not difficult at all to understand and use them, creating ones is quite another thing. Don't hesitate to check the official decorators doc and take the already existing ones as examples (src/utils/decorators
).
Coding principles
Here are some code principles the template is using and that you should be aware of.
Dependency Injection
Dependency injection is basically providing the objects that an object needs (its dependencies) instead of having it construct them itself. It's a very useful technique for testing, since it allows dependencies to be mocked or stubbed out.
Dependencies can be injected into objects by many means (such as constructor injection or setter injection). One can even use specialized dependency injection frameworks (e.g. Spring) to do that, but they certainly aren't required.
In addition of that, you can use the Singleton Pattern that will instantiate a class only once. Furthermore, this is this single instance that will be passed everywhere in your codebase using Dependency Injection.
We are using the tsyringe package internally to achieve this in this template.
Here is a concrete example of how you would use DI:
import { Service, Injectable } from '@/decorators'
@Service()
class LoggerService {
private uniqueIdentifier = Math.random() * 1000000 // each time this class is instantiated, it will have a merely unique 'uniqueIdentifier' attribute
public log(message: string) {
console.log(`[${this.uniqueIdentifier}] ${message}`)
}
}
@Injectable()
class Foo {
constructor(
private logger: LoggerService
) {}
public bar() {
this.logger.log('Hello world!')
}
}
const foo = new Foo()
foo.bar() // -> [175000] Hello World!
const logger = container.resolve(LoggerService)
logger.log('Hello World!') // -> [175000] Hello World!
Explanations:
- Thanks to the
@Service
decorator, theLoggerService
class will be instantiated once when your program starts - Then, this instance can be injected with 2 methods:
- By passing a parameter to the constructor with the class type of the desired instance (only possible when using the
@Injectable
decorator on the class) - By using the
container.resolve()
function (so we can get the instance from anywhere in the code, not only from classes)
- By passing a parameter to the constructor with the class type of the desired instance (only possible when using the
You can see that both methods will output 175000
as uniqueIdentifier
, showing that it is from the same class!
If you have an error like this:
Cannot inject the dependency "myDependency" at position #0 of "Service" constructor. Reason:
TypeInfo not known for "Object"
You should use the delay method even if you are not in the case of a circular dependency.
Resolvers
We also provide the resolveDependency
and resolveDependencies
functions from @/utils/functions
to resolve dependencies from anywhere.
It is, in some cases, better to use them than the automatic injection in the constructor.
Indeed, we've had some issues with the constructor injection in some edge cases that we hardly understand at the moment. We are working on it, but in the meantime, you can use these functions to resolve your dependencies if you encounter weird problems linked to tsyringe. For example, the previous Foo
class could be rewritten like this:
class Foo {
private logger: LoggerService
constructor() {
resolveDependency(LoggerService).then(logger => {
this.logger = logger
})
}
public bar() {
this.logger.log('Hello world!')
}
}