The following tutorial wil walk you through setting up a travetto
application from scratch. We'll be building a simple todo application. The entire source of the finished project can be found at github
. Additionally, you can use the Yeoman Generator
- Prerequisites
- Project initialization
- Establishing the model
- Configuring the data source
- Building the service layer
- Writing unit tests
- Creating the rest routes
- Creating the App Entry-point
- Test the final product
Install
Node
v10.x + (required)Mongodb
3.6+ (required)vscode
(recommended)Travetto Test Plugin
(recommended)
Install: Getting Ready
$ mkdir todo-project
$ cd todo-project
$ git init .
$ npm init -f
$ npm i @travetto/{log,test,rest-express,model-mongo}
Set tsconfig.json
to the following:
Config: Setting up tsconfig.json
{
"extends": "./node_modules/@travetto/base/tsconfig.json"
}
And set tslint.json
to the following:
Config: Setting up tslint.json
{
"extends": "@travetto/base/tslint.json"
}
Let's create the model for the todo application. The fields we will need should be:
id
as a unique identifiertext
as the actual todo informationcreated
the date the todo was createdcompleted
whether or not the todo was completed
Create the file src/model.ts
Code: Models, src/model.ts
import { Model, ModelCore } from '@travetto/model';
@Model()
export class Todo implements ModelCore {
id?: string;
text: string;
created?: Date;
completed?: boolean;
}
@Schema()
export class TodoSearch {
offset?: number;
limit?: number;
}
as you can see, the model structure is simple. Everything that uses the Model
services needs to implement ModelCore
.
Next we need to prepare the MongoModelSource
to be used at runtime.
We need to create src/config.ts
Code: Configuration, src/config.ts
import { InjectableFactory } from '@travetto/di';
import { ModelMongoSource, ModelMongoConfig } from '@travetto/model-mongo';
import { ModelSource } from '@travetto/model';
export class AppConfig {
@InjectableFactory()
static getDataSource(config: ModelMongoConfig): ModelSource {
return new ModelMongoSource(config);
}
}
The @InjectableFactory
allows you to create injection candidates. Note that the ModelMongoSource
has the return type
specified as ModelSource
.
Next we establish the functionality for the service layer. The operations we need are:
- Create a new todo
- Complete a todo
- Remove a todo
- Get all todos
Now we need to create src/service.ts
Code: Service Definition, src/service.ts
import { ModelService } from '@travetto/model';
import { Injectable, Inject } from '@travetto/di';
import { Todo, TodoSearch } from './model';
@Injectable()
export class TodoService {
@Inject()
private modelService: ModelService;
async add(todo: Todo) {
todo.created = new Date();
const saved = await this.modelService.save(Todo, todo);
return saved;
}
async getAll(search: TodoSearch) {
return this.modelService.getAllByQuery(Todo, search);
}
async complete(id: string, completed = true) {
return this.modelService.updatePartialByQuery(Todo,
{ where: { id } },
{ completed }
);
}
async remove(id: string) {
return this.modelService.deleteById(Todo, id);
}
}
After we have established our service layer, we will now construct some simple tests to verify the service layer is running correctly. First we need to initialize the testing configuration as the config in the src/
folder is not automatically scanned.
Create the new test config at test/config.ts
Code: Test configuration, test/config.ts
import { InjectableFactory } from '@travetto/di';
import { ModelSource, ModelService } from '@travetto/model';
import { ModelMongoSource, ModelMongoConfig } from '@travetto/model-mongo';
import { QueryVerifierService } from '@travetto/model/src/service/query';
export class TestConfig {
@InjectableFactory()
static testSource(): ModelSource {
return new ModelMongoSource(ModelMongoConfig.from({
namespace: `test-${Math.trunc(Math.random() * 10000)}`
}));
}
@InjectableFactory()
static modelService(src: ModelSource): ModelService {
return new ModelService(src, new QueryVerifierService());
}
}
The tests should be defined at test/service.ts
Code: Test bed, test/service.ts
import * as assert from 'assert';
import { Suite, Test, BeforeAll } from '@travetto/test';
import { DependencyRegistry } from '@travetto/di';
import { ModelRegistry } from '@travetto/model';
import { SchemaRegistry } from '@travetto/schema';
import { TodoService } from '../src/service';
import { Todo } from '../src/model';
@Suite()
export class TodoTest {
@BeforeAll()
async init() {
await import('./config');
await DependencyRegistry.init();
await ModelRegistry.init();
await SchemaRegistry.init();
}
@Test('Create todo')
async create() {
const svc = await DependencyRegistry.getInstance(TodoService);
const test = Todo.from({
text: 'Sample Task'
});
const saved = await svc.add(test);
assert.ok(saved.id);
}
@Test('Complete todo')
async complete() {
const svc = await DependencyRegistry.getInstance(TodoService);
const test = Todo.from({
text: 'Sample Task'
});
const saved = await svc.add(test);
assert.ok(saved.id);
let updated = await svc.complete(saved.id!);
assert(updated.completed === true);
updated = await svc.complete(saved.id!, false);
assert(updated.completed === false);
}
@Test('Delete todo')
async remove() {
const svc = await DependencyRegistry.getInstance(TodoService);
const test = Todo.from({
text: 'Sample Task'
});
const saved = await svc.add(test);
assert.ok(saved.id);
await svc.remove(saved.id!);
try {
await svc.get(saved.id!);
} catch (e) {
assert(e.message);
}
}
}
Now we establish the routes, providing an interface to the service layer.
Finally, we establish the controller at src/controller.ts
Code: Controller contents, src/controller.ts
import { Controller, Get, TypedBody, Post, Put, Delete, Request, TypedQuery } from '@travetto/rest';
import { Inject } from '@travetto/di';
import { SchemaBody, SchemaQuery } from '@travetto/schema/extension/rest';
import { TodoService } from './service';
import { Todo, TodoSearch } from './model';
@Controller('/todo')
export class TodoController {
@Inject()
private svc: TodoService;
/**
* Get all todos
*/
@Get('/')
@SchemaQuery(TodoSearch)
async getAll(req: TypedQuery<TodoSearch>): Promise<Todo[]> {
return this.svc.getAll(req.query);
}
/**
* Get Todo by id
* @param id {String} Todo id
*/
@Get('/:id')
async getById(req: Request): Promise<Todo> {
return this.svc.get(req.params.id);
}
/**
* Create a todo
*/
@Post('/')
@SchemaBody(Todo)
async create(req: TypedBody<Todo>): Promise<Todo> {
return await this.svc.add(req.body);
}
/**
* Complete a todod
* @param id {String} Todo id
*/
@Put('/:id/complete')
async complete(req: Request) {
return await this.svc.complete(req.params.id, req.query.completed);
}
/**
* Delete a todo
* @param id {String} Todo id
*/
@Delete('/:id')
async remove(req: Request) {
await this.svc.remove(req.params.id);
}
}
Finally, to pull all of this together, we need to launch the application entry point. This requires us to use the @Application
decorator to identify a class as an entry-point for the application. Any application can have multiple entry-points.
The entry-point should be at src/app.ts
as:
Code: Application entry point, src/app.ts
import { Application, InjectableFactory } from '@travetto/di';
import { RestApp, RestAppProvider } from '@travetto/rest';
import { RestExpressAppProvider } from '@travetto/rest-express';
@Application('todo')
export class TodoApp {
@InjectableFactory()
static getProvider(): RestAppProvider {
return new RestExpressAppProvider();
}
constructor(private app: RestApp) { }
run() {
this.app.run();
}
}
First we must start the application
Terminal: Output of application startup
$ npx travetto todo
DEBUG Initializing Phase bootstrap [ 'schema', 'base', 'log', 'config', 'compiler', 'registry' ]
Found configurations for [ 'dev' ]
INFO Initializing: application,dev,todo
INFO Configured {
"registry": {
"injectable": {
"@travetto/config": "Config",
"@travetto/model": [
"ModelController"
],
"@travetto/rest": [
"Controller"
]
},
"schema": {
"@travetto/model": "Model"
},
"rest": {
"interceptor": {
"@travetto/rest": [
"ContextInterceptor"
]
}
}
},
"api": {
"client": {
"output": "./api-client",
"format": "typescript-angular",
"formatOptions": "supportsES6=true,ngVersion=6.1"
}
}
}
Configured Transformers before [ 'test:line-numbers',
'registry',
'di',
'log',
'rest',
'schema',
'test:assert' ]
Initialized 0.072
2018-08-20T23:13:16 debug [@trv.rg.src.srv.registry: 42] Initialized @travetto.model:src.service.registry#$ModelRegistry
2018-08-20T23:13:16 debug [@trv.rg.src.srv.registry: 42] Initialized @travetto.rest:src.service.registry#$ControllerRegistry
2018-08-20T23:13:16 debug [@trv.rg.src.srv.registry: 42] Initialized @travetto.schema:src.service.registry#$SchemaRegistry
2018-08-20T23:13:16 debug [@trv.rg.src.srv.registry: 42] Initialized @travetto.di:src.service.registry#$DependencyRegistry
2018-08-20T23:13:16 debug [@trv.rg.src.srv.registry: 42] Initialized @travetto.registry:src.service.root#$RootRegistry
body-parser deprecated undefined extended: provide extended option node_modules/@travetto/registry/src/source/class-source.js:74:17
express-session deprecated undefined resave option; provide resave option node_modules/@travetto/registry/src/source/class-source.js:74:17
express-session deprecated undefined saveUninitialized option; provide saveUninitialized option node_modules/@travetto/registry/src/source/class-source.js:74:17
2018-08-20T23:13:16 info [@trv.rst.src.srv.app: 62] Sorting interceptors 0 []
2018-08-20T23:13:16 debug [@trv.rst.src.app: 62] Registering Controller Instance @travetto.swagger:src.service.controller#SwaggerController / 2
(node:29806) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
2018-08-20T23:13:16 debug [@trv.rst.src.app: 62] Registering Controller Instance @app:src.route#TodoController /todo 5
2018-08-20T23:13:16 info [@trv.rst.src.srv.app: 82] Listening on 3000
next, let's execute curl
requests to interact with the new api
Terminal: Creating todo by curl, and then fetching
# Let's create a new todo
$ curl -XPOST localhost:3000/todo -H 'Content-Type: application/json' -d '{ "text": "New Todo" }'
## returned data
{
"text": "New Todo",
"created": "2018-06-24T05:03:16.438Z",
"id": "5b2f2614020fd21df02cd216"
}
# Now let's list all todos currently saved
$ curl -XGET localhost:3000/todo -H 'Content-Type: application/json'
## returns
[
{
"id": "5b2f2614020fd21df02cd216",
"text": "New Todo",
"created": "2018-06-24T05:03:16.438Z"
}
]