Browse Source

Minimize the core (#286)

* Removed useless package fields

* Overkill dependencies removed

* Lockfile added

* Break down the module usage

* Switched to minimist for argument parsing

* Cleaned up the code a little

* Made LOC number in readme correct

* No transpilation happening anymore

* micro is leightweight

* Replaced minimist with nanomist

* Replaced minimist in the code as well

* Be more explicit about what lightweight means

* How to transpile if not on Node.js 8.0.0

* Improved code example

* Use native stream module instead of isstream

* Removed --silent option, since the inspection is happening in micro-dev

* Properly grouped code

* Better name for server handler

* Drain current requests before exit

This closes #284

* Replaced nanomist with mri

* Moved documentation to wiki and linked it

* Better documentation section

* Splitted into two paragraphs

* Moved link to awesome-micro to a different position

* Added usage information

* Default port added

* Made server stoppable on older Node.js versions

* Throw error if async/await isn't working

* Made function for getting usage info more pure

* And even more pure

* Brought docs back into readme

* No need to set host to null

* Removed now-specific code

* Default port is 3000

* Only consider prod case in success message

* Use npx for travis commands

* Added error 10

* Linked errors

* Added missing errors

* Bumped dependencies to the latest version

* Improved code style

* Markdown files for missing errors

* Corrected exit code for version output

* Fixed usage in help

* Ability to log the version and missing usage info

* Added missing support for version flag

* All Node.js versions support the .close() callback

* Correct newline for shutdown message

* Exit process with correct code

* Corrected credits

* Stop hiding stack traces when TESTING env var is set

* Log errors to stderr

* Removed test for not printing errors during test

* Better name for error logging function

* Only shutdown gracefully on SIGTERM

* Errors always have a code

* Made clear when to use micro-dev

* Bigger logo added

* Made logo a little tinier

* Adjusted readme heading

* Matched description everywhere

* Added tim as an author
master
Leo Lamprecht 7 years ago
committed by GitHub
parent
commit
c4a3afd52c
  1. 7
      .travis.yml
  2. 118
      bin/micro.js
  3. 13
      errors/invalid-entry.md
  4. 9
      errors/invalid-package-json.md
  5. 20
      errors/no-export.md
  6. 10
      errors/old-node-version.md
  7. 10
      errors/path-missing.md
  8. 13
      errors/path-not-existent.md
  9. 4
      lib/error.js
  10. 46
      lib/handler.js
  11. 15
      lib/help.js
  12. 163
      lib/index.js
  13. 62
      lib/listening.js
  14. 26
      lib/module.js
  15. 156
      lib/server.js
  16. 8584
      package-lock.json
  17. 42
      package.json
  18. 208
      readme.md
  19. 2
      test/development.js
  20. 2
      test/index.js
  21. 2
      test/production.js
  22. 34
      test/testing.js

7
.travis.yml

@ -1,10 +1,7 @@
{
"language": "node_js",
"node_js": [
"6",
"node"
],
"node_js": "node",
"after_success": [
"./node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls"
"npx nyc report --reporter=text-lcov | npx coveralls"
]
}

118
bin/micro.js

@ -2,48 +2,50 @@
// Native
const path = require('path')
const { existsSync } = require('fs')
// Packages
const updateNotifier = require('update-notifier')
const nodeVersion = require('node-version')
const args = require('args')
const isAsyncSupported = require('is-async-supported')
// Ours
const pkg = require('../package')
// Throw an error if node version is too low
if (nodeVersion.major < 6) {
console.error(
`Error! Micro requires at least version 6 of Node. Please upgrade!`
)
process.exit(1)
}
const parseArgs = require('mri')
// Let user know if there's an update
// This isn't important when deployed to Now
if (!process.env.NOW && pkg.dist) {
updateNotifier({ pkg }).notify()
}
args
.option('port', 'Port to listen on', parseInt(process.env.PORT, 10) || 3000, Number)
.option(['H', 'host'], 'Host to listen on', '0.0.0.0')
.option(['s', 'silent'], 'Silent mode')
// Utilities
const serve = require('../lib')
const handle = require('../lib/handler')
const generateHelp = require('../lib/help')
const { version } = require('../package')
const logError = require('../lib/error')
const flags = args.parse(process.argv, {
minimist: {
// Check if the user defined any options
const flags = parseArgs(process.argv.slice(2), {
string: ['host', 'port'],
boolean: ['help', 'version'],
alias: {
p: 'port',
H: 'host',
s: 'silent'
h: 'help',
v: 'version'
},
boolean: ['silent'],
string: ['host']
unknown(flag) {
console.log(`The option "${flag}" is unknown. Use one of these:`)
console.log(generateHelp())
process.exit(1)
}
})
let file = args.sub[0]
// When `-h` or `--help` are used, print out
// the usage information
if (flags.help) {
console.log(generateHelp())
process.exit()
}
// Print out the package's version when
// `--version` or `-v` are used
if (flags.version) {
console.log(version)
process.exit()
}
let file = flags._[0]
if (!file) {
try {
@ -52,39 +54,49 @@ if (!file) {
file = packageJson.main || 'index.js'
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
console.error(`micro: Could not read \`package.json\`: ${err.message}`)
logError(
`Could not read \`package.json\`: ${err.message}`,
'invalid-package-json'
)
process.exit(1)
}
}
}
if (!file) {
console.error('micro: Please supply a file.')
args.showHelp()
logError('Please supply a file!', 'path-missing')
process.exit(1)
}
if (file[0] !== '/') {
file = path.resolve(process.cwd(), file)
}
if (!isAsyncSupported()) {
const asyncToGen = require('async-to-gen/register')
// Support for keywords "async" and "await"
const pathSep = process.platform === 'win32' ? '\\\\' : '/'
const directoryName = path.parse(path.join(__dirname, '..')).base
// This is required to make transpilation work on Windows
const fileDirectoryPath = path.parse(file).dir.split(path.sep).join(pathSep)
asyncToGen({
includes: new RegExp(
`.*${directoryName}?${pathSep}(lib|bin)|${fileDirectoryPath}.*`
),
excludes: null,
sourceMaps: false
})
if (!existsSync(file)) {
logError(
`The file or directory "${path.basename(file)}" doesn't exist!`,
'path-not-existent'
)
process.exit(1)
}
// Load package core with async/await support
// If needed... Otherwise use the native implementation
require('../lib')(file, flags)
const loadedModule = handle(file)
const server = serve(loadedModule)
server.on('error', err => {
console.error('micro:', err.stack)
process.exit(1)
})
server.listen(flags.port || 3000, flags.host, () => {
const details = server.address()
process.on('SIGTERM', () => {
console.log('\nmicro: Gracefully shutting down. Please wait...')
server.close(process.exit)
})
// `micro` is designed to run only in production, so
// this message is perfectly for prod
console.log(`micro: Accepting connections on port ${details.port}`)
})

13
errors/invalid-entry.md

@ -0,0 +1,13 @@
# Invalid Entry File
#### Why This Error Occurred
When the `micro` command was ran, you passed a path to a file or directory that contains invalid code. This code might either not be syntactically correct or throw an error on execution.
#### Possible Ways to Fix It
The only way to avoid this error is to ensure that the entry file to your microservice (the one passed to the `micro`) command contains code that doesn't contain any syntax errors and doesn't throw an error when executed.
### Useful Links
- [JSLint](http://www.jslint.com) - Validate the code of your entry file

9
errors/invalid-package-json.md

@ -0,0 +1,9 @@
# Invalid `package.json` File
#### Why This Error Occurred
The content of the `package.json` file that's located near the entry file of your microservice is not valid. This means that it's not correct JSON syntax.
#### Possible Ways to Fix It
The only way to avoid this error is to ensure that the file contains a valid JSON object. You can use [JSONLint](https://jsonlint.com) to find any possible errors.

20
errors/no-export.md

@ -0,0 +1,20 @@
# No Export
#### Why This Error Occurred
When `micro` tried to ran your microservice, it noticed that your code didn't export anything that could be run.
#### Possible Ways to Fix It
You need to ensure that the entry file you passed to the `micro` command contains an export - like this one:
```js
module.exports = (req, res) => {
res.end('test')
}
```
### Useful Links
- [List of examples](https://github.com/zeit/micro/tree/master/examples)
- [Usage information](https://github.com/zeit/micro#usage)

10
errors/old-node-version.md

@ -0,0 +1,10 @@
# Old Node Version
#### Why This Error Occurred
In order to execute code that contains the `async` or `await` keywords, you need to have at least version 8.0.0 of [Node.js](https://nodejs.org/en/) installed on the device that runs `micro`. If that's not the case, you will see this error.
#### Possible Ways to Fix It
- Ensure that at least version 8.0.0 of [Node.js](https://nodejs.org/en/) is installed
- [Transpile](https://github.com/zeit/micro#transpilation) the code of your microservice

10
errors/path-missing.md

@ -0,0 +1,10 @@
# Path Missing
#### Why This Error Occurred
When running the `micro` command, you need to pass a path to a file or directory that contains your microservice. If you don't define one, it will detect the entry file to your code using the `main` property inside the `package.json` file in the directory where the command is run.
#### Possible Ways to Fix It
- Enter the path to your microservice in the `main` property inside `package.json`
- Specify the path of your entry file when running the command: `micro <path>`

13
errors/path-not-existent.md

@ -0,0 +1,13 @@
# Path Not Existent
#### Why This Error Occurred
When the `micro` command ran, you passed it a path to a file or directory that does't exist. This is how such a command can look like:
```bash
micro <not-existing-path>
```
#### Possible Ways to Fix It
The only way to fix this is to pass a path to a file or directory that exists and contains a working microservice.

4
lib/error.js

@ -0,0 +1,4 @@
module.exports = (message, errorCode) => {
console.error(`micro: ${message}`)
console.error(`micro: https://err.sh/micro/${errorCode}`)
}

46
lib/handler.js

@ -0,0 +1,46 @@
// Native
const vm = require('vm')
// Utilities
const logError = require('./error')
const checkAsyncAwait = () => {
try {
// eslint-disable-next-line no-new
new vm.Script('(async () => ({}))()')
return true
} catch (err) {
return false
}
}
module.exports = file => {
let mod
try {
// eslint-disable-next-line import/no-dynamic-require
mod = require(file)
if (mod && typeof mod === 'object') {
mod = mod.default
}
} catch (err) {
if (!checkAsyncAwait()) {
logError(
'In order for `async` & `await` to work, you need to use at least Node.js 8!',
'old-node-version'
)
process.exit(1)
}
logError(`Error when importing ${file}: ${err.stack}`, 'invalid-entry')
process.exit(1)
}
if (typeof mod !== 'function') {
logError(`The file "${file}" does not export a function.`, 'no-export')
process.exit(1)
}
return mod
}

15
lib/help.js

@ -0,0 +1,15 @@
const green = text => `\x1b[32m${text}\x1b[0m`
module.exports = () => {
const usage = `\n Usage: ${green('micro')} [path] [options]
Options:
${green('-p, --port <n>')} Port to listen on (defaults to 3000)
${green('-H, --host')} The host on which micro will run
${green('-v, --version')} Output the version number
${green('-h, --help')} Show this usage information
`
return usage
}

163
lib/index.js

@ -1,41 +1,156 @@
// Native
const server = require('http').Server
const { Stream } = require('stream')
// Packages
const getPort = require('get-port')
const getRawBody = require('raw-body')
const typer = require('media-typer')
const { NODE_ENV } = process.env
const DEV = NODE_ENV === 'development'
const serve = fn => server((req, res) => exports.run(req, res, fn))
module.exports = serve
exports = serve
exports.default = serve
const createError = (code, message, original) => {
const err = new Error(message)
err.statusCode = code
err.originalError = original
return err
}
const send = (res, code, obj = null) => {
res.statusCode = code
if (obj === null) {
res.end()
return
}
if (Buffer.isBuffer(obj)) {
if (!res.getHeader('Content-Type')) {
res.setHeader('Content-Type', 'application/octet-stream')
}
res.setHeader('Content-Length', obj.length)
res.end(obj)
return
}
// Ours
const serve = require('./server')
const listening = require('./listening')
const getModule = require('./module')
if (obj instanceof Stream) {
if (!res.getHeader('Content-Type')) {
res.setHeader('Content-Type', 'application/octet-stream')
}
module.exports = async (file, flags, module = getModule(file)) => {
const server = serve(module)
obj.pipe(res)
return
}
let port = flags.port
let host = flags.host
let str = obj
const open = await getPort(port)
let inUse = open !== port
if (typeof obj === 'object' || typeof obj === 'number') {
// We stringify before setting the header
// in case `JSON.stringify` throws and a
// 500 has to be sent instead
if (inUse) {
port = open
// the `JSON.stringify` call is split into
// two cases as `JSON.stringify` is optimized
// in V8 if called with only one argument
if (DEV) {
str = JSON.stringify(obj, null, 2)
} else {
str = JSON.stringify(obj)
}
if (!res.getHeader('Content-Type')) {
res.setHeader('Content-Type', 'application/json')
}
}
inUse = {
old: flags.port,
open
res.setHeader('Content-Length', Buffer.byteLength(str))
res.end(str)
}
const sendError = (req, res, { statusCode, status, message, stack }) => {
statusCode = statusCode || status
if (statusCode) {
send(res, statusCode, DEV ? stack : message)
} else {
send(res, 500, DEV ? stack : 'Internal Server Error')
}
if (host === '0.0.0.0') {
host = null
console.error(stack)
}
server.on('error', err => {
console.error('micro:', err.stack)
exports.send = send
exports.sendError = sendError
exports.createError = createError
exports.run = (req, res, fn) =>
new Promise(resolve => resolve(fn(req, res)))
.then(val => {
if (val === null) {
send(res, 204, null)
return
}
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1)
// Send value if it is not undefined, otherwise assume res.end
// will be called later
if (undefined !== val) {
send(res, res.statusCode || 200, val)
}
})
.catch(err => sendError(req, res, err))
// Maps requests to buffered raw bodies so that
// multiple calls to `json` work as expected
const rawBodyMap = new WeakMap()
const parseJSON = str => {
try {
return JSON.parse(str)
} catch (err) {
throw createError(400, 'Invalid JSON', err)
}
}
server.listen(port, host, () => {
return listening(server, inUse, flags.silent)
exports.buffer = (req, { limit = '1mb', encoding } = {}) =>
Promise.resolve().then(() => {
const type = req.headers['content-type'] || 'text/plain'
const length = req.headers['content-length']
if (encoding === undefined) {
encoding = typer.parse(type).parameters.charset
}
const body = rawBodyMap.get(req)
if (body) {
return body
}
return getRawBody(req, { limit, length, encoding })
.then(buf => {
rawBodyMap.set(req, buf)
return buf
})
.catch(err => {
if (err.type === 'entity.too.large') {
throw createError(413, `Body exceeded ${limit} limit`, err)
} else {
throw createError(400, 'Invalid body', err)
}
})
})
exports.text = (req, { limit, encoding } = {}) =>
exports.buffer(req, { limit, encoding }).then(body => body.toString(encoding))
exports.json = (req, opts) =>
exports.text(req, opts).then(body => parseJSON(body))

62
lib/listening.js

@ -1,62 +0,0 @@
// Packages
const { write: copy } = require('clipboardy')
const ip = require('ip')
const chalk = require('chalk')
const boxen = require('boxen')
const copyToClipboard = async text => {
try {
await copy(text)
return true
} catch (err) {
return false
}
}
module.exports = async (server, inUse, silent) => {
const details = server.address()
const ipAddress = ip.address()
const url = `http://${ipAddress}:${details.port}`
const isTTY = process.stdout.isTTY
process.on('SIGINT', () => {
server.close()
process.exit(0)
})
if (!(silent || process.env.NOW)) {
let message = chalk.green('Micro is running!')
if (inUse) {
message +=
' ' +
chalk.red(
`(on port ${inUse.open}, because ${inUse.old} is already in use)`
)
}
message += '\n\n'
const localURL = `http://localhost:${details.port}`
message += `${chalk.bold('Local: ')} ${localURL}\n`
message += `${chalk.bold('On Your Network: ')} ${url}\n\n`
if (isTTY) {
const copied = await copyToClipboard(localURL)
if (copied) {
message += `${chalk.grey('Copied local address to clipboard!')}`
}
}
console.log(
boxen(message, {
padding: 1,
borderColor: 'green',
margin: 1
})
)
}
}

26
lib/module.js

@ -1,26 +0,0 @@
module.exports = file => {
let mod
try {
// eslint-disable-next-line import/no-dynamic-require
mod = require(file)
if (mod && typeof mod === 'object') {
mod = mod.default
}
} catch (err) {
console.error(`micro: Error when importing ${file}: ${err.stack}`)
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1)
}
if (typeof mod !== 'function') {
console.error(`micro: "${file}" does not export a function.`)
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1)
}
return mod
}

156
lib/server.js

@ -1,156 +0,0 @@
// Native
const server = require('http').Server
// Packages
const getRawBody = require('raw-body')
const typer = require('media-typer')
const isStream = require('isstream')
const Promise = require('bluebird')
const DEV = process.env.NODE_ENV === 'development'
const TESTING = process.env.NODE_ENV === 'test'
const serve = fn => server((req, res) => exports.run(req, res, fn))
module.exports = serve
exports = serve
exports.default = serve
exports.send = send
exports.sendError = sendError
exports.createError = createError
exports.run = (req, res, fn) =>
new Promise(resolve => resolve(fn(req, res)))
.then(val => {
if (val === null) {
send(res, 204, null)
return
}
// Send value if it is not undefined, otherwise assume res.end
// will be called later
if (undefined !== val) {
send(res, res.statusCode || 200, val)
}
})
.catch(err => sendError(req, res, err))
// Maps requests to buffered raw bodies so that
// multiple calls to `json` work as expected
const rawBodyMap = new WeakMap()
const parseJSON = str => {
try {
return JSON.parse(str)
} catch (err) {
throw createError(400, 'Invalid JSON', err)
}
}
exports.buffer = (req, { limit = '1mb', encoding } = {}) =>
Promise.resolve().then(() => {
const type = req.headers['content-type'] || 'text/plain'
const length = req.headers['content-length']
encoding = encoding === undefined
? typer.parse(type).parameters.charset
: encoding
const body = rawBodyMap.get(req)
if (body) {
return body
}
return getRawBody(req, { limit, length, encoding })
.then(buf => {
rawBodyMap.set(req, buf)
return buf
})
.catch(err => {
if (err.type === 'entity.too.large') {
throw createError(413, `Body exceeded ${limit} limit`, err)
} else {
throw createError(400, 'Invalid body', err)
}
})
})
exports.text = (req, { limit, encoding } = {}) =>
exports.buffer(req, { limit, encoding }).then(body => body.toString(encoding))
exports.json = (req, opts) =>
exports.text(req, opts).then(body => parseJSON(body))
function send(res, code, obj = null) {
res.statusCode = code
if (obj === null) {
res.end()
return
}
if (Buffer.isBuffer(obj)) {
if (!res.getHeader('Content-Type')) {
res.setHeader('Content-Type', 'application/octet-stream')
}
res.setHeader('Content-Length', obj.length)
res.end(obj)
return
}
if (isStream(obj)) {
if (!res.getHeader('Content-Type')) {
res.setHeader('Content-Type', 'application/octet-stream')
}
obj.pipe(res)
return
}
let str = obj
if (typeof obj === 'object' || typeof obj === 'number') {
// We stringify before setting the header
// in case `JSON.stringify` throws and a
// 500 has to be sent instead
// the `JSON.stringify` call is split into
// two cases as `JSON.stringify` is optimized
// in V8 if called with only one argument
if (DEV) {
str = JSON.stringify(obj, null, 2)
} else {
str = JSON.stringify(obj)
}
if (!res.getHeader('Content-Type')) {
res.setHeader('Content-Type', 'application/json')
}
}
res.setHeader('Content-Length', Buffer.byteLength(str))
res.end(str)
}
function sendError(req, res, { statusCode, status, message, stack }) {
statusCode = statusCode || status
if (statusCode) {
send(res, statusCode, DEV ? stack : message)
} else {
send(res, 500, DEV ? stack : 'Internal Server Error')
}
if (!TESTING) {
console.error(stack)
}
}
function createError(code, msg, orig) {
const err = new Error(msg)
err.statusCode = code
err.originalError = orig
return err
}

8584
package-lock.json

File diff suppressed because it is too large

42
package.json

@ -2,7 +2,7 @@
"name": "micro",
"version": "7.3.3",
"description": "Asynchronous HTTP microservices",
"main": "./lib/server.js",
"main": "./lib/index.js",
"files": [
"bin",
"lib"
@ -16,7 +16,10 @@
"ignores": [
"examples/**/*"
],
"extends": "prettier"
"extends": "prettier",
"rules": {
"unicorn/no-process-exit": 0
}
},
"lint-staged": {
"*.js": [
@ -36,23 +39,15 @@
"serverless",
"API"
],
"author": {
"name": "Zeit, Inc.",
"email": "team@zeit.co"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/zeit/micro/issues"
},
"homepage": "https://github.com/zeit/micro#readme",
"devDependencies": {
"ava": "0.19.1",
"ava": "0.21.0",
"coveralls": "2.13.1",
"eslint-config-prettier": "2.0.0",
"husky": "0.13.3",
"lint-staged": "3.5.1",
"nyc": "11.0.1",
"prettier": "1.3.1",
"eslint-config-prettier": "2.3.0",
"husky": "0.14.3",
"lint-staged": "4.0.2",
"nyc": "11.0.3",
"prettier": "1.5.3",
"request": "2.81.0",
"request-promise": "4.2.1",
"resumer": "0.0.0",
@ -61,19 +56,8 @@
"xo": "0.18.2"
},
"dependencies": {
"args": "3.0.2",
"async-to-gen": "1.3.3",
"bluebird": "3.5.0",
"boxen": "1.1.0",
"chalk": "1.1.3",
"clipboardy": "1.1.2",
"get-port": "3.1.0",
"ip": "1.1.5",
"is-async-supported": "1.2.0",
"isstream": "0.1.2",
"media-typer": "0.3.0",
"node-version": "1.0.0",
"raw-body": "2.2.0",
"update-notifier": "2.1.0"
"mri": "1.1.0",
"raw-body": "2.2.0"
}
}

208
readme.md

@ -1,6 +1,6 @@
![](https://raw.githubusercontent.com/zeit/art/31913be3107827adf10e1f491ec61480f63e19af/micro/logo.png)
<img src="https://raw.githubusercontent.com/zeit/art/6451bc300e00312d970527274f316f9b2c07a27e/micro/logo.png" width="50"/>
_**Micro —** Async ES6 HTTP microservices_
_**Micro** — Asynchronous HTTP microservices_
[![Build Status](https://travis-ci.org/zeit/micro.svg?branch=master)](https://travis-ci.org/zeit/micro)
[![Coverage Status](https://coveralls.io/repos/github/zeit/micro/badge.svg?branch=master)](https://coveralls.io/github/zeit/micro?branch=master)
@ -9,38 +9,31 @@ _**Micro —** Async ES6 HTTP microservices_
## Features
* **Easy**. Designed for usage with `async` and `await` ([more](https://zeit.co/blog/async-and-await))
* **Fast**. Ultra-high performance (even JSON parsing is opt-in).
* **Micro**. The whole project is ~100 lines of code.
* **Agile**. Super easy deployment and containerization.
* **Simple**. Oriented for single purpose modules (function).
* **Explicit**. No middleware. Modules declare all dependencies.
* **Standard**. Just HTTP!
* **Lightweight**. The package is small and the `async` transpilation is fast and transparent
* **Easy**: Designed for usage with `async` and `await` ([more](https://zeit.co/blog/async-and-await))
* **Fast**: Ultra-high performance (even JSON parsing is opt-in)
* **Micro**: The whole project is ~260 lines of code
* **Agile**: Super easy deployment and containerization
* **Simple**: Oriented for single purpose modules (function)
* **Standard**: Just HTTP!
* **Explicit**: No middleware - modules declare all [dependencies](https://github.com/amio/awesome-micro)
* **Lightweight**: With all dependencies, the package weighs less than a megabyte
## Usage
Firstly, install it:
**Important:** Micro is only meant to be used in production. In development, you should use [micro-dev](https://github.com/zeit/micro-dev), which provides you with a tool belt specifically tailored for developing microservices.
To prepare your microservice for running in the production environment, firstly install `micro`:
```bash
npm install --save micro
```
Then add a `start` script to your `package.json` like this:
```json
{
"main": "index.js",
"scripts": {
"start": "micro"
}
}
```
Then create an `index.js` file and populate it with function, that accepts standard [http.IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage) and [http.ServerResponse](https://nodejs.org/api/http.html#http_class_http_serverresponse) objects:
```js
module.exports = (req, res) => { res.end('Welcome to Micro') }
module.exports = (req, res) => {
res.end('Welcome to Micro')
}
```
Micro provides [useful helpers](https://github.com/zeit/micro#body-parsing) but also handles return values – so you can write it even shorter!
@ -49,7 +42,18 @@ Micro provides [useful helpers](https://github.com/zeit/micro#body-parsing) but
module.exports = () => 'Welcome to Micro'
```
Once all of that is done, just start the server:
Next, ensure that the `main` property inside `package.json` points to your microservice (which is inside `index.js` in this example case) and add a `start` script:
```json
{
"main": "index.js",
"scripts": {
"start": "micro"
}
}
```
Once all of that is done, the server can be started like this:
```bash
npm start
@ -57,8 +61,6 @@ npm start
And go to this URL: `http://localhost:3000` - 🎉
Now make sure to check out [awesome-micro](https://github.com/amio/awesome-micro) - a collection of plugins for Micro!
### `async` & `await`
<p><details>
@ -77,17 +79,49 @@ module.exports = async (req, res) => {
}
```
#### Transpilation
### Transpilation
The package takes advantage of native support for `async` and `await`, which is always as of **Node.js 8.0.0**! In turn, we suggest either using at least this version both in development and production (if possible), or transpiling the code using [async-to-gen](https://github.com/leebyron/async-to-gen), if you can't use the latest Node.js version.
In order to do that, you firstly need to install it:
```bash
npm install -g async-to-gen
```
And then add the transpilation command to the `scripts.build` property inside `package.json`:
```json
{
"scripts": {
"build": "async-to-gen input.js > output.js"
}
}
```
Once these two steps are done, you can transpile the code by running this command:
```bash
npm run build
```
That's all it takes to transpile by yourself. But just to be clear: **Only do this if you can't use Node.js 8.0.0**! If you can, `async` and `await` will just work right out of the box.
We use [is-async-supported](https://github.com/timneutkens/is-async-supported) combined with [async-to-gen](https://github.com/leebyron/async-to-gen),
so that the we only convert `async` and `await` to generators when needed.
### Port Based on Environment Variable
If you want to do it manually, you can! `micro(1)` is idempotent and
should not interfere.
When you want to set the port using an environment variable you can use:
`micro` exclusively supports Node 6+ to avoid a big transpilation
pipeline. `async-to-gen` is fast and can be distributed with
the main `micro` package due to its small size.
```
micro -p $PORT
```
Optionally you can add a default if it suits your use case:
```
micro -p ${PORT:-3000}
```
`${PORT:-3000}` will allow a fallback to port `3000` when `$PORT` is not defined.
### Body parsing
@ -117,7 +151,7 @@ module.exports = async (req, res) => {
}
```
#### API
### API
##### `buffer(req, { limit = '1mb', encoding = 'utf8' })`
##### `text(req, { limit = '1mb', encoding = 'utf8' })`
@ -146,8 +180,6 @@ module.exports = async (req, res) => {
}
```
#### API
##### `send(res, statusCode, data = null)`
- Use `require('micro').send`.
@ -175,8 +207,6 @@ const server = micro(async (req, res) => {
server.listen(3000)
```
#### API
##### micro(fn)
- This function is exposed as the `default` export.
@ -184,7 +214,23 @@ server.listen(3000)
- Returns a [`http.Server`](https://nodejs.org/dist/latest-v6.x/docs/api/http.html#http_class_http_server) that uses the provided `function` as the request handler.
- The supplied function is run with `await`. So it can be `async`
### Error handling
##### sendError(req, res, error)
- Use `require('micro').sendError`.
- Used as the default handler for errors thrown.
- Automatically sets the status code of the response based on `error.statusCode`.
- Sends the `error.message` as the body.
- Stacks are printed out with `console.error` and during development (when `NODE_ENV` is set to `'development'`) also sent in responses.
- Usually, you don't need to invoke this method yourself, as you can use the [built-in error handling](#error-handling) flow with `throw`.
##### createError(code, msg, orig)
- Use `require('micro').createError`.
- Creates an error object with a `statusCode`.
- Useful for easily throwing errors with HTTP status codes, which are interpreted by the [built-in error handling](#error-handling).
- `orig` sets `error.originalError` which identifies the original error (if any).
## Error Handling
Micro allows you to write robust microservices. This is accomplished primarily by bringing sanity back to error handling and avoiding callback soup.
@ -232,9 +278,7 @@ try {
}
```
If the error is based on another error that **Micro** caught, like a `JSON.parse` exception, then `originalError` will point to it.
If a generic error is caught, the status will be set to `500`.
If the error is based on another error that **Micro** caught, like a `JSON.parse` exception, then `originalError` will point to it. If a generic error is caught, the status will be set to `500`.
In order to set up your own error handling mechanism, you can use composition in your handler:
@ -255,25 +299,7 @@ module.exports = handleErrors(async (req, res) => {
})
```
#### API
##### sendError(req, res, error)
- Use `require('micro').sendError`.
- Used as the default handler for errors thrown.
- Automatically sets the status code of the response based on `error.statusCode`.
- Sends the `error.message` as the body.
- Stacks are printed out with `console.error` and during development (when `NODE_ENV` is set to `'development'`) also sent in responses.
- Usually, you don't need to invoke this method yourself, as you can use the [built-in error handling](#error-handling) flow with `throw`.
##### createError(code, msg, orig)
- Use `require('micro').createError`.
- Creates an error object with a `statusCode`.
- Useful for easily throwing errors with HTTP status codes, which are interpreted by the [built-in error handling](#error-handling).
- `orig` sets `error.originalError` which identifies the original error (if any).
### Testing
## Testing
Micro makes tests compact and a pleasure to read and write.
We recommend [ava](https://github.com/sindresorhus/ava), a highly parallel Micro test framework with built-in support for async tests:
@ -301,60 +327,7 @@ test('my endpoint', async t => {
Look at [test-listen](https://github.com/zeit/test-listen) for a
function that returns a URL with an ephemeral port every time it's called.
### Transpilation
We use [is-async-supported](https://github.com/timneutkens/is-async-supported) combined with [async-to-gen](https://github.com/leebyron/async-to-gen),
so that we only convert `async` and `await` to generators when needed.
If you want to do it manually, you can! `micro(1)` is idempotent and
should not interfere.
`micro` exclusively supports Node 6+ to avoid a big transpilation
pipeline. `async-to-gen` is fast and can be distributed with
the main `micro` package due to its small size.
To use native `async/await` on Node v7.x, run `micro` like the following.
```bash
node --harmony-async-await node_modules/.bin/micro .
```
### Deployment
You can use the `micro` CLI for `npm start`:
```json
{
"name": "my-microservice",
"dependencies": {
"micro": "x.y.z"
},
"main": "microservice.js",
"scripts": {
"start": "micro"
}
}
```
Then simply run `npm start`!
#### Port based on environment variable
When you want to set the port using an environment variable you can use:
```
micro -p $PORT
```
Optionally you can add a default if it suits your use case:
```
micro -p ${PORT:-3000}
```
`${PORT:-3000}` will allow a fallback to port `3000` when `$PORT` is not defined
## Contribute
## Contributing
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device
2. Link the package to the global module directory: `npm link`
@ -365,9 +338,10 @@ As always, you can run the [AVA](https://github.com/sindresorhus/ava) and [ESLin
## Credits
Thanks Tom Yandell and Richard Hodgson for donating the `micro` npm name.
Thanks to Tom Yandell and Richard Hodgson for donating the name "micro" on [npm](https://www.npmjs.com)!
## Authors
- Guillermo Rauch ([@rauchg](https://twitter.com/rauchg)) - [▲ZEIT](https://zeit.co)
- Leo Lamprecht ([@notquiteleo](https://twitter.com/notquiteleo)) - [▲ZEIT](https://zeit.co)
- Tim Neutkens ([@timneutkens](https://twitter.com/timneutkens))

2
test/development.js

@ -4,7 +4,7 @@ const request = require('request-promise')
const listen = require('test-listen')
process.env.NODE_ENV = 'development'
const micro = require('../lib/server')
const micro = require('../')
const getUrl = fn => {
const srv = micro(fn)

2
test/index.js

@ -4,7 +4,7 @@ const request = require('request-promise')
const sleep = require('then-sleep')
const resumer = require('resumer')
const listen = require('test-listen')
const micro = require('../lib/server')
const micro = require('../')
const { send, sendError, buffer, json } = micro

2
test/production.js

@ -4,7 +4,7 @@ const request = require('request-promise')
const listen = require('test-listen')
process.env.NODE_ENV = 'production'
const micro = require('../lib/server')
const micro = require('../')
const getUrl = fn => {
const srv = micro(fn)

34
test/testing.js

@ -1,34 +0,0 @@
// Packages
const test = require('ava')
const request = require('request-promise')
const listen = require('test-listen')
process.env.NODE_ENV = 'test'
const micro = require('../lib/server')
const getUrl = fn => {
const srv = micro(fn)
return listen(srv)
}
test.serial('errors are not printed in console in testing', async t => {
let logged = false
const _error = console.error
console.error = () => {
logged = true
}
const fn = () => {
throw new Error('Bang')
}
const url = await getUrl(fn)
try {
await request(url)
} catch (err) {
t.false(logged)
t.deepEqual(err.statusCode, 500)
console.error = _error
}
})
Loading…
Cancel
Save