This repo contains a boilerplate application for an Express JS API written in typescript.
Pretty straightforward. Clone the repo, then:
npm install
To build it and generate swagger.json documentation
npm run build
Or without swagger
npm run build-no-swagger
You can run the api in the vscode debugger with the provided debug config. If you just want to run the build:
node build/index.js
And you can hit the running API at http://localhost:1337. You might have to supply some configurations in the /config/application.properties file for logging output, etc.
This example includes a Webade singleton as an example of not only how to use webade with the API, but mainly as an example for how to include oracle queries and MyBatis xml support. COnfigurations for webade can be found in config/application.properties
.
Once configured, webade will create collections for your application actions, roles, and proxy database connections. Proxy database connections that include pooling will have a pool created
The basic API structure is based off of Controller classes that implement Controller
and use decorators to define the API properties, much like the standard Java/Spring API's.
There are a number of decorators, so take a look at the ServiceController
example in the application controllers
folder.
Here's an example of a simple controller that does nothing useful:
import { Controller } from '../core/Controller'
import { Route, SuccessResponse, Get, Path, Query } from '../core/Decorators'
@Route('root/endpoint')
export class MyController extends Controller {
@Get('other/endpoint/{argument}')
@SuccessResponse('200', 'OK')
public async getFunction (@Path() argument: string, @Query() queryStringVal: string) {
// do stuff
return { my: 'valid response' }
}
}
- All endpoint functions must be
public async
. MUST. No promise, no endpoint - All endpoints must contain a
@Get
,@Put
,@Post
,@Patch
or@Delete
decorator at least, or they wont be registered @Path
and@Query
argument names can be overriden by supplying an override in the decorator function, if your callers will be creating requests with a different name than your function arguments@Body
can't be used with@Get
, and it will fail if you try.- You don't need to start your paths with a slash.
- If you don't include a value in
@Route
,@Get
,@Put
,@Post
,@Patch
or@Delete
, I assume you meant/
. - You can add as many properties as you want to
application.properties
. They'll be fetchable via the AppProperties static accessor. - It is vital that you import your decorators from
core/Decorators
. There's another package in the application, calledtsoa
which includes a number of identically named decorators.tsoa
is used for generating the swagger documentation, and using the base implementation will cause route registration to fail (you can always do it manually if you want though). I could directly override the tsoa implementation, but I wanted to leave it relatively seperated in case we implement our own swagger generator in the future. - If you exclude the decorators, or do not extend
Controller
, swagger generation and/or route autowiring will not work. - Your endpoints won't register if you don't instantiate the controller class before Routes are created. Read on to learn how to do that
To ensure your routes to the controller get created, you currently need to update the routes file in routes/Routes.ts
(note that I am working on changing this implementation in the future so it either won't be needed, or will be defined a bit cleaner).
You add your service class to Routes.ts and instantiate it before (BEFORE!!!) the call to RouteManager.initializeRoutes(router)
RouteManager.initControllers(new MyController(), new MyOtherController())
// other controllers...
// then...
RouteManager.initializeRoutes(router)
And RouteManager will take care of the rest. The endpoints you defined should now display in the log when you rebuild and re-run your application. If you define a controller after, it will not autowire and you'll be responsible for route registration.
Note that you don't need to use the RouteManager at all. Feel free to supply manual endpoint routing or custom endpoints handlers as needed, they're plenty of good cases for doing it, if the automatic controller management doesn't fit your needs for some reason (it should for 95% of your use cases, and if you find a case it doesn't work for, let me know and I can make it work).
Included in the API are some examples of using myBatis v3 mappers to generate queries. Take a look at the webade.ts loaders for examples on how they're used, until I create some example endpoints that actually hit a DB.
A Decorator exists called @Security
. Currently it's a basic implementation that will automatically inject JWT validation and webade authentication: @Security('BearerAuth', ['some', 'required', 'scopes'])
It's simple for now, so expect these examples to expand to support a broader range, likely with an implementation of the passport library for expressjs. In the meantime, it's pretty simple to add more support to the existing Security decorator for other types if you need them. Just create a new middleware and wire it up in the decorator.
A Top level endpoint will be generated for you when the application starts up. This will dynamically build based off of your router configuration.
Familiar to anyone who's used our standard Java Spring API templates, included in this example is a default endpoint called /healthCheck
. Running the Health Check will return some status information about the current health of the application and subcomponents.
You can create your own Health Checks by extending the HealthValidator
class, and adding instantiating your validator in the HealthCheckLoader.ts
file in the ./health-checks
folder. Documentation and examples are found there as well.
If you use the swagger generator, a swagger.json file will be created by default and placed in the /public
directory. When your API is running, you can hit the pre-defined /openapi
endpoint. This will open up a swagger document viewer allowing you to view the generated swagger spec. It's cool, check it out!
The API has built in scheduled task support, using the TaskManager
service. The TaskManager is initialized on app startup with any preconfigured tasks, or you can add them at runtime.
In the scheduled-tasks
folder is a class called TaskLoader.ts
. This class will be called when TaskManager initializes, and any predefined scheduled tasks will automatically wire up. There are a number of examples in the TaskLoader already.
A Scheduled Task is created by instantiating a Task
type. A Task
is made up of the following attributes:
- A Name
- A Callback Function
- A numeric interval (in milliseconds) or cron expression
- If you're using a numeric interval, a loop flag that defaults to
true
. Iffalse
, your task will only run once after the timeout expires - cron job settings:
For example, display the log message every 10 seconds:
export const scheduledTasks: Task[] = [
new Task('Example Interval', 10000, () => {
console.log('An example task with a 10 second loop')
})
]
or, display the log message every 5 minutes, but only run on Mondays:
export const scheduledTasks: Task[] = [
new Task('Example Interval', '*/5 * * * 1', () => {
console.log('An example task with a cron expression')
})
]
As mentioned, cron expressions have additional options. Two of them, to be specific:
new Task('Example Cron with Options', '*/5 * * * 1', myCallbackFunction, false, {
scheduled: true,
timezone: "America/Sao_Paulo"
})
/* or */
const task: Task = {
name: 'Example Cron with Options',
cron: '*/5 * * * 1',
cronOptions: {
scheduled: true,
timezone: "America/Sao_Paulo"
},
callback: myCallbackFunction
}
The scheduled
option determines if the schedule will be started by default. If you set it to false, you need to manually start it by calling TaskManager.restartTask('<name>')
. Useful for ensuring a time delay on starting the job if you need some additional initializing to occur first
The timezone
option identifies which timezone the cron schedule should use. By default, it will use your machines timezone settings, but if you want your job to run at an interval specified by a different timezone, set that option here.
Next update to the task scheduler will likely be replacing some functionality with https://github.com/breejs/bree, which will allow for scheduled tasks on worker threads.
By Default, the project.json
contains the install components and types for Oracle DB and Posrgres, with an example for Oracle connections in the webade components (which also uses the myBatis config). Unless you're connecting to both databases, you shouldn't use both. Remove the install and the code for the connection stuff you dont need.
Included in the package is mw-experts/rsql
which allows you to parse rsql query strings and convert them to Where clauses usable by oracle or postgres queries. You can do this by passing in your query as a param on your endpoint, then:
const where = RsqlToSqlConverter.getInstance().convert(query)
A more complete example:
import mybatisMapper = require('mybatis-mapper')
import { PagedResult } from "../core/model/PagedResult"
import { RsqlToSqlConverter } from '@mw-experts/rsql'
export class myService {
public async getThings (page: number, rows: number, query: string): Promise<any | undefined> {
mybatisMapper.createMapper([path.resolve(__dirname, '../query-configs/smy-queries.xml')])
let where = null
if (query) {
where = RsqlToSqlConverter.getInstance().convert(query)
}
// create query and fetch result
const sql = mybatisMapper.getStatement('myQuery', 'myQuery_paged', { offset, rows, where }, {language: 'sql', indent: ' '})
const result = await Database.query(sql)
return new PagedResult(page, rows, Math.ceil(Number(countResult?.rows[0].count) / rows), result.rows)
}
}
Note: PagedResult is a core model class included in the API as a helper for returning paged results
RSQL is a query language for parametrized filtering of entries in RESTful APIs.
It’s based on FIQL (Feed Item Query Language) a URI-friendly syntax for expressing filters across the entries in an Atom Feed.
The simplicity of RSQL and its capability to express complex queries in a compact and HTTP URI-friendly way makes it a good candidate for becoming a generic query language for searching REST endpoints.
For example, you can query your resource like this:
/stations?query=elevation=gt=100,station_acronym==null
or
/readings?query=precipitation=ge=10;danger==5
or even
/readings?query=precipitation>=10 and danger==5
RSQL introduces simple and composite operators which can be used to build basic and complex queries.
Basic Operator | Description |
---|---|
== | Equal To |
!= | Not Equal To |
=gt= | Greater Than |
> | Greater Than |
=ge= | Greater Or Equal To |
>= | Greater Or Equal To |
=lt= | Less Than |
< | Less Than |
=le= | Less Or Equal To |
<= | Less Or Equal To |
=in= | In |
=out= | Not in |
=includes-all= | Includes all |
=includes-one= | Includes one |
These operators can be used to do all sort of simple queries.
Composite Operator | Description |
---|---|
; | Logical AND |
and | Logical AND |
, | Logical OR |
or | Logical OR |
These operators can be used to join the simple queries and build more involved queries which can be as complex as required.
- in double quotes - space, any unicode letter, any unicode number,
_
,-
,.
,'
,(
,)
- in single quotes - space, any unicode letter, any unicode number,
_
,-
,.
,"
,(
,)
- without quotes - any unicode letter, any unicode number,
_
,-
,.
- with == or != operators you can also use asterisk
*
as a wildcard
By default, operators evaluated from left to right. However, a parenthesized expression can be used to change the precedence.
- precipitation=lt=20;(station_name==TOBA CAMP,station_name==NICOLL)