Skip to content

Commit

Permalink
docs(restful): use async (eggjs#1709)
Browse files Browse the repository at this point in the history
  • Loading branch information
atian25 authored and popomore committed Nov 29, 2017
1 parent b042937 commit e3ef3ec
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 220 deletions.
2 changes: 1 addition & 1 deletion docs/source/en/intro/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ exports.robot = {

Now try it using `curl localhost:7001/news -A "Baiduspider"`.

**Note:both Koa1 and Koa2 style middleware is supported, see [Use Koa's Middleware](../basics/middleware.md#Use-Koa's-Middleware)**
See [Middleware](../basics/middleware.md) for more details.


### Add Configurations
Expand Down
217 changes: 109 additions & 108 deletions docs/source/en/tutorials/restful.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ After interface convention, we begin to create a RESTful API.
Initializes the application using [egg-init](https://github.com/eggjs/egg-init) in the [quickstart](../intro/quickstart.md)

```bash
$ egg-init cnode-api --type=empty
$ egg-init cnode-api --type=simple
$ cd cnode-api
$ npm i
```
Expand All @@ -128,7 +128,7 @@ First of all, we follower previous design to register [router](../basics/router.
```js
// app/router.js
module.exports = app => {
app.resources('topics', '/api/v2/topics', 'topics');
app.router.resources('topics', '/api/v2/topics', app.controller.topics);
};
```

Expand All @@ -140,28 +140,35 @@ In [controller](../basics/controller.md), we only need to implement the interfac

```js
// app/controller/topics.js
const Controller = require('egg').Controller;

// defining the rule of request parameters
const createRule = {
accesstoken: 'string',
title: 'string',
tab: { type: 'enum', values: [ 'ask', 'share', 'job' ], required: false },
content: 'string',
};
exports.create = function* (ctx) {
// validate the `ctx.request.body` with the expected format
// status = 422 exception will be thrown if not passing the parameter validation
ctx.validate(createRule);
// call service to create a topic
const id = yield ctx.service.topics.create(ctx.request.body);
// configure the response body and status code
ctx.body = {
topic_id: id,
};
ctx.status = 201;
};

class TopicController extends Controller {
async create() {
const ctx = this.ctx;
// validate the `ctx.request.body` with the expected format
// status = 422 exception will be thrown if not passing the parameter validation
ctx.validate(createRule);
// call service to create a topic
const id = yield ctx.service.topics.create(ctx.request.body);
// configure the response body and status code
ctx.body = {
topic_id: id,
};
ctx.status = 201;
}
}
module.exports = TopicController;
```

As shown above, a controller mainly implements the following logic:
As shown above, a Controller mainly implements the following logic:

1. call the validate function to validate the request parameters
2. create a topic by calling service encapsulates business logic using the validated parameters
Expand All @@ -173,53 +180,53 @@ We will more focus on writing effective business logic in [service](../basics/se

```js
// app/service/topics.js
module.exports = app => {
class TopicService extends app.Service {
constructor(ctx) {
super(ctx);
this.root = 'https://cnodejs.org/api/v1';
}
const Service = require('egg').Service;

* create(params) {
class TopicService extends Service {
constructor(ctx) {
super(ctx);
this.root = 'https://cnodejs.org/api/v1';
}

async create(params) {
// call CNode V1 API
const result = yield this.ctx.curl(`${this.root}/topics`, {
method: 'post',
data: params,
dataType: 'json',
contentType: 'json',
});
// check whether the call was successful, throws an exception if it fails
this.checkSuccess(result);
// return the id of topis
return result.data.topic_id;
}
const result = await this.ctx.curl(`${this.root}/topics`, {
method: 'post',
data: params,
dataType: 'json',
contentType: 'json',
});
// check whether the call was successful, throws an exception if it fails
this.checkSuccess(result);
// return the id of topis
return result.data.topic_id;
}

// Encapsulated a uniform check function, can be reused in query, create, update and such on in service
checkSuccess(result) {
if (result.status !== 200) {
const errorMsg = result.data && result.data.error_msg ? result.data.error_msg : 'unknown error';
this.ctx.throw(result.status, errorMsg);
}
if (!result.data.success) {
// remote response error
this.ctx.throw(500, 'remote response error', { data: result.data });
}
// Encapsulated a uniform check function, can be reused in query, create, update and such on in service
checkSuccess(result) {
if (result.status !== 200) {
const errorMsg = result.data && result.data.error_msg ? result.data.error_msg : 'unknown error';
this.ctx.throw(result.status, errorMsg);
}
if (!result.data.success) {
// remote response error
this.ctx.throw(500, 'remote response error', { data: result.data });
}
}
}

return TopicService;
};
module.exports = TopicService;
```

After developing the service of topic creation, an interface have been completed from top to bottom.
After developing the Service of topic creation, an interface have been completed from top to bottom.

### Unified error handling

Normal business logic has been completed, but exceptions have not yet been processed. Controller and service may throw an exception as the previous coding, so it is recommended that throwing an exception to interrupt if passing invalided parameters from the client or calling the back-end service with exception.
Normal business logic has been completed, but exceptions have not yet been processed. Controller and Service may throw an exception as the previous coding, so it is recommended that throwing an exception to interrupt if passing invalided parameters from the client or calling the back-end service with exception.

- use controller `this.validate()` to validate the parameters, throw exception if it fails.
- call service `this.ctx.curl()` to access CNode service, may throw server exception due to network problems.
- an exception also will be thrown after service is getting the response of calling failure from CNode server.
- use Controller `this.ctx.validate()` to validate the parameters, throw exception if it fails.
- call Service `this.ctx.curl()` to access CNode API, may throw server exception due to network problems.
- an exception also will be thrown after Service is getting the response of calling failure from CNode API.

Default error handling is provided but might be inconsistent as the interface convention previously. We need to implement a unified error-handling middleware to handle the errors.

Expand All @@ -228,25 +235,25 @@ Create a file `error_handler.js` under `app/middleware` directory to create a ne
```js
// app/middleware/error_handler.js
module.exports = () => {
return function* (next) {
return async function errorHandler(ctx, next) {
try {
yield next;
await next();
} catch (err) {
// All exceptions will trigger an error event on the app and the error log will be recorded
this.app.emit('error', err, this);
ctx.app.emit('error', err, ctx);

const status = err.status || 500;
// error 500 not returning to client when in the production environment because it may contain sensitive information
const error = status === 500 && this.app.config.env === 'prod'
const error = status === 500 && ctx.app.config.env === 'prod'
? 'Internal Server Error'
: err.message;

// Reading from the properties of error object and set it to the response
this.body = { error };
ctx.body = { error };
if (status === 422) {
this.body.detail = err.errors;
ctx.body.detail = err.errors;
}
this.status = status;
ctx.status = status;
}
};
};
Expand All @@ -270,82 +277,73 @@ module.exports = {

Completing the coding just the first step, furthermore we need to add [Unit Test](../core/unittest.md) to the code.

### controller test
### Controller Testing

Let's start writing the unit test for the controller. We can simulate the implementation of the service layer in an appropriate way because the most important part is to test the logic as for controller. And mocking up the service layer according the convention of interface, so we can develop layered testing because the service layer itself can also covered by service unit test.
Let's start writing the unit test for the Controller. We can simulate the implementation of the Service layer in an appropriate way because the most important part is to test the logic as for Controller. And mocking up the Service layer according the convention of interface, so we can develop layered testing because the Service layer itself can also covered by Service unit test.

```js
const mock = require('egg-mock');
const { app, mock, assert } = require('egg-mock/bootstrap');

describe('test/app/controller/topics.test.js', () => {
let app;
before(() => {
// create an instance quickly using the egg-mock library
app = mock.app();
return app.ready();
});

afterEach(mock.restore);

// test the response of passing the error parameters
it('should POST /api/v2/topics/ 422', function* () {
it('should POST /api/v2/topics/ 422', () => {
app.mockCsrf();
yield app.httpRequest()
.post('/api/v2/topics')
.send({
accesstoken: '123',
})
.expect(422)
.expect({
error: 'Validation Failed',
detail: [{ message: 'required', field: 'title', code: 'missing_field' }, { message: 'required', field: 'content', code: 'missing_field' }],
});
return app.httpRequest()
.post('/api/v2/topics')
.send({
accesstoken: '123',
})
.expect(422)
.expect({
error: 'Validation Failed',
detail: [
{ message: 'required', field: 'title', code: 'missing_field' },
{ message: 'required', field: 'content', code: 'missing_field' },
],
});
});

// mock up the service layer and test the response of normal request
it('should POST /api/v2/topics/ 201', function* () {
it('should POST /api/v2/topics/ 201', () => {
app.mockCsrf();
app.mockService('topics', 'create', 123);
yield app.httpRequest()
.post('/api/v2/topics')
.send({
accesstoken: '123',
title: 'title',
content: 'hello',
})
.expect(201)
.expect({
topic_id: 123,
});
return app.httpRequest()
.post('/api/v2/topics')
.send({
accesstoken: '123',
title: 'title',
content: 'hello',
})
.expect(201)
.expect({
topic_id: 123,
});
});
});
```

As the controller testing above, we create an application using [egg-mock](https://github.com/eggjs/egg-mock) and simulate the client to send request through [SuperTest](https://github.com/visionmedia/supertest). In the testing, we also simulate the response from service layer to test the processing logic of controller layer
As the Controller testing above, we create an application using [egg-mock](https://github.com/eggjs/egg-mock) and simulate the client to send request through [SuperTest](https://github.com/visionmedia/supertest). In the testing, we also simulate the response from Service layer to test the processing logic of Controller layer

### service testing
### Service Testing

Unit test of service layer may focus on the coding logic. [egg-mock](https://github.com/eggjs/egg-mock) provides a quick method to test the service by calling the test method in the service, and SuperTest to simulate the client request is no longer needed.
Unit Test of Service layer may focus on the coding logic. [egg-mock](https://github.com/eggjs/egg-mock) provides a quick method to test the Service by calling the test method in the Service, and SuperTest to simulate the client request is no longer needed.

```js
const assert = require('assert');
const mock = require('egg-mock');
const { app, mock, assert } = require('egg-mock/bootstrap');

describe('test/app/service/topics.test.js', () => {
let app;
let ctx;
before(function* () {
app = mock.app();
yield app.ready();

beforeEach(() => {
// create a global context object so that can call the service function on a ctx object
ctx = app.mockContext();
});
})

describe('create()', () => {
it('should create failed by accesstoken error', function* () {
it('should create failed by accesstoken error', async () => {
try {
// calling service method on ctx directly
yield ctx.service.topics.create({
await ctx.service.topics.create({
accesstoken: 'hello',
title: 'title',
content: 'content',
Expand All @@ -354,9 +352,10 @@ describe('test/app/service/topics.test.js', () => {
assert(err.status === 401);
assert(err.message === 'error accessToken');
}
throw 'should not run here';
});

it('should create success', function* () {
it('should create success', async () => {
// not affect the normal operation of CNode by simulating the interface calling of CNode based on interface convention
// app.mockHttpclient method can easily simulate the appliation's HTTP request
app.mockHttpclient(`${ctx.service.topics.root}/topics`, 'POST', {
Expand All @@ -365,7 +364,8 @@ describe('test/app/service/topics.test.js', () => {
topic_id: '5433d5e4e737cbe96dcef312',
},
});
const id = yield ctx.service.topics.create({

const id = await ctx.service.topics.create({
accesstoken: 'hello',
title: 'title',
content: 'content',
Expand All @@ -375,8 +375,9 @@ describe('test/app/service/topics.test.js', () => {
});
});
```
In the testing of service layer above, we create a context object using the `app.createContext()` which provided by egg-mock and call the service method on context object to test directly. It can use `app.mockHttpclient()` to simulate the response of calling HTTP request, which allows us to focus on the logic testing of service layer without the impact of environment.

In the testing of Service layer above, we create a Context object using the `app.createContext()` which provided by egg-mock and call the Service method on Context object to test directly. It can use `app.mockHttpclient()` to simulate the response of calling HTTP request, which allows us to focus on the logic testing of Service layer without the impact of environment.

------

Details of code implementation and unit test are available in [eggjs/examples/cnode-api](https://github.com/eggjs/examples/tree/master/cnode-api)
See the full example at [eggjs/examples/cnode-api](https://github.com/eggjs/examples/tree/master/cnode-api).
2 changes: 1 addition & 1 deletion docs/source/zh-cn/intro/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ exports.robot = {

现在可以使用 `curl http://localhost:7001/news -A "Baiduspider"` 看看效果。

**提示:框架同时兼容 Koa1 和 Koa2 形式的中间件,具体参见 [使用 Koa 的中间件](../basics/middleware.md#使用-koa-的中间件)**
更多参见[中间件](../basics/middleware.md)文档。

### 配置文件

Expand Down
7 changes: 6 additions & 1 deletion docs/source/zh-cn/tutorials/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ title: 教程
- [快速入门](../intro/quickstart.md)
- [渐进式开发](./progressive.md)
- [RESTful API](./restful.md)
- [Async Function](./async-function.md)

## 模板引擎

Expand All @@ -12,6 +11,8 @@ title: 教程
可使用以下模板引擎,更多[查看](https://github.com/search?utf8=%E2%9C%93&q=topic%3Aegg-view&type=Repositories&ref=searchresults)

- [egg-view-nunjucks]
- [egg-view-react]
- [egg-view-vue]
- [egg-view-ejs]
- [egg-view-handlebars]
- [egg-view-pug]
Expand All @@ -24,6 +25,7 @@ title: 教程
- [egg-sequelize]
- [egg-mongoose]
- [egg-mysql],可查看 [MySQL 教程](./mysql.md)
- [egg-graphql]


[egg-sequelize]: https://github.com/eggjs/egg-sequelize
Expand All @@ -35,3 +37,6 @@ title: 教程
[egg-view-handlebars]: https://github.com/eggjs/egg-view-handlebars
[egg-view-pug]: https://github.com/chrisyip/egg-view-pug
[egg-view-xtpl]: https://github.com/eggjs/egg-view-xtpl
[egg-view-react]: https://github.com/eggjs/egg-view-react
[egg-view-vue]: https://github.com/eggjs/egg-view-vue
[egg-graphql]: https://github.com/eggjs/egg-graphql
Loading

0 comments on commit e3ef3ec

Please sign in to comment.