- Create folder
backend
and navigate inside created folder - Run
npm init
- Run
npm i -S tsoa express body-parser
- Run
npm i -D ts-node typescript mkdirp
- Run
npx tsc --init
- Open
tsconfig.json
- Inside
compilerOptions
, setexperimentalDecorators
andesModuleInterop
totrue
- Inside
- Create file
tsoa.json
Open tsoa.json
and copy&paste following snippet
{
"entryFile": "src/index.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": [
"src/**/*Controller.ts"
],
"spec": {
"outputDirectory": "../api",
"specVersion": 3
},
"routes": {
"routesDir": "src/generated"
}
}
Next open your package.json
and create following run-configs:
"build:api": "mkdirp ../api && mkdirp src/generated && tsoa spec-and-routes",
"build:server": "npm run build:api && tsc",
"dev": "npm run build:api && ts-node ./src/index.ts",
"start": "node ./dist/index.js"
mkdirp
will check, if the specified folders exist and will create them otherwise
tsoa spec-and-routes
will generate all transpiled controllers and the swagger-specification-file. Optionally you can
split this task into tsoa spec
and tsoa routes
to only run one of those tasks.
ts-node
is a run-environment for typesciprt-files. You dont need to manually compile your ts-files into js-files and
run them separately. For production purposes it is still recommended, which will be done
with tsc && node ./dist/index.js
- Create folder
src
in yourbackend
-folder - Create file
index.ts
inside thesrc
-folder - Create folder
controller
inside thesrc
-folder - Create file
MainController.ts
inside thesrc/controller
-folder
Paste following snippet into MainController
import {Controller, Get, Route} from 'tsoa';
@Route('/main')
export class MainController extends Controller {
@Get('/')
ping(): string {
return 'Pong';
}
}
The @Route
-decorator defines the route-prefix for all operations/routes inside the controller. Therefore the path for
the ping
-route will be /main/
.
Paste following snippet into index.ts
import express, {Request, Response} from 'express';
import bodyParser from 'body-parser';
const app = express();
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
app.all('*', (_req: Request, res: Response) => {
return res.status(404).send('Route not found');
});
app.listen(8080, (err?: Error) => {
if (err) {
return console.error(err);
}
console.log('Server running on port 8080');
})
http://expressjs.com/ is a NodeJS-Framework for HTTP(S)-Services.
Once you run npm run dev
, you should get the message Server running on port 8080
in your terminal. However, if you
send a HTTP-Request to http://localhost:8080/main
, you will receive an 404-error. Thats because the MainController
has not been registered yet.
First, run npm run build:api
, which will create a folder generated
inside the src
-folder and also an api
-folder
in the project-root. If you open the swagger.json
inside, you should see the specs for the ping-function you have
declared in the MainController before.
Now open index.ts
again, and paste following snipped at the begin of the file
import {RegisterRoutes} from './generated/routes';
and also the following snipped right after the two app.use(...)
-lines:
RegisterRoutes(app);
With tsoa routes
, tsoa has generated a file which contains a runnable version of your MainController, and any other
Controllers you might create in the future. Now restart the server and you should get a 'Pong'-Response when you make a
GET-Request to http://localhost:8080/main
For the sake of demonstration, open the MainController
again update the file to following snippet:
import {Controller, Post, Path, Query, Body, Get, Route} from 'tsoa';
interface DemoRequest {
name: string,
randomNumber: number,
optionalBoolean?: boolean
}
interface DemoResponse {
message: string,
yourId: number,
requestedPage?: number
}
@Route('/main')
export class MainController extends Controller {
@Get('/')
ping(): string {
return 'Pong';
}
@Post('/demo/{id}')
demo(
@Path() id: number,
@Body() payload: DemoRequest,
@Query() page?: number
): DemoResponse {
let message = `Hello ${payload.name}. Your lucky number is ${payload.randomNumber}.`;
if (payload.optionalBoolean) {
message = message + ' Well done!';
}
return {
message,
requestedPage: page,
yourId: id
}
}
}
This will generate another route, which will contain an url- & query-parameter and also requires a body. You can test
those routes with Postman or with the api.http
-file in this repo. Copy&Paste it into your
project and run it with your IDE.
- Go to the project-root
- Run
vue create frontend
- Select
Manually select features
- Make sure,
TypeScript
is enabled and finish setup process however you like - Go inside the
frontend
-folder - Run
npm i -D @openapitools/openapi-generator-cli
Open the package.json
inside the frontend
-folder and add the following run-config:
"build:api": "openapi-generator-cli generate -i ../api/swagger.json -o src/generated -g typescript-fetch --additional-properties=typescriptThreePlus=true"
Next, create inside the frontend
-folder a file named vue.config.js
and paste the following snipped into it:
module.exports = {
devServer: {
port: 3000,
proxy: 'http://localhost:8080'
}
}
With the proxy
-option, the vue-devserver will redirect all requests, which do not lead to existing resources, to your
backend on port 8080.
Make sure you have Java 8 set as JAVA_HOME
Next, run npm run build:api
. You will see a new folder generated
inside the src
-folder.
Now, open the HelloWorld.vue
and replace script
and template
with the following snippet:
<template>
<div class="hello">
<h3>API Calls</h3>
<form @submit.prevent="sendDemoRequest">
<input type="text" v-model="demoRequest.name" required>
<br>
<input type="number" v-model.number="demoRequest.randomNumber" required>
<br>
<input type="checkbox" v-model="demoRequest.optionalBoolean"> Optional Boolean
<br>
<button type="submit">Send Request</button>
</form>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import {Configuration, DefaultApi, DemoRequest} from '@/generated';
const apiConfiguration = new Configuration({
basePath: location.origin
});
const api = new DefaultApi(apiConfiguration);
export default Vue.extend({
name: 'HelloWorld',
data() {
return {
demoRequest: {
name: '',
randomNumber: 0,
optionalBoolean: false
} as DemoRequest
}
},
methods: {
async sendDemoRequest() {
const response = await api.demo({
id: 12,
page: 23,
demoRequest: this.demoRequest
});
alert(JSON.stringify(response));
}
}
});
</script>
DemoRequest
is the generated interface from the MainController
in the backend. Also, the id
-param and the
optional page
-queryparam are needed. If you change the api in the future, those interfaces will also update and you
will get an error on compilation.
In larger projects, you should do HTTP-Requests inside a Vuex-Store. You can do the same in any store or vuex-module. Simply instanciate the api you want to use.
- tsoa documentation
- NestJS (Evolution of tsoa/Springframework for NodeJS)
- Typescript documentation
- Openapi/Swagger