├── docs ├── README.md ├── getting-started.md ├── related-modules.md └── api.md ├── .gitignore ├── appa.jpg ├── .travis.yml ├── examples ├── README.md ├── basic-usage.js ├── server.js └── client.js ├── log.js ├── send.js ├── error.js ├── tests ├── README.md └── index.js ├── LICENSE.md ├── package.json ├── CHANGELOG.md ├── CONDUCT.md ├── CONTRIBUTING.md ├── README.md └── index.js /docs/README.md: -------------------------------------------------------------------------------- 1 | # appa documentation -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started with appa -------------------------------------------------------------------------------- /docs/related-modules.md: -------------------------------------------------------------------------------- 1 | # Using appa with related modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | tmp 4 | npm-debug.log -------------------------------------------------------------------------------- /appa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sethvincent/appa/HEAD/appa.jpg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "6" 5 | - "5" 6 | - "4" 7 | - "4.4" 8 | - "4.2" 9 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | - [Basic usage](basic-usage.js) 4 | - Example [server](server.js) and [client](client.js) 5 | -------------------------------------------------------------------------------- /log.js: -------------------------------------------------------------------------------- 1 | var logger = require('pino') 2 | 3 | module.exports = function createLogger (options) { 4 | options = options || { level: 'info' } 5 | return logger(options, options.stream) 6 | } 7 | -------------------------------------------------------------------------------- /send.js: -------------------------------------------------------------------------------- 1 | var response = require('response') 2 | 3 | module.exports = function send (statusCode, data) { 4 | if (typeof statusCode === 'object') { 5 | data = statusCode 6 | statusCode = 200 7 | } 8 | 9 | return response.json(data).status(statusCode) 10 | } 11 | -------------------------------------------------------------------------------- /examples/basic-usage.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var app = require('../index')() 3 | var send = require('../send') 4 | var log = app.log 5 | 6 | app.on('/', function (req, res, ctx) { 7 | send(200, { message: 'oh hey friends' }).pipe(res) 8 | }) 9 | 10 | http.createServer(app).listen(3000, function () { 11 | log.info('server started at http://127.0.0.1:3000') 12 | }) 13 | -------------------------------------------------------------------------------- /error.js: -------------------------------------------------------------------------------- 1 | var xtend = require('xtend') 2 | var send = require('./send') 3 | 4 | module.exports = function error (statusCode, message, data) { 5 | if (typeof statusCode === 'string') { 6 | data = message 7 | message = statusCode 8 | statusCode = 404 9 | } 10 | 11 | data = data || {} 12 | data = xtend(data, { statusCode: statusCode, message: message }) 13 | return send(statusCode, data) 14 | } 15 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Tests are run using [tape](https://npmjs.com/tape). 4 | 5 | Linting is performed using [standard](https://npmjs.com/standard) 6 | 7 | ## Running tests 8 | 9 | ```sh 10 | git clone {this repo} 11 | cd {this repo} 12 | npm install 13 | npm test 14 | ``` 15 | 16 | `npm test` runs both the linter and the tests. 17 | 18 | ### Only run the linter 19 | 20 | ```sh 21 | npm run lint 22 | ``` 23 | 24 | ### Only run the tests 25 | 26 | ```sh 27 | npm run test:node 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var app = require('../index')() 3 | var send = require('../send') 4 | var error = require('../error') 5 | var log = app.log 6 | 7 | app.on('/', function (req, res, ctx) { 8 | if (req.method === 'POST') { 9 | return send(200, ctx.body).pipe(res) 10 | } else if (req.method === 'GET') { 11 | return send(200, { message: 'oh hey friends' }).pipe(res) 12 | } 13 | 14 | return error(400, 'Method not allowed').pipe(res) 15 | }) 16 | 17 | http.createServer(app).listen(3000, function () { 18 | log.info('server started at http://127.0.0.1:3000') 19 | }) 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # [ISC License](https://spdx.org/licenses/ISC) 2 | 3 | Copyright (c) 2016, Seth Vincent 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /examples/client.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | 3 | var host = 'http://127.0.0.1:3000/' 4 | 5 | function post (cb) { 6 | request({ 7 | url: host, 8 | method: 'POST', 9 | json: { message: 'hi' } 10 | }, cb) 11 | } 12 | 13 | function get (cb) { 14 | request({ 15 | url: host, 16 | method: 'GET', 17 | json: true 18 | }, cb) 19 | } 20 | 21 | function destroy (cb) { 22 | request({ 23 | url: host, 24 | method: 'DELETE', 25 | json: true 26 | }, cb) 27 | } 28 | 29 | post(function (err, res, body) { 30 | if (err) console.log(err) 31 | console.log(res.statusCode, body) 32 | 33 | get(function (err, res, body) { 34 | if (err) console.log(err) 35 | console.log(res.statusCode, body) 36 | 37 | destroy(function (err, res, body) { 38 | if (err) console.log(err) 39 | console.log(res.statusCode, body) 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appa", 3 | "version": "6.1.3", 4 | "description": "Quickly create simple JSON API services.", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "standard", 8 | "deps": "dependency-check . && dependency-check . --unused --no-dev", 9 | "test:node": "tape tests/index.js | tap-spec", 10 | "test": "npm run lint && npm run deps && npm run test:node", 11 | "docs:api": "documentation build index.js -f md -o docs/API.md", 12 | "build": "npm run docs:api" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/sethvincent/appa.git" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/sethvincent/appa/issues" 22 | }, 23 | "homepage": "https://github.com/sethvincent/appa#readme", 24 | "dependencies": { 25 | "JSONStream": "^1.1.2", 26 | "pino": "^2.7.5", 27 | "pino-http": "^1.0.8", 28 | "pump": "^1.0.1", 29 | "qs": "^6.1.0", 30 | "raw-body": "^2.2.0", 31 | "response": "^0.18.0", 32 | "type-is": "^1.6.12", 33 | "wayfarer": "^6.1.5", 34 | "xtend": "^4.0.1" 35 | }, 36 | "devDependencies": { 37 | "dependency-check": "^2.5.3", 38 | "documentation": "^4.0.0-beta.18", 39 | "from2-string": "^1.1.0", 40 | "request": "^2.79.0", 41 | "standard": "^6.0.8", 42 | "tap-spec": "^4.0.2", 43 | "tape": "^4.5.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # appa change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## Unreleased 8 | 9 | > Note: unreleased changes are listed here 10 | 11 | ## 6.1.3 - 2017-07-02 12 | 13 | ## Fixed 14 | 15 | - make it possible to skip body parsing with `{ parse: false }` option 16 | 17 | ## 6.1.2 - 2017-02-25 18 | 19 | ## Fixed 20 | 21 | - fix double response bug 22 | 23 | ## 6.1.1 - 2017-02-25 24 | 25 | ## Fixed 26 | 27 | - uncaught errors inside request handlers are now caught and a `500 Internal server error` response is sent 28 | - Added docs to readme about error handling and logging 29 | - update tests/README.md 30 | 31 | ## 6.1.0 - 2017-02-19 32 | 33 | ## Added 34 | - disable logging with `var app = appa({ logging: false })` 35 | - Improved [api docs](docs/api.md) 36 | - refactored to use the raw-body module, which allows setting the limit of incoming request body sizes 37 | - individual request handlers can now pass a `parseJSON: false` option to disable JSON parsing 38 | - add [server](examples/server.js) and [client](examples/client.js) examples 39 | 40 | ## 6.0.0 - 2016-11-01 41 | 42 | ### Changed 43 | - use `send(data).pipe(res)` rather than `send(res, data)` 44 | 45 | ## 5.0.1 - 2016-10-24 46 | 47 | ### Added 48 | - Add a docs directory 49 | - Add CONDUCT.md 50 | - Add CHANGELOG.md 51 | 52 | ### Changed 53 | - Move send, error, and log into separate files 54 | - Revise README.md with links and updated info. 55 | - License: MIT > ISC 56 | - Create tests directory 57 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at sethvincent@gmail.com. The project team 59 | will review and investigate all complaints, and will respond in a way that it deems 60 | appropriate to the circumstances. The project team is obligated to maintain 61 | confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to `appa` 2 | 3 | ### Prerequisites: 4 | 5 | - Familiarity with [GitHub PRs](https://help.github.com/articles/using-pull-requests) (pull requests) and issues. 6 | - Knowledge of [markdown](https://help.github.com/articles/markdown-basics/) for editing `.md` documents. 7 | - Understanding of JavaScript, Node.js, and http 8 | 9 | ### Types of contributions 10 | 11 | Here are a few of the types of contributions that we're looking for. Have an idea but it doesn't fit into this list? Make an issue for it in the issue's queue foor the project. 12 | 13 | #### Ideas 14 | 15 | Participate in an issues thread or start your own to have your voice heard. 16 | 17 | #### Design 18 | 19 | Hey maybe a new `appa` logo that kinda looks like an abba logo but isn't just a sloppily manipulated photograph? 20 | 21 | #### Writing 22 | 23 | Contribute your expertise in an area by helping us expand the included documentation. 24 | 25 | #### Copy editing 26 | 27 | Fix typos, clarify language, and generally improve the quality of the code & documentation. 28 | 29 | #### Code 30 | 31 | Fix issues or contribute new features. 32 | 33 | 34 | ### Steps to contributing 35 | 36 | - Fork the repository 37 | - Create a branch for your changes 38 | - Make the changes you'd like to contribute. See the "types of contributions" list above to learn more about what we're looking for 39 | - Submit a pull request 40 | 41 | ## Conduct 42 | 43 | We are committed to providing a friendly, safe and welcoming environment for 44 | all, regardless of gender, sexual orientation, disability, ethnicity, religion, 45 | or similar personal characteristic. 46 | 47 | On IRC, please avoid using overtly sexual nicknames or other nicknames that 48 | might detract from a friendly, safe and welcoming environment for all. 49 | 50 | Please be kind and courteous. There's no need to be mean or rude. 51 | Respect that people have differences of opinion and that every design or 52 | implementation choice carries a trade-off and numerous costs. There is seldom 53 | a right answer, merely an optimal answer given a set of values and 54 | circumstances. 55 | 56 | Please keep unstructured critique to a minimum. If you have solid ideas you 57 | want to experiment with, make a fork and see how it works. 58 | 59 | We will exclude you from interaction if you insult, demean or harass anyone. 60 | That is not welcome behaviour. We interpret the term "harassment" as 61 | including the definition in the 62 | [Citizen Code of Conduct](http://citizencodeofconduct.org/); 63 | if you have any lack of clarity about what might be included in that concept, 64 | please read their definition. In particular, we don't tolerate behavior that 65 | excludes people in socially marginalized groups. 66 | 67 | Private harassment is also unacceptable. No matter who you are, if you feel 68 | you have been or are being harassed or made uncomfortable by a community 69 | member, please contact one of the channel ops or any of the core team 70 | immediately. Whether you're a regular contributor or a newcomer, we care about 71 | making this community a safe place for you and we've got your back. 72 | 73 | Likewise any spamming, trolling, flaming, baiting or other attention-stealing 74 | behaviour is not welcome. 75 | 76 | 77 | ## Communication 78 | 79 | GitHub issues are the primary way for communicating about specific proposed 80 | changes to this project. 81 | 82 | Please follow the conduct guidelines above in all communication about this project. Language issues 83 | are often contentious and we'd like to keep discussion brief, civil and focused 84 | on what we're actually doing, not wandering off into too much imaginary stuff. -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var http = require('http') 3 | var request = require('request') 4 | var fromString = require('from2-string') 5 | 6 | var createApp = require('../index') 7 | var send = require('../send') 8 | 9 | function createServer (app) { 10 | return http.createServer(app) 11 | } 12 | 13 | test('create a server', function (t) { 14 | var app = createApp({ log: false }) 15 | var server = createServer(app).listen(0, function () { 16 | t.ok(app) 17 | server.close() 18 | t.end() 19 | }) 20 | }) 21 | 22 | test('create a route', function (t) { 23 | t.plan(6) 24 | var app = createApp({ log: false }) 25 | 26 | app.on('/', function (req, res, context) { 27 | t.ok(req) 28 | t.ok(res) 29 | t.ok(context) 30 | send({ hello: 'hi' }).pipe(res) 31 | }) 32 | 33 | var server = createServer(app).listen(3131, function () { 34 | request({ url: 'http://127.0.0.1:3131', json: true }, function (err, res, body) { 35 | t.notOk(err) 36 | t.ok(body) 37 | t.equal(body.hello, 'hi') 38 | server.close() 39 | }) 40 | }) 41 | }) 42 | 43 | test('querystring is parsed', function (t) { 44 | t.plan(4) 45 | var app = createApp({ log: { level: 'silent' } }) 46 | 47 | app.on('/', function (req, res, context) { 48 | send(context.query).pipe(res) 49 | }) 50 | 51 | var server = createServer(app).listen(3131, function () { 52 | request({ url: 'http://127.0.0.1:3131?hi=hello&hey[wut]=wat', json: true }, function (err, res, body) { 53 | t.notOk(err) 54 | t.ok(body) 55 | t.equal(body.hi, 'hello') 56 | t.equal(body.hey.wut, 'wat') 57 | server.close() 58 | }) 59 | }) 60 | }) 61 | 62 | test('pipe a stream', function (t) { 63 | t.plan(5) 64 | var app = createApp({ log: { level: 'silent' } }) 65 | 66 | app.on('/', function (req, res, context) { 67 | var stream = fromString(JSON.stringify(context.query)) 68 | app.pipe(stream, res, function (err) { 69 | t.notOk(err) 70 | }) 71 | }) 72 | 73 | var server = createServer(app).listen(3131, function () { 74 | request({ url: 'http://127.0.0.1:3131?hi=hello&hey[wut]=wat', json: true }, function (err, res, body) { 75 | t.notOk(err) 76 | t.ok(body) 77 | t.equal(body.hi, 'hello') 78 | t.equal(body.hey.wut, 'wat') 79 | server.close() 80 | }) 81 | }) 82 | }) 83 | 84 | test('receive params', function (t) { 85 | t.plan(7) 86 | var app = createApp({ log: { level: 'silent' } }) 87 | 88 | app.on('/list/:listkey/item/:itemkey', function (req, res, ctx) { 89 | t.ok(ctx.params) 90 | t.ok(ctx.params.listkey) 91 | t.ok(ctx.params.itemkey) 92 | send(ctx.params).pipe(res) 93 | }) 94 | 95 | var server = createServer(app).listen(3131, function () { 96 | request({ url: 'http://127.0.0.1:3131/list/hello/item/wat', json: true }, function (err, res, body) { 97 | t.notOk(err) 98 | t.ok(body) 99 | t.equal(body.listkey, 'hello') 100 | t.equal(body.itemkey, 'wat') 101 | server.close() 102 | }) 103 | }) 104 | }) 105 | 106 | test('handle internal errors', function (t) { 107 | t.plan(4) 108 | var app = createApp({ log: false }) 109 | 110 | app.on('/error', function (req, res, ctx) { 111 | throw new Error('oops') 112 | }) 113 | 114 | var server = createServer(app).listen(3131, function () { 115 | request({ url: 'http://127.0.0.1:3131/error', json: true }, function (err, res, body) { 116 | t.notOk(err) 117 | t.equal(res.statusCode, 500) 118 | t.equal(body.statusCode, 500) 119 | t.equal(body.message, 'Internal server error') 120 | server.close() 121 | }) 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # appa 2 | 3 | Quickly create simple JSON API services. 4 | 5 | [![npm][npm-image]][npm-url] 6 | [![travis][travis-image]][travis-url] 7 | [![standard][standard-image]][standard-url] 8 | [![conduct][conduct]][conduct-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/appa.svg?style=flat-square 11 | [npm-url]: https://www.npmjs.com/package/appa 12 | [travis-image]: https://img.shields.io/travis/sethvincent/appa.svg?style=flat-square 13 | [travis-url]: https://travis-ci.org/sethvincent/appa 14 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 15 | [standard-url]: http://npm.im/standard 16 | [conduct]: https://img.shields.io/badge/code%20of%20conduct-contributor%20covenant-green.svg?style=flat-square 17 | [conduct-url]: CONDUCT.md 18 | 19 | ![appa](https://raw.githubusercontent.com/sethvincent/appa/master/appa.jpg) 20 | 21 | ## Install 22 | 23 | Make sure you've got [node installed](http://nodejs.org), then make `appa` a project dependency: 24 | 25 | ```sh 26 | npm install --save appa 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```js 32 | var http = require('http') 33 | var app = require('appa')() 34 | var send = require('appa/send') 35 | var log = app.log 36 | 37 | app.on('/', function (req, res, context) { 38 | send({ message: 'oh hey friends' }).pipe(res) 39 | }) 40 | 41 | http.createServer(app).listen(3000, function () { 42 | log.info('server started at http://127.0.0.1:3000') 43 | }) 44 | ``` 45 | 46 | ## Error handling 47 | 48 | Any uncaught errors that occur in a request handler will be caught and a `500 Internal server error` response will be sent. 49 | 50 | Send error responses using the `appa/error` module: 51 | 52 | ```js 53 | var error = require('appa/error') 54 | 55 | module.exports = function (req, res, ctx) { 56 | return error(404, 'Not found').pipe(res) 57 | } 58 | ``` 59 | 60 | Sending an error response does not automatically log the error, so to add that you can do something like: 61 | 62 | ```js 63 | var error = require('appa/error') 64 | var log = require('appa/log')() 65 | 66 | module.exports = function (req, res, ctx) { 67 | log.error(req.method, '500', errorStack) 68 | return error(500, 'Internal server error').pipe(res) 69 | } 70 | ``` 71 | 72 | ## Logging 73 | 74 | appa uses [pino](https://npmjs.com/pino) for logging. Pass options to pino with `options.log`: `appa({ log: pinoOptions })`. 75 | 76 | See [example pino usage](https://github.com/pinojs/pino#usage) and [all pino options](https://github.com/pinojs/pino/blob/master/docs/API.md#pinooptions-stream). 77 | 78 | Or disable logging completely by setting `options.log` to `false`: `appa({ log: false })`. 79 | 80 | ## Documentation 81 | - [API](docs/api.md) 82 | - [Tests](tests/) 83 | 84 | ### Examples 85 | - [Basic example](examples/basic.js) 86 | - Example [server](examples/server.js) and [client](examples/client.js) 87 | 88 | ## Contributing 89 | 90 | Contributions are welcome! Please read the [contributing guidelines](CONTRIBUTING.md) first. 91 | 92 | ## Conduct 93 | 94 | It is important that this project contributes to a friendly, safe, and welcoming environment for all. Read this project's [code of conduct](CONDUCT.md) 95 | 96 | ## Changelog 97 | 98 | Read about the changes to this project in [CHANGELOG.md](CHANGELOG.md). The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 99 | 100 | ## Contact 101 | 102 | - **issues** – Please open issues in the [issues queue](https://github.com/sethvincent/appa/issues) 103 | - **twitter** – Have a question? [@sethdvincent](https://twitter.com/sethdvincent) 104 | - **email** – Need in-depth support via paid contract? Send an email to sethvincent@gmail.com 105 | 106 | ## License 107 | 108 | [ISC](LICENSE.md) 109 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Table of Contents 4 | 5 | - [createApp](#createapp) 6 | - [app](#app) 7 | - [app.on](#appon) 8 | - [app.send](#appsend) 9 | - [app.error](#apperror) 10 | - [app.log](#applog) 11 | - [app.json](#appjson) 12 | - [app.pipe](#apppipe) 13 | 14 | ## createApp 15 | 16 | Create the application. Returns the `app` function that can be passed into `http.createServer`. 17 | 18 | **Parameters** 19 | 20 | - `config` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** 21 | - `config.log` **([Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean))** – Set to `false` to disable logging. Set to an object with options that are passed to [pino](https://npmjs.com/pino) and [pino-http](https://npmjs.com/pino-http) 22 | - `config.notFound` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** – Function that handlers 404 routes. Provides `req`, `res`, and `ctx` arguments just like other appa route handlers. Default: a function that sends a `404` statusCode with the message `Not found`. 23 | 24 | **Examples** 25 | 26 | ```javascript 27 | var appa = require('appa') 28 | 29 | var app = appa({ 30 | log: { level: 'info' }, // or set to `false` to disable all logging 31 | notFound: function (req, res, ctx) { 32 | return app.error(404, 'Not Found').pipe(res) 33 | } 34 | }) 35 | ``` 36 | 37 | Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** `app` 38 | 39 | ## app 40 | 41 | The request, response handler that is passed to `http.createServer`, and the object that 42 | provides methods for your app. 43 | 44 | **Parameters** 45 | 46 | - `req` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** – the http request object 47 | - `res` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** – the http response object 48 | 49 | **Examples** 50 | 51 | ```javascript 52 | var http = require('http') 53 | var appa = require('appa') 54 | 55 | var app = appa() 56 | var server = http.createServer(app) 57 | ``` 58 | 59 | ## app.on 60 | 61 | Route handler 62 | 63 | **Parameters** 64 | 65 | - `pathname` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** – the route for this handler 66 | - `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** – options for the request handler 67 | - `options.parseJSON` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** – optionally disable JSON parsing of the body. Default: `true` 68 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** – the route handler 69 | 70 | **Examples** 71 | 72 | ```javascript 73 | app.on('/', function (req, res, ctx) { 74 | // handle the route 75 | }) 76 | ``` 77 | 78 | ## app.send 79 | 80 | Send a JSON object as a response 81 | 82 | **Parameters** 83 | 84 | - `statusCode` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** – the status code of the response, default is 200 85 | - `data` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** – the data that will be stringified into JSON 86 | 87 | **Examples** 88 | 89 | ```javascript 90 | var send = require('appa/send') 91 | 92 | app.on('/', function (req, res, ctx) { 93 | send({ message: 'hi' }).pipe(res) 94 | }) 95 | ``` 96 | 97 | ## app.error 98 | 99 | Send a JSON error response 100 | 101 | **Parameters** 102 | 103 | - `statusCode` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** – the status code of the response, default is 404 104 | - `message` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** – the message that will be stringified into JSON 105 | - `data` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** – additional data about the error to send in the response 106 | 107 | **Examples** 108 | 109 | ```javascript 110 | var error = require('appa/error') 111 | 112 | app.on('/', function (req, res, ctx) { 113 | error(404, 'Resource not found').pipe(res) 114 | }) 115 | ``` 116 | 117 | ## app.log 118 | 119 | Create logs using the pino module: 120 | 121 | ## app.json 122 | 123 | Parse or stringify a JSON stream using the JSONStream module: 124 | 125 | ## app.pipe 126 | 127 | Compose a stream using the pump module: 128 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var qs = require('qs') 3 | var url = require('url') 4 | var rawBody = require('raw-body') 5 | var createRouter = require('wayfarer') 6 | var isType = require('type-is') 7 | var httplogger = require('pino-http') 8 | 9 | var createLogger = require('./log') 10 | var send = require('./send') 11 | var error = require('./error') 12 | 13 | /** 14 | * Create the application. Returns the `app` function that can be passed into `http.createServer`. 15 | * @name createApp 16 | * @param {Object} config 17 | * @param {Object|Boolean} config.log – Set to `false` to disable logging. Set to an object with options that are passed to [pino](https://npmjs.com/pino) and [pino-http](https://npmjs.com/pino-http) 18 | * @param {Function} config.notFound – Function that handlers 404 routes. Provides `req`, `res`, and `ctx` arguments just like other appa route handlers. Default: a function that sends a `404` statusCode with the message `Not found`. 19 | * @returns {Function} `app` 20 | * @example 21 | * var appa = require('appa') 22 | * 23 | * var app = appa({ 24 | * log: { level: 'info' }, // or set to `false` to disable all logging 25 | * notFound: function (req, res, ctx) { 26 | * return app.error(404, 'Not Found').pipe(res) 27 | * } 28 | * }) 29 | */ 30 | module.exports = function createApp (config) { 31 | config = config || {} 32 | 33 | config.log = config.log === false 34 | ? { level: 'silent' } 35 | : config.log || { level: 'info' } 36 | 37 | var log = createLogger(config.log) 38 | var httplog = httplogger(config.log, config.log.stream) 39 | var router = app.router = createRouter('/404') 40 | 41 | // provide a 404 fallback 42 | on('/404', (config.notFound || notFound)) 43 | function notFound (req, res) { error('Not found').pipe(res) } 44 | 45 | // ignore favicon.ico requests 46 | on('/favicon.ico', function (req, res) { send(200).pipe(res) }) 47 | 48 | /** 49 | * The request, response handler that is passed to `http.createServer`, and the object that 50 | * provides methods for your app. 51 | * @name app 52 | * @param {Object} req – the http request object 53 | * @param {Object} res – the http response object 54 | * @example 55 | * var http = require('http') 56 | * var appa = require('appa') 57 | * 58 | * var app = appa() 59 | * var server = http.createServer(app) 60 | */ 61 | function app (req, res) { 62 | httplog(req, res) 63 | var ctx = url.parse(req.url) 64 | ctx.query = qs.parse(ctx.query) 65 | return router(ctx.pathname, req, res, ctx) 66 | } 67 | 68 | /** 69 | * Route handler 70 | * @name app.on 71 | * @param {String} pathname – the route for this handler 72 | * @param {Object} options – options for the request handler 73 | * @param {Boolean} options.parse – optionally disable parsing of the body. Default: `true` 74 | * @param {Function} callback – the route handler 75 | * @example 76 | * app.on('/', function (req, res, ctx) { 77 | * // handle the route 78 | * }) 79 | */ 80 | function on (pathname, options, callback) { 81 | if (typeof options === 'function') { 82 | callback = options 83 | options = {} 84 | } 85 | 86 | assert.equal(typeof pathname, 'string', 'appa: pathname is required and must be a string') 87 | assert.equal(typeof options, 'object', 'appa: options must be an object') 88 | assert.equal(typeof callback, 'function', 'appa: callback function is required') 89 | 90 | options.parse = options.parse === false ? options.parse : true 91 | 92 | return router.on(pathname, function (params, req, res, ctx) { 93 | ctx.params = params 94 | log.info(ctx) 95 | 96 | function parse (req, options) { 97 | body(req, options, function (err, result) { 98 | if (err) return error(err.status, err.type).pipe(res) 99 | 100 | if (isType(req, ['json'])) { 101 | ctx.body = parseJSON(res, result) 102 | } else { 103 | ctx.body = result 104 | } 105 | 106 | return respond(req, res, ctx) 107 | }) 108 | } 109 | 110 | function respond (req, res, ctx) { 111 | try { 112 | return callback(req, res, ctx) 113 | } catch (e) { 114 | return error(500, 'Internal server error', e).pipe(res) 115 | } 116 | } 117 | 118 | if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') { 119 | if (options.parse === false) return respond(req, res, ctx) 120 | return parse(req, options) 121 | } 122 | 123 | return respond(req, res, ctx) 124 | }) 125 | } 126 | 127 | function parseJSON (res, result) { 128 | try { 129 | return JSON.parse(result) 130 | } catch (err) { 131 | return error(400, 'Invalid JSON').pipe(res) 132 | } 133 | } 134 | 135 | function body (req, options, callback) { 136 | if (!options.limit) options.limit = '1mb' 137 | if (!options.encoding) options.encoding = 'utf8' 138 | rawBody(req, options, callback) 139 | } 140 | 141 | /** 142 | * Send a JSON object as a response 143 | * @name app.send 144 | * @param {Number} statusCode – the status code of the response, default is 200 145 | * @param {Object} data – the data that will be stringified into JSON 146 | * @example 147 | * var send = require('appa/send') 148 | * 149 | * app.on('/', function (req, res, ctx) { 150 | * send({ message: 'hi' }).pipe(res) 151 | * }) 152 | */ 153 | app.send = send 154 | 155 | /** 156 | * Send a JSON error response 157 | * @name app.error 158 | * @param {Number} statusCode – the status code of the response, default is 404 159 | * @param {String} message – the message that will be stringified into JSON 160 | * @param {Object} data – additional data about the error to send in the response 161 | * @example 162 | * var error = require('appa/error') 163 | * 164 | * app.on('/', function (req, res, ctx) { 165 | * error(404, 'Resource not found').pipe(res) 166 | * }) 167 | */ 168 | app.error = error 169 | 170 | /** 171 | * Create logs using the pino module: https://npmjs.com/pino 172 | * @name app.log 173 | */ 174 | app.log = log 175 | 176 | /** 177 | * Parse or stringify a JSON stream using the JSONStream module: https://npmjs.com/JSONStream 178 | * @name app.json 179 | */ 180 | app.json = require('JSONStream') 181 | 182 | /** 183 | * Compose a stream using the pump module: https://npmjs.com/pump 184 | * @name app.pipe 185 | */ 186 | app.pipe = require('pump') 187 | 188 | app.on = on 189 | return app 190 | } 191 | --------------------------------------------------------------------------------