├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .npmrc ├── .yarnrc ├── LICENSE ├── README.md ├── errors ├── invalid-entry.md ├── invalid-package-json.md ├── invalid-port-socket.md ├── invalid-server-port.md ├── invalid-socket.md ├── no-export.md ├── path-missing.md └── path-not-existent.md ├── examples ├── external-api-call │ ├── README.md │ ├── index.js │ └── package.json ├── json-body-parsing │ ├── README.md │ ├── index.js │ └── package.json ├── socket.io-chat-app │ ├── README.md │ ├── index.html │ ├── index.js │ ├── package.json │ └── websocket-server.js ├── urlencoded-body-parsing │ ├── README.md │ ├── index.js │ └── package.json ├── with-graphql-request │ ├── README.md │ ├── index.js │ └── package.json └── with-https │ ├── README.md │ ├── index.js │ └── package.json ├── lerna.json ├── package.json ├── packages └── micro │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ ├── bin │ │ └── micro.ts │ └── lib │ │ ├── error.ts │ │ ├── handler.ts │ │ ├── index.ts │ │ └── parse-endpoint.ts │ ├── tsconfig.json │ └── types │ └── src │ ├── bin │ ├── micro.d.ts │ └── micro.d.ts.map │ └── lib │ ├── error.d.ts │ ├── error.d.ts.map │ ├── handler.d.ts │ ├── handler.d.ts.map │ ├── index.d.ts │ ├── index.d.ts.map │ ├── parse-endpoint.d.ts │ └── parse-endpoint.d.ts.map ├── test ├── .eslintrc.js ├── package.json ├── suite │ ├── handler.ts │ ├── index.ts │ └── parse-endpoint.ts └── tsconfig.json └── yarn.lock /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: 12 | - 16 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: Install dependencies 19 | run: yarn 20 | - name: Run tests 21 | run: yarn run test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # logs 5 | npm-debug.log 6 | yarn-error.log 7 | 8 | # coverage 9 | coverage 10 | .nyc_output 11 | 12 | # build 13 | dist/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix "" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/micro/README.md -------------------------------------------------------------------------------- /errors/invalid-entry.md: -------------------------------------------------------------------------------- 1 | # Invalid Entry File 2 | 3 | #### Why This Error Occurred 4 | 5 | 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. 6 | 7 | #### Possible Ways to Fix It 8 | 9 | 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. 10 | 11 | ### Useful Links 12 | 13 | - [JSLint](http://www.jslint.com) - Validate the code of your entry file 14 | -------------------------------------------------------------------------------- /errors/invalid-package-json.md: -------------------------------------------------------------------------------- 1 | # Invalid `package.json` File 2 | 3 | #### Why This Error Occurred 4 | 5 | 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. 6 | 7 | #### Possible Ways to Fix It 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /errors/invalid-port-socket.md: -------------------------------------------------------------------------------- 1 | # Port and socket provided 2 | 3 | #### Why This Error Occurred 4 | 5 | When the `micro` command was ran, you passed both a port and a socket. Node.js can only listen to either a port or a socket, not both. 6 | 7 | #### Possible Ways to Fix It 8 | 9 | Only provide one of the arguments. If both are needed you can start 2 instances of micro with different arguments. 10 | -------------------------------------------------------------------------------- /errors/invalid-server-port.md: -------------------------------------------------------------------------------- 1 | # Invalid Server Port 2 | 3 | #### Why This Error Occurred 4 | 5 | When the `micro` command was ran, you supplied the port flag although it is 6 | not a valid number. 7 | 8 | 9 | #### Possible Ways to Fix It 10 | 11 | The port must be a valid number between 1 and 65535. Although, remember some are 12 | reserved to the operating system and others not in userland (only accessible 13 | with administrator access). 14 | -------------------------------------------------------------------------------- /errors/invalid-socket.md: -------------------------------------------------------------------------------- 1 | # Invalid socket 2 | 3 | #### Why This Error Occurred 4 | 5 | When the `micro` command was ran, you passed `-s` or `--socket` without a value. 6 | 7 | #### Possible Ways to Fix It 8 | 9 | Run `micro` with a value: 10 | 11 | ``` 12 | micro -s '/tmp/micro.sock' 13 | ``` 14 | -------------------------------------------------------------------------------- /errors/no-export.md: -------------------------------------------------------------------------------- 1 | # No Export 2 | 3 | #### Why This Error Occurred 4 | 5 | When `micro` tried to ran your microservice, it noticed that your code didn't export anything that could be run. 6 | 7 | #### Possible Ways to Fix It 8 | 9 | You need to ensure that the entry file you passed to the `micro` command contains an export - like this one: 10 | 11 | ```js 12 | module.exports = (req, res) => { 13 | res.end('test'); 14 | }; 15 | ``` 16 | 17 | ### Useful Links 18 | 19 | - [List of examples](https://github.com/vercel/micro/tree/master/examples) 20 | - [Usage information](https://github.com/vercel/micro#usage) 21 | -------------------------------------------------------------------------------- /errors/path-missing.md: -------------------------------------------------------------------------------- 1 | # Path Missing 2 | 3 | #### Why This Error Occurred 4 | 5 | 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. 6 | 7 | #### Possible Ways to Fix It 8 | 9 | - Enter the path to your microservice in the `main` property inside `package.json` 10 | - Specify the path of your entry file when running the command: `micro <path>` 11 | -------------------------------------------------------------------------------- /errors/path-not-existent.md: -------------------------------------------------------------------------------- 1 | # Path Not Existent 2 | 3 | #### Why This Error Occurred 4 | 5 | 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: 6 | 7 | ```bash 8 | micro <not-existing-path> 9 | ``` 10 | 11 | #### Possible Ways to Fix It 12 | 13 | The only way to fix this is to pass a path to a file or directory that exists and contains a working microservice. 14 | -------------------------------------------------------------------------------- /examples/external-api-call/README.md: -------------------------------------------------------------------------------- 1 | # External API call example 2 | 3 | ## How to use 4 | 5 | Download the example [or clone the repo](https://github.com/vercel/micro): 6 | 7 | ```bash 8 | curl https://codeload.github.com/vercel/micro/tar.gz/master | tar -xz --strip=2 micro-master/examples/external-api-call 9 | cd external-api-call 10 | ``` 11 | 12 | Install it and run: 13 | 14 | ```bash 15 | npm install 16 | npm run start 17 | ``` 18 | 19 | ## The idea behind the example 20 | 21 | Shows how to get data from an external api using async/await. 22 | -------------------------------------------------------------------------------- /examples/external-api-call/index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | module.exports = async () => { 4 | const response = await fetch('https://api.example.com'); 5 | const json = await response.json(); 6 | 7 | return json; 8 | }; 9 | -------------------------------------------------------------------------------- /examples/external-api-call/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "external-api-call", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "micro" 7 | }, 8 | "dependencies": { 9 | "micro": "latest", 10 | "node-fetch": "latest" 11 | }, 12 | "author": "", 13 | "license": "MIT" 14 | } 15 | -------------------------------------------------------------------------------- /examples/json-body-parsing/README.md: -------------------------------------------------------------------------------- 1 | # Parse JSON body example 2 | 3 | ## How to use 4 | 5 | Download the example [or clone the repo](https://github.com/vercel/micro): 6 | 7 | ```bash 8 | curl https://codeload.github.com/vercel/micro/tar.gz/master | tar -xz --strip=2 micro-master/examples/json-body-parsing 9 | cd json-body-parsing 10 | ``` 11 | 12 | Install it and run: 13 | 14 | ```bash 15 | npm install 16 | npm run start 17 | ``` 18 | 19 | ## The idea behind the example 20 | 21 | Shows how to get data posted to your microservice using async/await. 22 | -------------------------------------------------------------------------------- /examples/json-body-parsing/index.js: -------------------------------------------------------------------------------- 1 | const {json} = require('micro'); 2 | 3 | module.exports = async req => { 4 | const data = await json(req); 5 | console.log(data); 6 | 7 | return 'Data logged to your console'; 8 | }; 9 | -------------------------------------------------------------------------------- /examples/json-body-parsing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-body-parsing", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "micro" 7 | }, 8 | "dependencies": { 9 | "micro": "latest" 10 | }, 11 | "author": "", 12 | "license": "MIT" 13 | } 14 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/README.md: -------------------------------------------------------------------------------- 1 | # Chat app with socket.io 2 | 3 | ## How to use 4 | 5 | Download the example [or clone the repo](https://github.com/vercel/micro): 6 | 7 | ```bash 8 | curl https://codeload.github.com/vercel/micro/tar.gz/master | tar -xz --strip=2 micro-master/examples/socket.io-chat-app 9 | cd socket.io-chat-app 10 | ``` 11 | 12 | Install it and run: 13 | 14 | ```bash 15 | npm install 16 | npm run start 17 | ``` 18 | 19 | ## The idea behind the example 20 | 21 | Shows how to make use of socket.io with micro, to deploy on now. 22 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <title>Socket.IO Chat Example</title> 6 | <link rel="stylesheet" href="style.css"> 7 | </head> 8 | <body> 9 | <style> 10 | * { margin: 0; padding: 0; box-sizing: border-box; } 11 | body { font: 13px Helvetica, Arial; } 12 | form { background: grey; padding: 10px; position: fixed; bottom: 0; width: 100%; } 13 | form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; } 14 | form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; } 15 | #messages { list-style-type: none; margin: 0; padding: 0; } 16 | #messages li { padding: 5px 10px; } 17 | #messages li:nth-child(odd) { background: #eee; } 18 | </style> 19 | 20 | <ul id="messages"></ul> 21 | 22 | <form action=""> 23 | <input id="m" autocomplete="off" /><button>Send</button> 24 | </form> 25 | 26 | <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.3/socket.io.min.js"></script> 27 | 28 | <script> 29 | var socket = io() 30 | 31 | document.querySelector('form').addEventListener('submit', (e) => { 32 | e.preventDefault(); 33 | socket.emit('chat message', document.getElementById('m').value); 34 | document.getElementById('m').value = ''; 35 | }); 36 | 37 | socket.on('chat message', msg => { 38 | document.getElementById('messages').insertAdjacentHTML('beforeend', `<li>${msg}</li>`); 39 | }); 40 | </script> 41 | </body> 42 | </html> 43 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/index.js: -------------------------------------------------------------------------------- 1 | const micro = require('micro'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const document = path.join(__dirname, 'index.html'); 6 | const html = fs.readFileSync(document); 7 | 8 | const server = micro(async (req, res) => { 9 | console.log('Serving index.html'); 10 | res.end(html); 11 | }); 12 | 13 | const io = require('socket.io')(server); 14 | 15 | // socket-io handlers are in websocket-server.js 16 | require('./websocket-server.js')(io); 17 | 18 | server.listen(4000, () => console.log('Listening on localhost:4000')); 19 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatnow", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "node ." 7 | }, 8 | "author": "Lucas Kostka", 9 | "license": "ISC", 10 | "dependencies": { 11 | "micro": "latest", 12 | "socket.io": "1.7.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/socket.io-chat-app/websocket-server.js: -------------------------------------------------------------------------------- 1 | module.exports = function startServer(io) { 2 | io.on('connection', socket => { 3 | console.log('a user connected'); 4 | 5 | socket.on('disconnect', () => { 6 | console.log('user disconnected'); 7 | }); 8 | 9 | socket.on('chat message', msg => { 10 | console.log(`message: ${msg}`); 11 | io.emit('chat message', msg); 12 | }); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /examples/urlencoded-body-parsing/README.md: -------------------------------------------------------------------------------- 1 | # Parse JSON body example 2 | 3 | ## How to use 4 | 5 | Download the example [or clone the repo](https://github.com/vercel/micro): 6 | 7 | ```bash 8 | curl https://codeload.github.com/vercel/micro/tar.gz/master | tar -xz --strip=2 micro-master/examples/urlencoded-body-parsing 9 | cd urlencoded-body-parsing 10 | ``` 11 | 12 | Install it and run: 13 | 14 | ```bash 15 | npm install 16 | npm run start 17 | ``` 18 | 19 | ## The idea behind the example 20 | 21 | Shows how to get urlencoded (html form post) data posted to your microservice using async/await. 22 | -------------------------------------------------------------------------------- /examples/urlencoded-body-parsing/index.js: -------------------------------------------------------------------------------- 1 | const parse = require('urlencoded-body-parser'); 2 | 3 | module.exports = async req => { 4 | const data = await parse(req); 5 | console.log(data); 6 | 7 | return 'Data logged to your console'; 8 | }; 9 | -------------------------------------------------------------------------------- /examples/urlencoded-body-parsing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "urlencoded-body-parsing", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "micro" 7 | }, 8 | "dependencies": { 9 | "micro": "latest", 10 | "urlencoded-body-parser": "latest" 11 | }, 12 | "author": "", 13 | "license": "MIT" 14 | } 15 | -------------------------------------------------------------------------------- /examples/with-graphql-request/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Request example 2 | 3 | ## How to use 4 | 5 | Download the example [or clone the repo](https://github.com/vercel/micro): 6 | 7 | ```bash 8 | curl https://codeload.github.com/vercel/micro/tar.gz/master | tar -xz --strip=2 micro-master/examples/with-graphql-request 9 | cd with-graphql-request 10 | ``` 11 | 12 | Install it and run: 13 | 14 | ```bash 15 | $ yarn install # (or `$ npm install`) 16 | $ yarn run start # (or `$ npm run start`) 17 | ``` 18 | 19 | ## The idea behind the example 20 | 21 | Shows how to get data from a GraphQL endpoint using [GraphQL Request](https://github.com/graphcool/graphql-request). 22 | This example relies on [graph.cool](https://www.graph.cool) for its GraphQL backend. 23 | -------------------------------------------------------------------------------- /examples/with-graphql-request/index.js: -------------------------------------------------------------------------------- 1 | const {request} = require('graphql-request'); 2 | const endpoint = 'https://api.graph.cool/simple/v1/movies'; 3 | 4 | // Prepare simple query 5 | const query = ` 6 | query Movie($title: String!) { 7 | movie: Movie(title: $title) { 8 | releaseDate 9 | actors { 10 | name 11 | } 12 | } 13 | } 14 | `; 15 | 16 | module.exports = async () => { 17 | // Perform query 18 | const data = await request(endpoint, query, {title: 'Inception'}); 19 | 20 | // Return Movie 21 | return data.movie; 22 | }; 23 | -------------------------------------------------------------------------------- /examples/with-graphql-request/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-graphql-request", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "micro" 7 | }, 8 | "dependencies": { 9 | "graphql-request": "latest", 10 | "micro": "latest" 11 | }, 12 | "license": "MIT" 13 | } 14 | -------------------------------------------------------------------------------- /examples/with-https/README.md: -------------------------------------------------------------------------------- 1 | # Micro app with HTTPS 2 | 3 | ## How to use 4 | 5 | Download the example [or clone the repo](https://github.com/vercel/micro): 6 | 7 | ```bash 8 | curl https://codeload.github.com/vercel/micro/tar.gz/master | tar -xz --strip=2 micro-master/examples/with-https 9 | cd socket.io-chat-app 10 | ``` 11 | 12 | Install it and run: 13 | 14 | ```bash 15 | npm install 16 | npm run start 17 | ``` 18 | 19 | ## The idea behind the example 20 | 21 | Shows how to make use of HTTPS requests with micro. 22 | -------------------------------------------------------------------------------- /examples/with-https/index.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const {run, send} = require('micro'); 3 | 4 | const {key, cert, passphrase} = require('openssl-self-signed-certificate'); 5 | 6 | const PORT = process.env.PORT || 3443; 7 | 8 | const options = {key, cert, passphrase}; 9 | 10 | const microHttps = fn => https.createServer(options, (req, res) => run(req, res, fn)); 11 | 12 | const server = microHttps(async (req, res) => { 13 | send(res, 200, {encrypted: req.client.encrypted}); 14 | }); 15 | 16 | server.listen(PORT); 17 | console.log(`Listening on https://localhost:${PORT}`); 18 | -------------------------------------------------------------------------------- /examples/with-https/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-https", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node ." 8 | }, 9 | "dependencies": { 10 | "micro": "latest", 11 | "openssl-self-signed-certificate": "^1.1.6" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "useWorkspaces": true, 4 | "packages": [ 5 | "packages/*" 6 | ], 7 | "command": { 8 | "version": { 9 | "exact": true 10 | }, 11 | "publish": { 12 | "npmClient": "npm", 13 | "allowBranch": [ 14 | "master", 15 | "canary" 16 | ], 17 | "registry": "https://registry.npmjs.org/" 18 | } 19 | }, 20 | "version": "9.4.0" 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*", 5 | "test" 6 | ], 7 | "scripts": { 8 | "prepublish": "lerna run prepublish", 9 | "publish-canary": "lerna version prerelease --preid canary --force-publish && release --pre", 10 | "publish-stable": "lerna version --force-publish", 11 | "test": "cd test && yarn run test" 12 | }, 13 | "license": "MIT", 14 | "devDependencies": { 15 | "lerna": "^3.4.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/micro/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | require.resolve('@vercel/style-guide/eslint/node'), 5 | require.resolve('@vercel/style-guide/eslint/typescript'), 6 | ], 7 | parserOptions: { 8 | tsconfigRootDir: __dirname, 9 | project: ['./tsconfig.json'], 10 | }, 11 | ignorePatterns: ['dist/**', 'types/**'], 12 | }; 13 | -------------------------------------------------------------------------------- /packages/micro/README.md: -------------------------------------------------------------------------------- 1 | # Micro — Asynchronous HTTP microservices 2 | 3 | ## Features 4 | 5 | - **Easy**: Designed for usage with `async` and `await` 6 | - **Fast**: Ultra-high performance (even JSON parsing is opt-in) 7 | - **Micro**: The whole project is ~260 lines of code 8 | - **Agile**: Super easy deployment and containerization 9 | - **Simple**: Oriented for single purpose modules (function) 10 | - **Standard**: Just HTTP! 11 | - **Explicit**: No middleware - modules declare all [dependencies](https://github.com/amio/awesome-micro) 12 | - **Lightweight**: With all dependencies, the package weighs less than a megabyte 13 | 14 | **Disclaimer:** Micro was created for use within containers and is not intended for use in serverless environments. For those using Vercel, this means that there is no requirement to use Micro in your projects as the benefits it provides are not applicable to the platform. Utility features provided by Micro, such as `json`, are readily available in the form of [Serverless Function helpers](https://vercel.com/docs/runtimes#official-runtimes/node-js/node-js-request-and-response-objects). 15 | 16 | ## Installation 17 | 18 | **Important:** Micro is only meant to be used in production. In development, you should use [micro-dev](https://github.com/vercel/micro-dev), which provides you with a tool belt specifically tailored for developing microservices. 19 | 20 | To prepare your microservice for running in the production environment, firstly install `micro`: 21 | 22 | ```bash 23 | npm install --save micro 24 | ``` 25 | 26 | ## Usage 27 | 28 | Create an `index.js` file and export a function that accepts the 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: 29 | 30 | ```js 31 | module.exports = (req, res) => { 32 | res.end('Welcome to Micro'); 33 | }; 34 | ``` 35 | 36 | Micro provides [useful helpers](https://github.com/vercel/micro#body-parsing) but also handles return values – so you can write it even shorter! 37 | 38 | ```js 39 | module.exports = () => 'Welcome to Micro'; 40 | ``` 41 | 42 | 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: 43 | 44 | ```json 45 | { 46 | "main": "index.js", 47 | "scripts": { 48 | "start": "micro" 49 | } 50 | } 51 | ``` 52 | 53 | Once all of that is done, the server can be started like this: 54 | 55 | ```bash 56 | npm start 57 | ``` 58 | 59 | And go to this URL: `http://localhost:3000` - 🎉 60 | 61 | ### Command line 62 | 63 | ``` 64 | micro - Asynchronous HTTP microservices 65 | 66 | USAGE 67 | 68 | $ micro --help 69 | $ micro --version 70 | $ micro [-l listen_uri [-l ...]] [entry_point.js] 71 | 72 | By default micro will listen on 0.0.0.0:3000 and will look first 73 | for the "main" property in package.json and subsequently for index.js 74 | as the default entry_point. 75 | 76 | Specifying a single --listen argument will overwrite the default, not supplement it. 77 | 78 | OPTIONS 79 | 80 | --help shows this help message 81 | 82 | -v, --version displays the current version of micro 83 | 84 | -l, --listen listen_uri specify a URI endpoint on which to listen (see below) - 85 | more than one may be specified to listen in multiple places 86 | 87 | ENDPOINTS 88 | 89 | Listen endpoints (specified by the --listen or -l options above) instruct micro 90 | to listen on one or more interfaces/ports, UNIX domain sockets, or Windows named pipes. 91 | 92 | For TCP (traditional host/port) endpoints: 93 | 94 | $ micro -l tcp://hostname:1234 95 | 96 | For UNIX domain socket endpoints: 97 | 98 | $ micro -l unix:/path/to/socket.sock 99 | 100 | For Windows named pipe endpoints: 101 | 102 | $ micro -l pipe:\\.\pipe\PipeName 103 | ``` 104 | 105 | ### `async` & `await` 106 | 107 | <p><details> 108 | <summary><b>Examples</b></summary> 109 | <ul><li><a href="./examples/external-api-call">Fetch external api</a></li></ul> 110 | </details></p> 111 | 112 | Micro is built for usage with async/await. 113 | 114 | ```js 115 | const sleep = require('then-sleep'); 116 | 117 | module.exports = async (req, res) => { 118 | await sleep(500); 119 | return 'Ready!'; 120 | }; 121 | ``` 122 | 123 | ### Port Based on Environment Variable 124 | 125 | When you want to set the port using an environment variable you can use: 126 | 127 | ``` 128 | micro -l tcp://0.0.0.0:$PORT 129 | ``` 130 | 131 | Optionally you can add a default if it suits your use case: 132 | 133 | ``` 134 | micro -l tcp://0.0.0.0:${PORT-3000} 135 | ``` 136 | 137 | `${PORT-3000}` will allow a fallback to port `3000` when `$PORT` is not defined. 138 | 139 | Note that this only works in Bash. 140 | 141 | ### Body parsing 142 | 143 | <p id="body-parsing-examples"><details> 144 | <summary><b>Examples</b></summary> 145 | <ul> 146 | <li><a href="./examples/json-body-parsing">Parse JSON</a></li> 147 | <li><a href="./examples/urlencoded-body-parsing">Parse urlencoded form (html `form` tag)</a></li> 148 | </ul> 149 | </details></p> 150 | 151 | For parsing the incoming request body we included an async functions `buffer`, `text` and `json` 152 | 153 | ```js 154 | const { buffer, text, json } = require('micro'); 155 | 156 | module.exports = async (req, res) => { 157 | const buf = await buffer(req); 158 | console.log(buf); 159 | // <Buffer 7b 22 70 72 69 63 65 22 3a 20 39 2e 39 39 7d> 160 | const txt = await text(req); 161 | console.log(txt); 162 | // '{"price": 9.99}' 163 | const js = await json(req); 164 | console.log(js.price); 165 | // 9.99 166 | return ''; 167 | }; 168 | ``` 169 | 170 | ### API 171 | 172 | ##### `buffer(req, { limit = '1mb', encoding = 'utf8' })` 173 | 174 | ##### `text(req, { limit = '1mb', encoding = 'utf8' })` 175 | 176 | ##### `json(req, { limit = '1mb', encoding = 'utf8' })` 177 | 178 | - Buffers and parses the incoming body and returns it. 179 | - Exposes an `async` function that can be run with `await`. 180 | - Can be called multiple times, as it caches the raw request body the first time. 181 | - `limit` is how much data is aggregated before parsing at max. Otherwise, an `Error` is thrown with `statusCode` set to `413` (see [Error Handling](#error-handling)). It can be a `Number` of bytes or [a string](https://www.npmjs.com/package/bytes) like `'1mb'`. 182 | - If JSON parsing fails, an `Error` is thrown with `statusCode` set to `400` (see [Error Handling](#error-handling)) 183 | 184 | For other types of data check the [examples](#body-parsing-examples) 185 | 186 | ### Sending a different status code 187 | 188 | So far we have used `return` to send data to the client. `return 'Hello World'` is the equivalent of `send(res, 200, 'Hello World')`. 189 | 190 | ```js 191 | const { send } = require('micro'); 192 | 193 | module.exports = async (req, res) => { 194 | const statusCode = 400; 195 | const data = { error: 'Custom error message' }; 196 | 197 | send(res, statusCode, data); 198 | }; 199 | ``` 200 | 201 | ##### `send(res, statusCode, data = null)` 202 | 203 | - Use `require('micro').send`. 204 | - `statusCode` is a `Number` with the HTTP status code, and must always be supplied. 205 | - If `data` is supplied it is sent in the response. Different input types are processed appropriately, and `Content-Type` and `Content-Length` are automatically set. 206 | - `Stream`: `data` is piped as an `octet-stream`. Note: it is _your_ responsibility to handle the `error` event in this case (usually, simply logging the error and aborting the response is enough). 207 | - `Buffer`: `data` is written as an `octet-stream`. 208 | - `object`: `data` is serialized as JSON. 209 | - `string`: `data` is written as-is. 210 | - If JSON serialization fails (for example, if a cyclical reference is found), a `400` error is thrown. See [Error Handling](#error-handling). 211 | 212 | ### Programmatic use 213 | 214 | You can use Micro programmatically by requiring Micro directly: 215 | 216 | ```js 217 | const http = require('http'); 218 | const sleep = require('then-sleep'); 219 | const { serve } = require('micro'); 220 | 221 | const server = new http.Server( 222 | serve(async (req, res) => { 223 | await sleep(500); 224 | return 'Hello world'; 225 | }), 226 | ); 227 | 228 | server.listen(3000); 229 | ``` 230 | 231 | ##### serve(fn) 232 | 233 | - Use `require('micro').serve`. 234 | - Returns a function with the `(req, res) => void` signature. That uses the provided `function` as the request handler. 235 | - The supplied function is run with `await`. So it can be `async` 236 | 237 | ##### sendError(req, res, error) 238 | 239 | - Use `require('micro').sendError`. 240 | - Used as the default handler for errors thrown. 241 | - Automatically sets the status code of the response based on `error.statusCode`. 242 | - Sends the `error.message` as the body. 243 | - Stacks are printed out with `console.error` and during development (when `NODE_ENV` is set to `'development'`) also sent in responses. 244 | - Usually, you don't need to invoke this method yourself, as you can use the [built-in error handling](#error-handling) flow with `throw`. 245 | 246 | ##### createError(code, msg, orig) 247 | 248 | - Use `require('micro').createError`. 249 | - Creates an error object with a `statusCode`. 250 | - Useful for easily throwing errors with HTTP status codes, which are interpreted by the [built-in error handling](#error-handling). 251 | - `orig` sets `error.originalError` which identifies the original error (if any). 252 | 253 | ## Error Handling 254 | 255 | Micro allows you to write robust microservices. This is accomplished primarily by bringing sanity back to error handling and avoiding callback soup. 256 | 257 | If an error is thrown and not caught by you, the response will automatically be `500`. **Important:** Error stacks will be printed as `console.error` and during development mode (if the env variable `NODE_ENV` is `'development'`), they will also be included in the responses. 258 | 259 | If the `Error` object that's thrown contains a `statusCode` property, that's used as the HTTP code to be sent. Let's say you want to write a rate limiting module: 260 | 261 | ```js 262 | const rateLimit = require('my-rate-limit'); 263 | 264 | module.exports = async (req, res) => { 265 | await rateLimit(req); 266 | // ... your code 267 | }; 268 | ``` 269 | 270 | If the API endpoint is abused, it can throw an error with `createError` like so: 271 | 272 | ```js 273 | if (tooMany) { 274 | throw createError(429, 'Rate limit exceeded'); 275 | } 276 | ``` 277 | 278 | Alternatively you can create the `Error` object yourself 279 | 280 | ```js 281 | if (tooMany) { 282 | const err = new Error('Rate limit exceeded'); 283 | err.statusCode = 429; 284 | throw err; 285 | } 286 | ``` 287 | 288 | The nice thing about this model is that the `statusCode` is merely a suggestion. The user can override it: 289 | 290 | ```js 291 | try { 292 | await rateLimit(req); 293 | } catch (err) { 294 | if (429 == err.statusCode) { 295 | // perhaps send 500 instead? 296 | send(res, 500); 297 | } 298 | } 299 | ``` 300 | 301 | 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`. 302 | 303 | In order to set up your own error handling mechanism, you can use composition in your handler: 304 | 305 | ```js 306 | const { send } = require('micro'); 307 | 308 | const handleErrors = (fn) => async (req, res) => { 309 | try { 310 | return await fn(req, res); 311 | } catch (err) { 312 | console.log(err.stack); 313 | send(res, 500, 'My custom error!'); 314 | } 315 | }; 316 | 317 | module.exports = handleErrors(async (req, res) => { 318 | throw new Error('What happened here?'); 319 | }); 320 | ``` 321 | 322 | ## Testing 323 | 324 | Micro makes tests compact and a pleasure to read and write. 325 | We recommend [Node TAP](https://node-tap.org/) or [AVA](https://github.com/avajs/ava), a highly parallel test framework with built-in support for async tests: 326 | 327 | ```js 328 | const http = require('http'); 329 | const { send, serve } = require('micro'); 330 | const test = require('ava'); 331 | const listen = require('test-listen'); 332 | const fetch = require('node-fetch'); 333 | 334 | test('my endpoint', async (t) => { 335 | const service = new http.Server( 336 | serve(async (req, res) => { 337 | send(res, 200, { 338 | test: 'woot', 339 | }); 340 | }), 341 | ); 342 | 343 | const url = await listen(service); 344 | const response = await fetch(url); 345 | const body = await response.json(); 346 | 347 | t.deepEqual(body.test, 'woot'); 348 | service.close(); 349 | }); 350 | ``` 351 | 352 | Look at [test-listen](https://github.com/vercel/test-listen) for a 353 | function that returns a URL with an ephemeral port every time it's called. 354 | 355 | ## Contributing 356 | 357 | 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 358 | 2. Link the package to the global module directory: `npm link` 359 | 3. Within the module you want to test your local development instance of Micro, just link it to the dependencies: `npm link micro`. Instead of the default one from npm, node will now use your clone of Micro! 360 | 361 | You can run the tests using: `npm test`. 362 | 363 | ## Credits 364 | 365 | Thanks to Tom Yandell and Richard Hodgson for donating the name "micro" on [npm](https://www.npmjs.com)! 366 | 367 | ## Authors 368 | 369 | - Guillermo Rauch ([@rauchg](https://x.com/rauchg)) - [Vercel](https://vercel.com) 370 | - Leo Lamprecht ([@leo](https://x.com/leo)) - [Vercel](https://vercel.com) 371 | - Tim Neutkens ([@timneutkens](https://x.com/timneutkens)) - [Vercel](https://vercel.com) 372 | -------------------------------------------------------------------------------- /packages/micro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micro", 3 | "version": "10.0.1", 4 | "description": "Asynchronous HTTP microservices", 5 | "license": "MIT", 6 | "main": "./dist/src/lib/index.js", 7 | "types": "./types/src/lib", 8 | "files": [ 9 | "src", 10 | "dist", 11 | "types" 12 | ], 13 | "bin": { 14 | "micro": "./dist/src/bin/micro.js" 15 | }, 16 | "engines": { 17 | "node": ">= 16.0.0" 18 | }, 19 | "scripts": { 20 | "build": "tsc", 21 | "prepublishOnly": "yarn run build", 22 | "eslint-check": "eslint --max-warnings=0 .", 23 | "prettier-check": "prettier --check .", 24 | "type-check": "tsc --noEmit" 25 | }, 26 | "repository": "vercel/micro", 27 | "keywords": [ 28 | "micro", 29 | "service", 30 | "microservice", 31 | "serverless", 32 | "API" 33 | ], 34 | "dependencies": { 35 | "arg": "4.1.0", 36 | "content-type": "1.0.4", 37 | "raw-body": "2.4.1" 38 | }, 39 | "devDependencies": { 40 | "@types/content-type": "1.1.5", 41 | "@types/node": "18.0.3", 42 | "@vercel/style-guide": "3.0.0", 43 | "eslint": "8.19.0", 44 | "prettier": "2.7.1", 45 | "typescript": "4.7.4" 46 | }, 47 | "prettier": "@vercel/style-guide/prettier" 48 | } 49 | -------------------------------------------------------------------------------- /packages/micro/src/bin/micro.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable eslint-comments/disable-enable-pair */ 3 | /* eslint-disable no-console */ 4 | 5 | // Native 6 | import Module from 'module'; 7 | import http from 'http'; 8 | import path from 'path'; 9 | import { existsSync } from 'fs'; 10 | // Packages 11 | import arg from 'arg'; 12 | // Utilities 13 | import { serve } from '../lib'; 14 | import { handle } from '../lib/handler'; 15 | import { version } from '../../package.json'; 16 | import { logError } from '../lib/error'; 17 | import { parseEndpoint } from '../lib/parse-endpoint'; 18 | import type { AddressInfo } from 'net'; 19 | import type { RequestHandler } from '../lib'; 20 | 21 | // Check if the user defined any options 22 | const args = arg({ 23 | '--listen': parseEndpoint, 24 | '-l': '--listen', 25 | '--help': Boolean, 26 | '--version': Boolean, 27 | '-v': '--version', 28 | }); 29 | 30 | // When `-h` or `--help` are used, print out 31 | // the usage information 32 | if (args['--help']) { 33 | console.error(` 34 | micro - Asynchronous HTTP microservices 35 | 36 | USAGE 37 | 38 | $ micro --help 39 | $ micro --version 40 | $ micro [-l listen_uri [-l ...]] [entry_point.js] 41 | 42 | By default micro will listen on 0.0.0.0:3000 and will look first 43 | for the "main" property in package.json and subsequently for index.js 44 | as the default entry_point. 45 | 46 | Specifying a single --listen argument will overwrite the default, not supplement it. 47 | 48 | OPTIONS 49 | 50 | --help shows this help message 51 | 52 | -v, --version displays the current version of micro 53 | 54 | -l, --listen listen_uri specify a URI endpoint on which to listen (see below) - 55 | more than one may be specified to listen in multiple places 56 | 57 | ENDPOINTS 58 | 59 | Listen endpoints (specified by the --listen or -l options above) instruct micro 60 | to listen on one or more interfaces/ports, UNIX domain sockets, or Windows named pipes. 61 | 62 | For TCP (traditional host/port) endpoints: 63 | 64 | $ micro -l tcp://hostname:1234 65 | 66 | For UNIX domain socket endpoints: 67 | 68 | $ micro -l unix:/path/to/socket.sock 69 | 70 | For Windows named pipe endpoints: 71 | 72 | $ micro -l pipe:\\\\.\\pipe\\PipeName 73 | `); 74 | process.exit(2); 75 | } 76 | 77 | // Print out the package's version when 78 | // `--version` or `-v` are used 79 | if (args['--version']) { 80 | console.log(version); 81 | process.exit(); 82 | } 83 | 84 | if (!args['--listen']) { 85 | // default endpoint 86 | args['--listen'] = [String(3000)]; 87 | } 88 | 89 | let file = args._[0]; 90 | 91 | if (!file) { 92 | try { 93 | const req = Module.createRequire(module.filename); 94 | const packageJson: unknown = req( 95 | path.resolve(process.cwd(), 'package.json'), 96 | ); 97 | if (hasMain(packageJson)) { 98 | file = packageJson.main; 99 | } else { 100 | file = 'index.js'; 101 | } 102 | } catch (err) { 103 | if (isNodeError(err) && err.code !== 'MODULE_NOT_FOUND') { 104 | logError( 105 | `Could not read \`package.json\`: ${err.message}`, 106 | 'invalid-package-json', 107 | ); 108 | process.exit(1); 109 | } 110 | } 111 | } 112 | 113 | if (!file) { 114 | logError('Please supply a file!', 'path-missing'); 115 | process.exit(1); 116 | } 117 | 118 | if (!file.startsWith('/')) { 119 | file = path.resolve(process.cwd(), file); 120 | } 121 | 122 | if (!existsSync(file)) { 123 | logError( 124 | `The file or directory "${path.basename(file)}" doesn't exist!`, 125 | 'path-not-existent', 126 | ); 127 | process.exit(1); 128 | } 129 | 130 | function registerShutdown(fn: () => void) { 131 | let run = false; 132 | 133 | const wrapper = () => { 134 | if (!run) { 135 | run = true; 136 | fn(); 137 | } 138 | }; 139 | 140 | process.on('SIGINT', wrapper); 141 | process.on('SIGTERM', wrapper); 142 | process.on('exit', wrapper); 143 | } 144 | 145 | function startEndpoint(module: RequestHandler, endpoint: string) { 146 | const server = new http.Server(serve(module)); 147 | 148 | server.on('error', (err) => { 149 | console.error('micro:', err.stack); 150 | process.exit(1); 151 | }); 152 | 153 | server.listen(endpoint, () => { 154 | const details = server.address(); 155 | registerShutdown(() => { 156 | console.log('micro: Gracefully shutting down. Please wait...'); 157 | server.close(); 158 | process.exit(); 159 | }); 160 | 161 | // `micro` is designed to run only in production, so 162 | // this message is perfect for prod 163 | if (typeof details === 'string') { 164 | console.log(`micro: Accepting connections on ${details}`); 165 | } else if (isAddressInfo(details)) { 166 | console.log(`micro: Accepting connections on port ${details.port}`); 167 | } else { 168 | console.log('micro: Accepting connections'); 169 | } 170 | }); 171 | } 172 | 173 | async function start() { 174 | if (file && args['--listen']) { 175 | const loadedModule = await handle(file); 176 | 177 | for (const endpoint of args['--listen']) { 178 | startEndpoint(loadedModule as RequestHandler, endpoint); 179 | } 180 | } 181 | } 182 | 183 | start() 184 | .then() 185 | .catch((error) => { 186 | if (error instanceof Error) { 187 | logError(error.message, 'STARTUP_FAILURE'); 188 | } 189 | process.exit(1); 190 | }); 191 | 192 | function hasMain(packageJson: unknown): packageJson is { main: string } { 193 | return ( 194 | typeof packageJson === 'object' && 195 | packageJson !== null && 196 | 'main' in packageJson 197 | ); 198 | } 199 | 200 | function isNodeError( 201 | error: unknown, 202 | ): error is { code: string; message: string } { 203 | return error instanceof Error && 'code' in error; 204 | } 205 | 206 | function isAddressInfo(obj: unknown): obj is AddressInfo { 207 | return 'port' in (obj as AddressInfo); 208 | } 209 | -------------------------------------------------------------------------------- /packages/micro/src/lib/error.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line eslint-comments/disable-enable-pair 2 | /* eslint-disable no-console */ 3 | export function logError(message: string, errorCode: string) { 4 | console.error(`micro: ${message}`); 5 | console.error(`micro: https://err.sh/micro/${errorCode}`); 6 | } 7 | -------------------------------------------------------------------------------- /packages/micro/src/lib/handler.ts: -------------------------------------------------------------------------------- 1 | // Utilities 2 | import { logError } from './error'; 3 | 4 | export const handle = async (file: string) => { 5 | let mod: unknown; 6 | 7 | try { 8 | mod = await import(file); 9 | 10 | mod = await (mod as { default: unknown }).default; // use ES6 module's default export 11 | } catch (err: unknown) { 12 | if (isErrorObject(err) && err.stack) { 13 | logError(`Error when importing ${file}: ${err.stack}`, 'invalid-entry'); 14 | } 15 | process.exit(1); 16 | } 17 | 18 | if (typeof mod !== 'function') { 19 | logError(`The file "${file}" does not export a function.`, 'no-export'); 20 | process.exit(1); 21 | } 22 | 23 | return mod; 24 | }; 25 | 26 | function isErrorObject(error: unknown): error is Error { 27 | return (error as Error).stack !== undefined; 28 | } 29 | -------------------------------------------------------------------------------- /packages/micro/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // Native 2 | import { Stream, Readable } from 'stream'; 3 | // Packages 4 | import contentType from 'content-type'; 5 | import getRawBody from 'raw-body'; 6 | import type { RawBodyError } from 'raw-body'; 7 | //Types 8 | import type { IncomingMessage, ServerResponse, RequestListener } from 'http'; 9 | 10 | // slight modification of is-stream https://github.com/sindresorhus/is-stream/blob/c918e3795ea2451b5265f331a00fb6a8aaa27816/license 11 | function isStream(stream: unknown): stream is Stream { 12 | return ( 13 | stream !== null && 14 | typeof stream === 'object' && 15 | stream instanceof Stream && 16 | typeof stream.pipe === 'function' 17 | ); 18 | } 19 | 20 | function readable(stream: unknown): stream is Readable { 21 | return ( 22 | isStream(stream) && // TODO: maybe this isn't needed because we could use only the checks below 23 | stream instanceof Readable && 24 | stream.readable 25 | ); 26 | } 27 | 28 | export type RequestHandler = ( 29 | req: IncomingMessage, 30 | res: ServerResponse, 31 | ) => unknown; 32 | 33 | type Serve = (fn: RequestHandler) => RequestListener; 34 | 35 | export const serve: Serve = (fn) => (req, res) => run(req, res, fn); 36 | 37 | export class HttpError extends Error { 38 | constructor(message: string) { 39 | super(message); 40 | Object.setPrototypeOf(this, HttpError.prototype); 41 | } 42 | 43 | statusCode?: number; 44 | originalError?: Error; 45 | } 46 | 47 | function isError(error: unknown): error is Error | HttpError { 48 | return error instanceof Error || error instanceof HttpError; 49 | } 50 | 51 | export const createError = (code: number, message: string, original: Error) => { 52 | const err = new HttpError(message); 53 | 54 | err.statusCode = code; 55 | err.originalError = original; 56 | 57 | return err; 58 | }; 59 | 60 | export const send = ( 61 | res: ServerResponse, 62 | code: number, 63 | obj: unknown = null, 64 | ) => { 65 | res.statusCode = code; 66 | 67 | if (obj === null) { 68 | res.end(); 69 | return; 70 | } 71 | 72 | if (Buffer.isBuffer(obj)) { 73 | if (!res.getHeader('Content-Type')) { 74 | res.setHeader('Content-Type', 'application/octet-stream'); 75 | } 76 | 77 | res.setHeader('Content-Length', obj.length); 78 | res.end(obj); 79 | return; 80 | } 81 | 82 | if (obj instanceof Stream || readable(obj)) { 83 | //TODO: Wouldn't (obj instanceof Stream) be the only check here? Do we specifically need a Readable stream or a Stream object that's not of NodeJS Stream? 84 | if (!res.getHeader('Content-Type')) { 85 | res.setHeader('Content-Type', 'application/octet-stream'); 86 | } 87 | 88 | obj.pipe(res); 89 | return; 90 | } 91 | 92 | let str = obj; 93 | 94 | if (typeof obj === 'object' || typeof obj === 'number') { 95 | // We stringify before setting the header 96 | // in case `JSON.stringify` throws and a 97 | // 500 has to be sent instead 98 | str = JSON.stringify(obj); 99 | 100 | if (!res.getHeader('Content-Type')) { 101 | res.setHeader('Content-Type', 'application/json; charset=utf-8'); 102 | } 103 | } 104 | 105 | if (typeof str === 'string') { 106 | res.setHeader('Content-Length', Buffer.byteLength(str)); 107 | } 108 | 109 | res.end(str); 110 | }; 111 | 112 | export const sendError = ( 113 | req: IncomingMessage, 114 | res: ServerResponse, 115 | errorObj: Error | HttpError, 116 | ) => { 117 | if ('statusCode' in errorObj && errorObj.statusCode) { 118 | send(res, errorObj.statusCode, errorObj.message); 119 | } else send(res, 500, 'Internal Server Error'); 120 | 121 | if (errorObj instanceof Error) { 122 | // eslint-disable-next-line no-console 123 | console.error(errorObj.stack); 124 | } else { 125 | // eslint-disable-next-line no-console 126 | console.warn('thrown error must be an instance Error'); 127 | } 128 | }; 129 | 130 | export const run = ( 131 | req: IncomingMessage, 132 | res: ServerResponse, 133 | fn: RequestHandler, 134 | ) => 135 | new Promise((resolve) => { 136 | resolve(fn(req, res)); 137 | }) 138 | .then((val) => { 139 | if (val === null) { 140 | send(res, 204, null); 141 | return; 142 | } 143 | 144 | // Send value if it is not undefined, otherwise assume res.end 145 | // will be called later 146 | if (val !== undefined) { 147 | send(res, res.statusCode || 200, val); 148 | } 149 | }) 150 | .catch((err: unknown) => { 151 | if (isError(err)) { 152 | sendError(req, res, err); 153 | } 154 | }); 155 | 156 | // Maps requests to buffered raw bodies so that 157 | // multiple calls to `json` work as expected 158 | const rawBodyMap = new WeakMap<IncomingMessage, Buffer>(); 159 | 160 | const parseJSON = (str: string): unknown => { 161 | try { 162 | return JSON.parse(str); 163 | } catch (err: unknown) { 164 | throw createError(400, 'Invalid JSON', err as Error); 165 | } 166 | }; 167 | 168 | export interface BufferInfo { 169 | limit?: string | number | undefined; 170 | encoding?: BufferEncoding; 171 | } 172 | 173 | function isRawBodyError(error: unknown): error is RawBodyError { 174 | return 'type' in (error as RawBodyError); 175 | } 176 | 177 | export const buffer = ( 178 | req: IncomingMessage, 179 | { limit = '1mb', encoding }: BufferInfo = {}, 180 | ) => 181 | Promise.resolve().then(() => { 182 | const type = req.headers['content-type'] || 'text/plain'; 183 | const length = req.headers['content-length']; 184 | 185 | const body = rawBodyMap.get(req); 186 | 187 | if (body) { 188 | return body; 189 | } 190 | 191 | return getRawBody(req, { 192 | limit, 193 | length, 194 | encoding: encoding ?? contentType.parse(type).parameters.charset, 195 | }) 196 | .then((buf) => { 197 | rawBodyMap.set(req, buf); 198 | return buf; 199 | }) 200 | .catch((err) => { 201 | if (isRawBodyError(err) && err.type === 'entity.too.large') { 202 | throw createError(413, `Body exceeded ${limit} limit`, err); 203 | } else { 204 | throw createError(400, 'Invalid body', err as Error); 205 | } 206 | }); 207 | }); 208 | 209 | export const text = ( 210 | req: IncomingMessage, 211 | { limit, encoding }: BufferInfo = {}, 212 | ) => buffer(req, { limit, encoding }).then((body) => body.toString(encoding)); 213 | 214 | export const json = (req: IncomingMessage, opts: BufferInfo = {}) => 215 | text(req, opts).then((body) => parseJSON(body)); 216 | -------------------------------------------------------------------------------- /packages/micro/src/lib/parse-endpoint.ts: -------------------------------------------------------------------------------- 1 | export function parseEndpoint(endpoint: string) { 2 | const url = new URL(endpoint); 3 | 4 | switch (url.protocol) { 5 | case 'pipe:': { 6 | // some special handling 7 | const cutStr = endpoint.replace(/^pipe:/, ''); 8 | if (!cutStr.startsWith('\\\\.\\')) { 9 | throw new Error(`Invalid Windows named pipe endpoint: ${endpoint}`); 10 | } 11 | return [cutStr]; 12 | } 13 | case 'unix:': 14 | if (!url.pathname) { 15 | throw new Error(`Invalid UNIX domain socket endpoint: ${endpoint}`); 16 | } 17 | return [url.pathname]; 18 | case 'tcp:': 19 | url.port = url.port || '3000'; 20 | return [parseInt(url.port, 10).toString(), url.hostname]; 21 | default: 22 | throw new Error( 23 | `Unknown --listen endpoint scheme (protocol): ${url.protocol}`, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/micro/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vercel/style-guide/typescript", 3 | "compilerOptions": { 4 | "target": "ES2021", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node16", 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "outDir": "dist", 10 | "declaration": true, 11 | "declarationDir": "./types", 12 | "declarationMap": true, 13 | "removeComments": true 14 | }, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/micro/types/src/bin/micro.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | export {}; 3 | //# sourceMappingURL=micro.d.ts.map -------------------------------------------------------------------------------- /packages/micro/types/src/bin/micro.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"micro.d.ts","sourceRoot":"","sources":["../../../src/bin/micro.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /packages/micro/types/src/lib/error.d.ts: -------------------------------------------------------------------------------- 1 | export declare function logError(message: string, errorCode: string): void; 2 | //# sourceMappingURL=error.d.ts.map -------------------------------------------------------------------------------- /packages/micro/types/src/lib/error.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../../src/lib/error.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAG1D"} -------------------------------------------------------------------------------- /packages/micro/types/src/lib/handler.d.ts: -------------------------------------------------------------------------------- 1 | export declare const handle: (file: string) => Promise<Function>; 2 | //# sourceMappingURL=handler.d.ts.map -------------------------------------------------------------------------------- /packages/micro/types/src/lib/handler.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/lib/handler.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,MAAM,SAAgB,MAAM,sBAoBxC,CAAC"} -------------------------------------------------------------------------------- /packages/micro/types/src/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="node" /> 2 | /// <reference types="node" /> 3 | import type { IncomingMessage, ServerResponse, RequestListener } from 'http'; 4 | export declare type RequestHandler = (req: IncomingMessage, res: ServerResponse) => unknown; 5 | declare type Serve = (fn: RequestHandler) => RequestListener; 6 | export declare const serve: Serve; 7 | export declare class HttpError extends Error { 8 | constructor(message: string); 9 | statusCode?: number; 10 | originalError?: Error; 11 | } 12 | export declare const createError: (code: number, message: string, original: Error) => HttpError; 13 | export declare const send: (res: ServerResponse, code: number, obj?: unknown) => void; 14 | export declare const sendError: (req: IncomingMessage, res: ServerResponse, errorObj: Error | HttpError) => void; 15 | export declare const run: (req: IncomingMessage, res: ServerResponse, fn: RequestHandler) => Promise<void>; 16 | export interface BufferInfo { 17 | limit?: string | number | undefined; 18 | encoding?: BufferEncoding; 19 | } 20 | export declare const buffer: (req: IncomingMessage, { limit, encoding }?: BufferInfo) => Promise<Buffer>; 21 | export declare const text: (req: IncomingMessage, { limit, encoding }?: BufferInfo) => Promise<string>; 22 | export declare const json: (req: IncomingMessage, opts?: BufferInfo) => Promise<unknown>; 23 | export {}; 24 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /packages/micro/types/src/lib/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/index.ts"],"names":[],"mappings":";;AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAoB7E,oBAAY,cAAc,GAAG,CAC3B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,KAChB,OAAO,CAAC;AAEb,aAAK,KAAK,GAAG,CAAC,EAAE,EAAE,cAAc,KAAK,eAAe,CAAC;AAErD,eAAO,MAAM,KAAK,EAAE,KAA+C,CAAC;AAEpE,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;IAK3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,KAAK,CAAC;CACvB;AAMD,eAAO,MAAM,WAAW,SAAU,MAAM,WAAW,MAAM,YAAY,KAAK,cAOzE,CAAC;AAEF,eAAO,MAAM,IAAI,QACV,cAAc,QACb,MAAM,QACP,OAAO,SA+Cb,CAAC;AAEF,eAAO,MAAM,SAAS,QACf,eAAe,OACf,cAAc,YACT,KAAK,GAAG,SAAS,SAa5B,CAAC;AAEF,eAAO,MAAM,GAAG,QACT,eAAe,OACf,cAAc,MACf,cAAc,kBAqBd,CAAC;AAcP,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAMD,eAAO,MAAM,MAAM,QACZ,eAAe,wBACS,UAAU,oBA4BrC,CAAC;AAEL,eAAO,MAAM,IAAI,QACV,eAAe,wBACC,UAAU,oBAC4C,CAAC;AAE9E,eAAO,MAAM,IAAI,QAAS,eAAe,SAAQ,UAAU,qBACV,CAAC"} -------------------------------------------------------------------------------- /packages/micro/types/src/lib/parse-endpoint.d.ts: -------------------------------------------------------------------------------- 1 | export declare function parseEndpoint(endpoint: string): string[]; 2 | //# sourceMappingURL=parse-endpoint.d.ts.map -------------------------------------------------------------------------------- /packages/micro/types/src/lib/parse-endpoint.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"parse-endpoint.d.ts","sourceRoot":"","sources":["../../../src/lib/parse-endpoint.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,YAyB7C"} -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | require.resolve('@vercel/style-guide/eslint/node'), 5 | require.resolve('@vercel/style-guide/eslint/typescript'), 6 | ], 7 | parserOptions: { 8 | tsconfigRootDir: __dirname, 9 | project: ['./tsconfig.json'], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "engines": { 5 | "node": ">= 16.0.0" 6 | }, 7 | "scripts": { 8 | "eslint-check": "eslint --max-warnings=0 .", 9 | "prettier-check": "prettier --check .", 10 | "type-check": "tsc --noEmit", 11 | "test": "tap --ts suite/" 12 | }, 13 | "devDependencies": { 14 | "@types/node-fetch": "2.6.2", 15 | "@types/sinon": "10.0.13", 16 | "@types/tap": "15.0.7", 17 | "@vercel/style-guide": "3.0.0", 18 | "eslint": "8.19.0", 19 | "node-fetch": "2.6.6", 20 | "prettier": "2.7.1", 21 | "sinon": "14.0.0", 22 | "tap": "16.3.0", 23 | "ts-node": "10.9.1", 24 | "typescript": "4.7.4", 25 | "micro": "*" 26 | }, 27 | "prettier": "@vercel/style-guide/prettier" 28 | } 29 | -------------------------------------------------------------------------------- /test/suite/handler.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap'; 2 | import { handle } from 'micro/src/lib/handler'; 3 | import { stub } from 'sinon'; 4 | 5 | void test('handle a non-async function', async (t) => { 6 | const dir = t.testdir({ 7 | 'regular-function-export.js': `module.exports = () => 'Test';`, 8 | }); 9 | 10 | const result = await handle(`${dir}/regular-function-export.js`); 11 | t.type(result, 'function'); 12 | }); 13 | 14 | void test('handle async function', async (t) => { 15 | const dir = t.testdir({ 16 | 'promise-export.js': `module.exports = async () => 'Test';`, 17 | }); 18 | 19 | const result = await handle(`${dir}/promise-export.js`); 20 | t.type(result, 'function'); 21 | }); 22 | 23 | void test(`handle ESM's non-async function`, async (t) => { 24 | const dir = t.testdir({ 25 | 'esm-function-export.mjs': `export default () => 'Hello ESM';`, 26 | }); 27 | 28 | const result = await handle(`${dir}/esm-function-export.mjs`); 29 | t.type(result, 'function'); 30 | }); 31 | 32 | void test(`handle ESM's async function`, async (t) => { 33 | const dir = t.testdir({ 34 | 'esm-async-export.mjs': `export default async () => 'Hello ESM';`, 35 | }); 36 | 37 | const result = await handle(`${dir}/esm-async-export.mjs`); 38 | t.type(result, 'function'); 39 | }); 40 | 41 | void test('process.exit when handling an invalid export', async (t) => { 42 | const dir = t.testdir({ 43 | 'regular-object.js': `module.exports = {};`, 44 | }); 45 | const processStub = stub(process, 'exit').callsFake(() => { 46 | throw new Error('Fake'); 47 | }); 48 | 49 | await t.rejects(handle(`${dir}/regular-object.js`), { message: 'Fake' }); 50 | t.equal(processStub.calledOnceWith(1), true); 51 | 52 | processStub.restore(); 53 | }); 54 | 55 | void test('process.exit when handling and inexisting file', async (t) => { 56 | const dir = t.testdir(); 57 | const processStub = stub(process, 'exit').callsFake(() => { 58 | throw new Error('Fake'); 59 | }); 60 | 61 | await t.rejects(handle(`${dir}/foo/bar`), { message: 'Fake' }); 62 | t.equal(processStub.calledOnceWith(1), true); 63 | 64 | processStub.restore(); 65 | }); 66 | -------------------------------------------------------------------------------- /test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import Stream from 'stream'; 3 | import { Socket } from 'net'; 4 | import { stub } from 'sinon'; 5 | import { test } from 'tap'; 6 | import { 7 | serve, 8 | run, 9 | send, 10 | sendError, 11 | buffer, 12 | json, 13 | HttpError, 14 | } from 'micro/src/lib/index'; 15 | import fetch from 'node-fetch'; 16 | import type { AddressInfo } from 'net'; 17 | import type { RequestHandler, BufferInfo } from 'micro/src/lib/index'; 18 | 19 | function startServer(handler: RequestHandler): Promise<[string, () => void]> { 20 | return new Promise((resolve, reject) => { 21 | const server = http.createServer(serve(handler)); 22 | 23 | server.on('error', reject); 24 | 25 | server.listen(() => { 26 | const { port } = server.address() as AddressInfo; 27 | resolve([ 28 | `http://localhost:${port}`, 29 | () => { 30 | server.close(); 31 | }, 32 | ]); 33 | }); 34 | }); 35 | } 36 | 37 | function sleep(ms: number) { 38 | return new Promise((resolve) => { 39 | setTimeout(resolve, ms); 40 | }); 41 | } 42 | 43 | void test('send(200, <String>)', async (t) => { 44 | const fn: RequestHandler = (req, res) => { 45 | send(res, 200, 'woot'); 46 | }; 47 | 48 | const [url, shutdown] = await startServer(fn); 49 | const res = await fetch(url); 50 | const body = await res.text(); 51 | 52 | t.same(body, 'woot'); 53 | shutdown(); 54 | }); 55 | 56 | void test('send(200, <Object>)', async (t) => { 57 | const fn: RequestHandler = (req, res) => { 58 | send(res, 200, { 59 | a: 'b', 60 | }); 61 | }; 62 | 63 | const [url, shutdown] = await startServer(fn); 64 | 65 | const res: unknown = await fetch(url).then((r) => r.json()); 66 | 67 | t.same(res, { 68 | a: 'b', 69 | }); 70 | shutdown(); 71 | }); 72 | 73 | void test('send(200, <Number>)', async (t) => { 74 | const fn: RequestHandler = (req, res) => { 75 | // Chosen by fair dice roll. guaranteed to be random. 76 | send(res, 200, 4); 77 | }; 78 | 79 | const [url, shutdown] = await startServer(fn); 80 | const res: unknown = await fetch(url).then((r) => r.json()); 81 | 82 | t.same(res, 4); 83 | shutdown(); 84 | }); 85 | 86 | void test('send(200, <Buffer>)', async (t) => { 87 | const fn: RequestHandler = (req, res) => { 88 | send(res, 200, Buffer.from('muscle')); 89 | }; 90 | 91 | const [url, shutdown] = await startServer(fn); 92 | const res = await fetch(url).then((r) => r.text()); 93 | 94 | t.same(res, 'muscle'); 95 | shutdown(); 96 | }); 97 | 98 | void test('send(200, <Stream>)', async (t) => { 99 | const fn: RequestHandler = (req, res) => { 100 | send(res, 200, 'waterfall'); 101 | }; 102 | 103 | const [url, shutdown] = await startServer(fn); 104 | const res = await fetch(url).then((r) => r.text()); 105 | 106 | t.same(res, 'waterfall'); 107 | shutdown(); 108 | }); 109 | 110 | void test('send(<Number>)', async (t) => { 111 | const fn: RequestHandler = (req, res) => { 112 | send(res, 404); 113 | }; 114 | 115 | const [url, shutdown] = await startServer(fn); 116 | 117 | const { status } = await fetch(url); 118 | t.same(status, 404); 119 | shutdown(); 120 | }); 121 | 122 | void test('return <String>', async (t) => { 123 | const fn: RequestHandler = () => 'woot'; 124 | 125 | const [url, shutdown] = await startServer(fn); 126 | const res = await fetch(url).then((r) => r.text()); 127 | 128 | t.same(res, 'woot'); 129 | shutdown(); 130 | }); 131 | 132 | void test('return <Promise>', async (t) => { 133 | const fn: RequestHandler = async () => { 134 | await sleep(100); 135 | return 'I Promise'; 136 | }; 137 | 138 | const [url, shutdown] = await startServer(fn); 139 | const res = await fetch(url).then((r) => r.text()); 140 | 141 | t.same(res, 'I Promise'); 142 | shutdown(); 143 | }); 144 | 145 | void test('sync return <String>', async (t) => { 146 | const fn: RequestHandler = () => 'argon'; 147 | 148 | const [url, shutdown] = await startServer(fn); 149 | const res = await fetch(url).then((r) => r.text()); 150 | 151 | t.same(res, 'argon'); 152 | shutdown(); 153 | }); 154 | 155 | void test('return empty string', async (t) => { 156 | const fn: RequestHandler = () => ''; 157 | 158 | const [url, shutdown] = await startServer(fn); 159 | const res = await fetch(url).then((r) => r.text()); 160 | 161 | t.same(res, ''); 162 | shutdown(); 163 | }); 164 | 165 | void test('return <Object>', async (t) => { 166 | const fn: RequestHandler = () => ({ 167 | a: 'b', 168 | }); 169 | 170 | const [url, shutdown] = await startServer(fn); 171 | const res: unknown = await fetch(url).then((r) => r.json()); 172 | 173 | t.same(res, { 174 | a: 'b', 175 | }); 176 | shutdown(); 177 | }); 178 | 179 | void test('return <Number>', async (t) => { 180 | const fn: RequestHandler = () => 181 | // Chosen by fair dice roll. guaranteed to be random. 182 | 4; 183 | 184 | const [url, shutdown] = await startServer(fn); 185 | const res: unknown = await fetch(url).then((r) => r.json()); 186 | 187 | t.same(res, 4); 188 | shutdown(); 189 | }); 190 | 191 | void test('return <Buffer>', async (t) => { 192 | const fn: RequestHandler = () => Buffer.from('Hammer'); 193 | 194 | const [url, shutdown] = await startServer(fn); 195 | const res = await fetch(url).then((r) => r.text()); 196 | 197 | t.same(res, 'Hammer'); 198 | shutdown(); 199 | }); 200 | 201 | void test('return <Stream>', async (t) => { 202 | const fn: RequestHandler = () => { 203 | const stream = new Stream.Transform(); 204 | stream.push('River'); 205 | stream.end(); 206 | return stream; 207 | }; 208 | 209 | const [url, shutdown] = await startServer(fn); 210 | const res = await fetch(url).then((r) => r.text()); 211 | 212 | t.same(res, 'River'); 213 | shutdown(); 214 | }); 215 | 216 | void test('return <null>', async (t) => { 217 | const fn: RequestHandler = () => null; 218 | 219 | const [url, shutdown] = await startServer(fn); 220 | const res = await fetch(url); 221 | const body = await res.text(); 222 | 223 | t.equal(res.status, 204); 224 | t.equal(body, ''); 225 | shutdown(); 226 | }); 227 | 228 | void test('return <null> calls res.end once', async (t) => { 229 | const fn: RequestHandler = () => null; 230 | 231 | const req = new http.IncomingMessage(new Socket()); 232 | const res = new http.ServerResponse(req); 233 | const fake = stub(res, 'end'); 234 | 235 | await run(req, res, fn); 236 | 237 | t.equal(fake.calledOnce, true); 238 | }); 239 | 240 | void test('throw with code', async (t) => { 241 | const fn: RequestHandler = async () => { 242 | await sleep(100); 243 | 244 | const err = new HttpError('Error from test (expected)'); 245 | err.statusCode = 402; 246 | throw err; 247 | }; 248 | 249 | const [url, shutdown] = await startServer(fn); 250 | 251 | const { status } = await fetch(url); 252 | 253 | t.same(status, 402); 254 | shutdown(); 255 | }); 256 | 257 | void test('throw (500)', async (t) => { 258 | const fn: RequestHandler = () => { 259 | throw new Error('500 from test (expected)'); 260 | }; 261 | 262 | const [url, shutdown] = await startServer(fn); 263 | 264 | const { status } = await fetch(url); 265 | t.same(status, 500); 266 | shutdown(); 267 | }); 268 | 269 | void test('throw (500) sync', async (t) => { 270 | const fn: RequestHandler = () => { 271 | throw new Error('500 from test (expected)'); 272 | }; 273 | 274 | const [url, shutdown] = await startServer(fn); 275 | 276 | const { status } = await fetch(url); 277 | t.same(status, 500); 278 | shutdown(); 279 | }); 280 | 281 | void test('send(200, <Stream>) with error on same tick', async (t) => { 282 | const fn: RequestHandler = (req, res) => { 283 | const stream = new Stream.Transform(); 284 | stream.push('error-stream'); 285 | 286 | stream.emit('error', new Error('500 from test (expected)')); 287 | stream.end(); 288 | send(res, 200, stream); 289 | }; 290 | 291 | const [url, shutdown] = await startServer(fn); 292 | const { status } = await fetch(url); 293 | 294 | t.same(status, 500); 295 | shutdown(); 296 | }); 297 | 298 | void test('custom error', async (t) => { 299 | const fn: RequestHandler = async () => { 300 | await sleep(50); 301 | throw new Error('500 from test (expected)'); 302 | }; 303 | 304 | const handleErrors = 305 | (ofn: RequestHandler) => 306 | async (req: http.IncomingMessage, res: http.ServerResponse) => { 307 | try { 308 | return await ofn(req, res); 309 | } catch (err) { 310 | send(res, 200, 'My custom error!'); 311 | } 312 | }; 313 | 314 | const [url, shutdown] = await startServer(handleErrors(fn)); 315 | const res = await fetch(url).then((r) => r.text()); 316 | 317 | t.same(res, 'My custom error!'); 318 | shutdown(); 319 | }); 320 | 321 | void test('custom async error', async (t) => { 322 | const fn: RequestHandler = async () => { 323 | await sleep(50); 324 | throw new Error('500 from test (expected)'); 325 | }; 326 | 327 | const handleErrors = 328 | (ofn: RequestHandler) => 329 | async (req: http.IncomingMessage, res: http.ServerResponse) => { 330 | try { 331 | return await ofn(req, res); 332 | } catch (err) { 333 | send(res, 200, 'My custom error!'); 334 | } 335 | }; 336 | 337 | const [url, shutdown] = await startServer(handleErrors(fn)); 338 | const res = await fetch(url).then((r) => r.text()); 339 | 340 | t.same(res, 'My custom error!'); 341 | shutdown(); 342 | }); 343 | 344 | void test('json parse error', async (t) => { 345 | const fn: RequestHandler = async (req, res) => { 346 | const body = await json(req); 347 | send(res, 200, (body as { woot: string }).woot); 348 | }; 349 | 350 | const [url, shutdown] = await startServer(fn); 351 | 352 | const { status } = await fetch(url, { 353 | method: 'POST', 354 | body: '{ "bad json" }', 355 | headers: { 356 | 'Content-Type': 'application/json', 357 | }, 358 | }); 359 | t.same(status, 400); 360 | shutdown(); 361 | }); 362 | 363 | void test('json', async (t) => { 364 | interface Payload { 365 | some: { cool: string }; 366 | } 367 | const fn: RequestHandler = async (req, res) => { 368 | const body = await json(req); 369 | 370 | send(res, 200, { 371 | response: (body as Payload).some.cool, 372 | }); 373 | }; 374 | 375 | const [url, shutdown] = await startServer(fn); 376 | 377 | const res = await fetch(url, { 378 | method: 'POST', 379 | body: JSON.stringify({ 380 | some: { 381 | cool: 'json', 382 | }, 383 | }), 384 | }); 385 | const body: unknown = await res.json(); 386 | 387 | t.same((body as { response: unknown }).response, 'json'); 388 | shutdown(); 389 | }); 390 | 391 | void test('json limit (below)', async (t) => { 392 | interface Payload { 393 | some: { cool: string }; 394 | } 395 | const fn: RequestHandler = async (req, res) => { 396 | const body = await json(req, { 397 | limit: 100, 398 | }); 399 | 400 | send(res, 200, { 401 | response: (body as Payload).some.cool, 402 | }); 403 | }; 404 | 405 | const [url, shutdown] = await startServer(fn); 406 | 407 | const res = await fetch(url, { 408 | method: 'POST', 409 | body: JSON.stringify({ 410 | some: { 411 | cool: 'json', 412 | }, 413 | }), 414 | }); 415 | const body: unknown = await res.json(); 416 | 417 | t.same((body as { response: unknown }).response, 'json'); 418 | shutdown(); 419 | }); 420 | 421 | void test('json limit (over)', async (t) => { 422 | const fn: RequestHandler = async (req, res) => { 423 | try { 424 | await json(req, { 425 | limit: 3, 426 | }); 427 | } catch (err) { 428 | t.same((err as HttpError).statusCode, 413); 429 | } 430 | 431 | send(res, 200, 'ok'); 432 | }; 433 | 434 | const [url, shutdown] = await startServer(fn); 435 | const res = await fetch(url, { 436 | method: 'POST', 437 | body: JSON.stringify({ 438 | some: { 439 | cool: 'json', 440 | }, 441 | }), 442 | }); 443 | t.same(res.status, 200); 444 | shutdown(); 445 | }); 446 | 447 | void test('json circular', async (t) => { 448 | interface Payload { 449 | circular: boolean; 450 | obj?: Payload; 451 | } 452 | const fn: RequestHandler = (req, res) => { 453 | const obj: Payload = { 454 | circular: true, 455 | }; 456 | 457 | obj.obj = obj; 458 | send(res, 200, obj); 459 | }; 460 | 461 | const [url, shutdown] = await startServer(fn); 462 | 463 | const { status } = await fetch(url); 464 | t.same(status, 500); 465 | shutdown(); 466 | }); 467 | 468 | void test('no async', async (t) => { 469 | const fn: RequestHandler = (req, res) => { 470 | send(res, 200, { 471 | a: 'b', 472 | }); 473 | }; 474 | 475 | const [url, shutdown] = await startServer(fn); 476 | const obj: unknown = await fetch(url).then((r) => r.json()); 477 | 478 | t.same((obj as { a: string }).a, 'b'); 479 | shutdown(); 480 | }); 481 | 482 | void test('limit included in error', async (t) => { 483 | interface Payload { 484 | some: { cool: string }; 485 | } 486 | const fn: RequestHandler = async (req, res) => { 487 | let body; 488 | 489 | try { 490 | body = await json(req, { 491 | limit: 3, 492 | }); 493 | } catch (err) { 494 | t.ok((err as Error).message.includes('exceeded 3 limit')); 495 | } 496 | 497 | send(res, 200, { 498 | response: (body as Payload).some.cool, 499 | }); 500 | }; 501 | 502 | const [url, shutdown] = await startServer(fn); 503 | const res = await fetch(url, { 504 | method: 'POST', 505 | body: JSON.stringify({ 506 | some: { 507 | cool: 'json', 508 | }, 509 | }), 510 | }); 511 | 512 | t.same(res.status, 500); 513 | shutdown(); 514 | }); 515 | 516 | void test('support for status fallback in errors', async (t) => { 517 | const fn: RequestHandler = (req, res) => { 518 | const err = new HttpError('Custom'); 519 | err.statusCode = 403; 520 | sendError(req, res, err); 521 | }; 522 | 523 | const [url, shutdown] = await startServer(fn); 524 | const { status } = await fetch(url); 525 | t.same(status, 403); 526 | shutdown(); 527 | }); 528 | 529 | void test('json from rawBodyMap works', async (t) => { 530 | interface Payload { 531 | some: { cool: string }; 532 | } 533 | const fn: RequestHandler = async (req, res) => { 534 | const bodyOne = await json(req); 535 | const bodyTwo = await json(req); 536 | 537 | t.same(bodyOne, bodyTwo); 538 | 539 | send(res, 200, { 540 | response: (bodyOne as Payload).some.cool, 541 | }); 542 | }; 543 | 544 | const [url, shutdown] = await startServer(fn); 545 | const res = await fetch(url, { 546 | method: 'POST', 547 | body: JSON.stringify({ 548 | some: { 549 | cool: 'json', 550 | }, 551 | }), 552 | }); 553 | const body: unknown = await res.json(); 554 | 555 | t.same((body as { response: unknown }).response, 'json'); 556 | shutdown(); 557 | }); 558 | 559 | void test('statusCode defaults to 200', async (t) => { 560 | const fn: RequestHandler = () => { 561 | return 'woot'; 562 | }; 563 | 564 | const [url, shutdown] = await startServer(fn); 565 | const res = await fetch(url); 566 | const body = await res.text(); 567 | t.equal(body, 'woot'); 568 | t.equal(res.status, 200); 569 | shutdown(); 570 | }); 571 | 572 | void test('statusCode on response works', async (t) => { 573 | const fn: RequestHandler = (req, res) => { 574 | res.statusCode = 400; 575 | return 'woot'; 576 | }; 577 | 578 | const [url, shutdown] = await startServer(fn); 579 | 580 | const { status } = await fetch(url); 581 | t.same(status, 400); 582 | shutdown(); 583 | }); 584 | 585 | void test('Content-Type header is preserved on string', async (t) => { 586 | const fn: RequestHandler = (req, res) => { 587 | res.setHeader('Content-Type', 'text/html'); 588 | return '<blink>woot</blink>'; 589 | }; 590 | 591 | const [url, shutdown] = await startServer(fn); 592 | const res = await fetch(url); 593 | 594 | t.equal(res.headers.get('content-type'), 'text/html'); 595 | shutdown(); 596 | }); 597 | 598 | void test('Content-Type header is preserved on stream', async (t) => { 599 | const fn: RequestHandler = (req, res) => { 600 | res.setHeader('Content-Type', 'text/html'); 601 | const stream = new Stream.Transform(); 602 | stream.push('River'); 603 | stream.end(); 604 | return stream; 605 | }; 606 | 607 | const [url, shutdown] = await startServer(fn); 608 | const res = await fetch(url); 609 | 610 | t.equal(res.headers.get('content-type'), 'text/html'); 611 | shutdown(); 612 | }); 613 | 614 | void test('Content-Type header is preserved on buffer', async (t) => { 615 | const fn: RequestHandler = (req, res) => { 616 | res.setHeader('Content-Type', 'text/html'); 617 | return Buffer.from('hello'); 618 | }; 619 | 620 | const [url, shutdown] = await startServer(fn); 621 | const res = await fetch(url); 622 | 623 | t.equal(res.headers.get('content-type'), 'text/html'); 624 | shutdown(); 625 | }); 626 | 627 | void test('Content-Type header is preserved on object', async (t) => { 628 | const fn: RequestHandler = (req, res) => { 629 | res.setHeader('Content-Type', 'text/html'); 630 | return {}; 631 | }; 632 | 633 | const [url, shutdown] = await startServer(fn); 634 | const res = await fetch(url); 635 | 636 | t.equal(res.headers.get('content-type'), 'text/html'); 637 | shutdown(); 638 | }); 639 | 640 | void test('res.end is working', async (t) => { 641 | const fn: RequestHandler = (req, res) => { 642 | setTimeout(() => res.end('woot'), 100); 643 | }; 644 | 645 | const [url, shutdown] = await startServer(fn); 646 | const res = await fetch(url).then((r) => r.text()); 647 | 648 | t.same(res, 'woot'); 649 | shutdown(); 650 | }); 651 | 652 | void test('json should throw 400 on empty body with no headers', async (t) => { 653 | const fn: RequestHandler = (req) => json(req); 654 | 655 | const [url, shutdown] = await startServer(fn); 656 | 657 | const res = await fetch(url); 658 | const body = await res.text(); 659 | t.equal(body, 'Invalid JSON'); 660 | t.equal(res.status, 400); 661 | shutdown(); 662 | }); 663 | 664 | void test('buffer should throw 400 on invalid encoding', async (t) => { 665 | const bufferInfo = { encoding: 'lol' }; 666 | 667 | const fn: RequestHandler = async (req) => 668 | buffer(req, bufferInfo as BufferInfo); 669 | 670 | const [url, shutdown] = await startServer(fn); 671 | 672 | const res = await fetch(url, { 673 | method: 'POST', 674 | body: '❤️', 675 | }); 676 | const body = await res.text(); 677 | 678 | t.equal(body, 'Invalid body'); 679 | t.equal(res.status, 400); 680 | shutdown(); 681 | }); 682 | 683 | void test('buffer works', async (t) => { 684 | const fn: RequestHandler = (req) => buffer(req); 685 | const [url, shutdown] = await startServer(fn); 686 | const res = await fetch(url, { method: 'POST', body: '❤️' }); 687 | const body = await res.text(); 688 | t.equal(body, '❤️'); 689 | shutdown(); 690 | }); 691 | 692 | void test('Content-Type header for JSON is set', async (t) => { 693 | const [url, shutdown] = await startServer(() => ({})); 694 | const res = await fetch(url); 695 | 696 | t.equal(res.headers.get('content-type'), 'application/json; charset=utf-8'); 697 | shutdown(); 698 | }); 699 | -------------------------------------------------------------------------------- /test/suite/parse-endpoint.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'tap'; 2 | import { parseEndpoint } from 'micro/src/lib/parse-endpoint'; 3 | 4 | void test('parses TCP URI', (t) => { 5 | t.same(parseEndpoint('tcp://my-host-name.foo.bar:12345'), [ 6 | 12345, 7 | 'my-host-name.foo.bar', 8 | ]); 9 | t.same(parseEndpoint('tcp://0.0.0.0:8080'), [8080, '0.0.0.0']); 10 | 11 | // with the default 12 | t.same(parseEndpoint('tcp://1.2.3.4'), [3000, '1.2.3.4']); 13 | t.end(); 14 | }); 15 | 16 | void test('parses UNIX domain socket URI', (t) => { 17 | t.same(parseEndpoint('unix:/foo/bar.sock'), ['/foo/bar.sock']); 18 | t.same(parseEndpoint('unix:///foo/bar.sock'), ['/foo/bar.sock']); 19 | t.end(); 20 | }); 21 | 22 | void test('parses Windows named pipe URI', (t) => { 23 | t.same(parseEndpoint('pipe:\\\\.\\pipe\\some-name'), [ 24 | '\\\\.\\pipe\\some-name', 25 | ]); 26 | t.end(); 27 | }); 28 | 29 | void test('throws on invalid scheme (protocol)', (t) => { 30 | t.throws( 31 | () => parseEndpoint('foobar://blah'), 32 | 'Unknown --listen endpoint scheme (protocol): foobar:', 33 | ); 34 | t.end(); 35 | }); 36 | 37 | void test('throws on invalid Windows named pipe', (t) => { 38 | t.throws( 39 | () => parseEndpoint('pipe:lolsickbro'), 40 | 'Invalid Windows named pipe endpoint: pipe:lolsickbro', 41 | ); 42 | t.throws( 43 | () => parseEndpoint('pipe://./pipe/lol'), 44 | 'Invalid Windows named pipe endpoint: pipe://./pipe/lol', 45 | ); 46 | t.end(); 47 | }); 48 | 49 | void test('throws on invalid UNIX domain socket', (t) => { 50 | t.throws( 51 | () => parseEndpoint('unix:'), 52 | 'Invalid UNIX domain socket endpoint: unix:', 53 | ); 54 | t.end(); 55 | }); 56 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vercel/style-guide/typescript", 3 | "compilerOptions": { 4 | "target": "ES2021", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node16", 7 | "esModuleInterop": true, 8 | "noEmit": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------