# Documentation Conceptually **server** is a function that accepts options and other functions. The heavy lifting is already implemented **so you can focus on your project**: ```js // Import the variable into the file const server = require('server'); // All of the arguments are optional server(options, fn1, fn2, fn3, ...); ``` > You can also learn Node.js development by [following the tutorials](/tutorials). ## Getting started There's [a getting started tutorial for beginners](/tutorials/getting-started/). If you know your way around: ```bash npm install server ``` Then create some demo code in your `index.js`: ```js // Import the library const server = require('server'); // Answers to any request server(ctx => 'Hello world'); ``` Run it from the terminal: ```bash node . ``` And open your browser on [localhost:3000](http://localhost:3000/) to see it in action. ## Basic usage Some of the components are the main function on itself, [router](/documentation/router/) and [reply](/documentation/reply/). The main function accepts first an optional object for [the options](/documentation/options/), and then as many [middleware](#middleware) or arrays of middleware as wanted: ```js const server = require('server'); server({ port: 3000 }, ctx => 'Hello 世界'); ``` To use the router and reply extract their methods as needed: ```js const server = require('server'); const { get, post } = server.router; const { render, json } = server.reply; server([ get('/', ctx => render('index.hbs')), post('/', ctx => json(ctx.data)), get(ctx => status(404)) ]); ``` Then when you are splitting your files into different parts and don't have access to the global server you can import only the corresponding parts: ```js const { get, post } = require('server/router'); const { render, json } = require('server/reply'); ``` ## Middleware A *middleware* is plain function that will be called on each request. It receives [a context object](/documentation/context) and [returns a reply](/documentation/reply/), a [basic type](/documentation/reply/#return-value) or nothing. A couple of examples: ```js const setname = ctx => { ctx.user = 'Francisco'; }; const sendname = ctx => send(ctx.user); server(setname, sendname); ``` They can be placed as `server()` arguments, combined into an array or imported/exported from other files: ```js server( ctx => send(ctx.user), [ ctx => console.log(ctx.data) ], require('./comments/router.js') ); ``` Then in `./comments/router.js`: ```js const { get, post, put, del } = require('server/router'); const { json } = require('server/reply'); module.exports = [ get('/', ctx => { /* ... */ }), post('/', ctx => { /* ... */ }), put('/:id', ctx => { /* ... */ }), del('/:id', ctx => { /* ... */ }), ]; ``` The main difference between synchronous and asynchronous functions is that you use `async` keyword to then be able to use the keyword `await` within the function, avoiding [callback hell](http://callbackhell.com/). Some examples of middleware: ```js // Some simple logging const mid = () => { console.log('Hello 世界'); }; // Asynchronous, find user with Mongoose (MongoDB) const mid = async ctx => { ctx.user = await User.find({ name: 'Francisco' }).exec(); console.log(ctx.user); }; // Make sure that there is a user const mid = ctx => { if (!ctx.user) { throw new Error('No user detected!'); } }; // Send some info to the browser const mid = ctx => { return `Some info for ${ctx.user.name}`; }; ``` In this way you can `await` inside of your function. Server.js will also await to your middleware before proceeding to the next one: ```js server(async ctx => { await someAsyncOperation(); console.log('I am first'); }, ctx => { console.log('I am second'); }); ``` If you find an error in an async function you can throw it. It will be caught, a 500 error will be displayed to the user and the error will be logged: ```js const middle = async ctx => { if (!ctx.user) { throw new Error('No user :('); } }; ```
**Avoid callback-based functions**: error propagation is problematic and they have to be converted to promises. Strongly prefer an async/await workflow.## Express middleware Server.js is using express as the underlying library (we <3 express!). You can import middleware designed for express with `modern`: ```js const server = require('server'); // Require it and initialize it with some options const legacy = require('helmet')({ ... }); // Convert it to server.js middleware const mid = server.utils.modern(legacy); // Add it as you'd add a normal middleware server(mid, ...); ``` > Note: the `{ ... }` represent the options for that middleware since many of [express libraries](https://expressjs.com/en/guide/writing-middleware.html) follow the [factory pattern](https://github.com/expressjs/express/issues/3150). To simplify it, we can also perform this operation inline: ```js const server = require('server'); const { modern } = server.utils; server( modern(require('express-mid-1')({ ... })), modern(require('express-mid-2')({ ... })), // ... ); ``` Or just keep the whole middleware in a separated file/folder: ```js // index.js const server = require('server'); const middleware = require('./middleware'); const routes = require('./routes'); server(middleware, routes); ``` Then in our `middleware.js`: ```js // middleware.js const server = require('server'); const { modern } = server.utils; module.exports = [ modern(require('express-mid-1')({ /* ... */ })), modern(require('express-mid-2')({ /* ... */ })) ]; ``` Read the next section for a great example of a common middleware from express used with server. ## CORS To allow requesting a resource from another domain you must enable [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). To do so, you have two options: do it manually or through a great library. Both of them end up setting some headers. Let's see how to do it manually for any domain: ```js const server = require('server'); const { header } = server.reply; // OR server.reply; const cors = [ ctx => header("Access-Control-Allow-Origin", "*"), ctx => header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"), ctx => header("Access-Control-Allow-Methods", "GET, PUT, PATCH, POST, DELETE, HEAD"), ctx => ctx.method.toLowerCase() === 'options' ? 200 : false ]; server({}, cors, ...); ``` If you want to whitelist some domains it's not easy manually, so we can use the great package [`cors` from npm](https://www.npmjs.com/package/cors): ```js const server = require('server'); // Load it with the options const corsExpress = require('cors')({ origin: ['https://example.com', 'https://example2.com'] }); // Make the express middleware compatible with server const cors = server.utils.modern(corsExpress); // Launch the server with this specific middleware server({}, cors, ...); ``` ## Routing This is the concept of redirecting each request to our server to the right place. For instance, if the user requests our homepage `/` we want to render the homepage, but if they request an image gallery `/gallery/67546` we want to render the gallery `67546`. For this we will be creating routes using server's routers. We can import it like this: ```js const server = require('server'); const { get, post } = server.router; // OR const { get, post } = require('server/router'); ``` There are some other ways, but these are the recommended ones. Then we say the path of the request for the method that we want to listen to and a middleware: ```js const getHome = get('/', () => render('index.pug')); const getGallery = get('/gallery/:id', async ctx => { const images = await db.find({ id: ctx.params.id }).exec(); return render('gallery.pug', { images }); }); ``` Let's put it all together to see how they work: ```js const server = require('server'); const { get, post } = server.router; const getHome = get('/', () => render('index.pug')); const getGallery = get('/gallery/:id', async ctx => { const images = await db.find({ id: ctx.params.id }).exec(); return render('gallery.pug', { images }); }); server(getHome, getGallery); ``` We can also receive `post`, `del`, `error`, `socket` and other request types through the router. To see them all, visit the Router documentation: Router Documentation ## Advanced topics There is a lot of basic to mid-difficulty documentation to do until we even get here. Just a quick note so far: The main function returns a promise that will be fulfilled when the server is running and can be accessed. It will receive a more primitive context. So this is perfectly valid: ```js server(ctx => 'Hello world').then(app => { console.log(`Server launched on http://localhost:${app.options.port}/`); }); ``` If you need to stop the server manually, you can do so by invoking the `.close()` function: ```js server(ctx => 'Hello world').then(async app => { console.log('Launched'); await app.close(); console.log('Closed'); }); ``` # Options Available options, their defaults, types and names in `.env`: |name |default |[.env](#environment) |type | |-----------------------|-------------------|--------------------------|------| |[`port`](#port) |`3000` |`PORT=3000` |Number| |[`secret`](#secret) |`'secret-XXXX'` |`SECRET=secret-XXXX` |String| |[`public`](#public) |`'public'` |`PUBLIC=public` |String| |[`views`](#views) |`'views'` |`VIEWS=views` |String| |[`engine`](#engine) |`'pug'` |`ENGINE=pug` |String| |[`env`](#env) |`'development'` |**`NODE_ENV=development`**|String| |[`favicon`](#favicon) |`false` |`FAVICON=public/logo.png` |String| |[`parse`](#parse) |[[info]](#parse) |[[info]](#parse) |Object| |[`session`](#session) |[[info]](#session) |[[info]](#session) |Object| |[`socket`](#socket) |[[info]](#socket) |[[info]](#socket) |Object| |[`security`](#security)|[[info]](#security)|[[info]](#security) |Object| |[`log`](#log) |`'info'` |`LOG=info` |String| You can set those through **the first argument** in `server()` function: ```js // Import the main library const server = require('server'); // Launch the server with the options server({ port: 3000, public: 'public', }); ``` The options preference order is this, from more important to less: 1. `.env`: the variable [within the environment](#environment). 2. `server({ OPTION: 3000 })`: the variable [set as a parameter](#parameter) when launching the server. 3. *defaults*: defaults will be used as can be seen below They are accessible for your dev needs through `ctx.options` ([read more in context options](/documentation/context/#options)): ```js server(ctx => console.log(ctx.options)); // { port: 3000, public: './public', ... } ``` ### Environment Environment variables are *not commited in your version control* but instead they are provided by the machine or Node.js process. In this way these options can be different in your machine and in testing, production or other type of servers. They are uppercase and they can be set through a file called literally `.env` in your root folder: ``` PORT=3000 PUBLIC=public SECRET=secret-XXXX ENGINE=pug NODE_ENV=development ``` > Remember to **add `.env` to your `.gitignore`**. To set them in remote server it will depend on the hosting that you use ([see Heroku example](https://devcenter.heroku.com/articles/config-vars)). ### Argument The alternative to the environment variables is to pass them **as the first argument** when calling `server()`. Each option is a combination of key/value in the object and they all go in lowercase. See some options with their defaults: ```js const server = require('server'); server({ port: 3000, public: 'public', secret: 'secret-XXXX', engine: 'pug', env: 'development' // Remember this is "env" and not "node_env" here }); ``` ### Special cases As a general rule, an option that is an object becomes a `_` separated string in uppercase for the `.env` file. For example, for the SSL we have to pass an object such as: ```js server({ port: 3000, ssl: { key: './ssl.pem', cert: './ssl.cert' } }); ``` So if we want to put this in the environment variable we'd set it up such as: ``` PORT=3000 SSL_KEY=test/fixtures/keys/agent2-key.pem SSL_CERT=test/fixtures/keys/agent2-cert.cert ``` The converse is not true; a `_` separated string in the `.env` does not necessarily become an object as a parameter. You'll have to read the documentation of each option and plugin for the specific details. ## Port The port where you want to launch the server. Defaults to `process.env.PORT` or `3000` if not found, and it's the only option that can be specified as a single option: ```js server(); // Use the default port 3000 server(3000); // Specify the port server({ port: 3000 }); // The same as the previous one ``` If you are setting the port in your environment that will take preference over the argument [as all environment variables](#environment). So it will work seamlessly in Heroku and other hosts that define a `PORT` environment variable. Or you can leave it empty and just use your `.env` file: ``` PORT=3000 ``` Example: setting the port to some other number. For numbers 1-1024 you'd need administrator permission, so we're testing it with higher ports: ```js const options = { port: 5001 }; /* test */ const same = ctx => ({ port: ctx.options.port }); const res = await run(options, same).get('/'); expect(res.body.port).toBe(5001); ``` ## Secret It is [**highly recommended**](https://serverjs.io/tutorials/sessions-production/) that you set this in your environment variable for both development and production before you start coding. It should be a random and long string. It can be used by middleware for storing secrets and keeping cookies/sessions: ``` SECRET=your-random-string-here ``` The *default* provided will be different each time the server is launched. This is not suitable for production, since you want persistent sessions even with server restarts. See the [session in production tutorial](https://serverjs.io/tutorials/sessions-production/) to set it up properly (includes some extras such as Redis sessions). It **cannot** be set as a variable:
Make sure to **return** the reply that you want to use. It won't work otherwise.The `ctx` argument is [explained in middleware's Context](/documentation/context). The reply methods can be imported in several ways: ```js // For whenever you have previously defined `server` const { send, json } = server.reply; // For standalone files: const { send, json } = require('server/reply'); ``` There are many more ways of importing the reply methods, but those above are the recommended ones. ### Chainable While most of the replies are final and they should be invoked only once, there are a handful of others that can be chained. These add something to the ongoing response: - [cookie()](#cookie-): add cookie headers - [header()](#header-): add any headers you want - [status()](#status-): set the status of the response - [type()](#type-): adds the header 'Content-Type' You can chain those among themselves and any of those with a final method that sends. If no final method is called in any place the request will be finished with a 404 response. The `status()` reply can be used as final or as chainable if something else is added. ### Return value Both in synchronous mode or asyncrhonous mode you can just return a string to create a response: ```js // Send a string const middle = ctx => 'Hello 世界'; // Test it const res = await run(middle).get('/'); expect(res.body).toBe('Hello 世界'); ``` Returning an array or an object will stringify them as JSON: ```js server(ctx => ['life', 42]); // Note: extra parenthesis needed by the arrow function to return an object server(ctx => ({ life: 42 })); ``` A single number will be interpreted as a status code and the corresponding body for that status will be returned: ```js server(get('/nonexisting', => 404)); ``` You can also throw anything to trigger an error: ```js const middle = ({ req }) => { if (!req.body) { throw new Error('No body provided'); } } const handler = error(ctx => ctx.error.message); // Test it const res = await run(middle, handler).get('/nonexisting'); expect(res.body).toBe('No body provided'); ``` ### Multiple replies Another important thing is that the first reply used is the one that will be used. However, you should try to avoid this and we might make it more strict in the future: ```js // I hope you speak Spanish server([ ctx => 'Hola mundo', ctx => 'Hello world', ctx => 'こんにちは、世界' ]); ``` To avoid this, just specify the url for each request in a [router](/documentation/router): ```js // I hope you speak Spanish server([ get('/es', ctx => 'Hola mundo'), get('/en', ctx => 'Hello world'), get('/jp', ctx => 'こんにちは、世界') ]); ``` Then each of those URLs will use a different language. ## cookie() Send one or multiple cookies to the browser: ```js cookie(name, value, [options]) ``` By default it will _just_ set a cookie and not finish the request, so if you want to also send the body you need to do it explicitly: ```js server( ctx => cookie('foo', 'bar'), // Set a cookie ctx => cookie('xyz', { obj: 'okay' }), // Stringify the object ctx => cookie('abc', 'def', { maxAge: 100000 }), // Pass some options ctx => cookie('fizz', 'buzz').send(), // Set cookie AND finish the request ); ``` ```js const { cookie } = server.reply; const setCookie = ctx => cookie('foo', 'bar').send(); // Test run(setCookie).get('/').then(res => { expect(res.headers['Set-Cookie:']).toMatch(/foo\=bar/); }); ``` ### Cookie Options The options is an optional object with these keys/values: | Key | Default | Type | |-------------|------------------------|-------------------| | `domain` | Current domain | String | | `encode` | `encodeURIComponent` | Function | | `expires` | `undefined` (session) | Date | | `httpOnly` | `false` | Boolean | | `maxAge` | `undefined` (session) | Number | | `path` | `"/"` | String | | `secure` | `false` | Boolean | | `signed` | `false` | Boolean | | `sameSite` | `false` | Boolean or String | See a better explanation of each one of those in [express' documentation](https://expressjs.com/en/4x/api.html#res.cookie). ## download() An async function that takes a local path and an optional filename. It will return the local file with the filename name for the browser to download. ```js server(ctx => download('user-file-5674354.pdf')); server(ctx => download('user-file-5674354.pdf', 'report.pdf')); ``` You can handle errors for this method downstream: ```js server([ ctx => download('user-file-5674354.pdf'), error(ctx => { console.log(ctx.error); }) ]); ``` ## file() Send a file to the browser with the correct mime type: ```js server(ctx => file('user-profile-5674354.png')); ``` It does not accept a name since the user is not prompted for download. It will stream the file, so that no files are read fully into memory. You can handle errors for this method downstream: ```js server([ ctx => file('user-profile-5674354.png'), error(ctx => { console.log(ctx.error); }) ]); ``` ## header() Set a header to be sent with the response. It accepts two strings as key and value or an object to set multiple headers: ```js const mid = ctx => header('Content-Type', 'text/plain'); const mid2 = ctx => header('Content-Length', '123'); // Same as above const mid = ctx => header({ 'Content-Type': 'text/plain', 'Content-Length': '123' }); ``` You can also send multiple headers with the same name by passing an array as the second parameter: ```js const mid = ctx => header({ 'Link': ['Fake Value', 'Another Fake Value'] }); ``` This [can be chained](#chainable) with other methods to e.g. prompt for a download: ```js const mid = async ctx => { const data = await readFileAsync('./hello.pdf', 'utf-8'); return header({ 'Content-Disposition': `filename='welcome.pdf'` }) .type('application/pdf') .send(new Buffer(data)); }; ``` ## json() Sends a JSON response. It accepts a plain object or an array that will be stringified with `JSON.stringify`. Sets the correct `Content-Type` headers as well: ```js const mid = ctx => json({ foo: 'bar' }); // Test it run(mid).get('/').then(res => { expect(res.body).toEqual(`{"foo":"bar"}`); }); ``` ## jsonp() Same as [json()](#json) but wrapped with a callback. [Read more about JSONP](https://en.wikipedia.org/wiki/JSONP): ```js const mid = ctx => jsonp({ foo: 'bar' }); // Test it run(mid).get('/?callback=callback').then(res => { expect(res.body).toMatch('callback({foo:"bar"})'); }); ``` It is useful for loading data Cross-Domain. The query `?callback=foo` **is mandatory** and you should set the callback name there: ```js const mid = ctx => jsonp({ foo: 'bar' }); // Test it run(mid).get('/?callback=foo').then(res => { expect(res.body).toMatch('foo({foo:"bar"})'); }); ``` ## redirect() Redirects to the url specified. It can be either internal (just a path) or an external URL: ```js const mid1 = ctx => redirect('/foo'); const mid2 = ctx => redirect('../user'); const mid3 = ctx => redirect('https://google.com'); const mid4 = ctx => redirect(301, 'https://google.com'); ``` ## render() This is the most complex method and yet the most useful one. It takes a filename and some data and renders it: ```js const mid1 = ctx => render('index.hbs'); const mid2 = ctx => render('index.hbs', { user: 'Francisco' }); ``` The filename is relative to the [views option](/documentation/options/#-views-) (defaults to `'views'`): ```js // Renders PROJECT/somefolder/index.hbs server({ views: 'somefolder' }, ctx => render('index.hbs')); ``` The extension of this filename is optional. It accepts by default `.hbs`, `.pug` and `.html` and can accept more types [installing other engines](/documentation/options/#-engine-): ```js const mid1 = ctx => render('index.pug'); const mid2 = ctx => render('index.hbs'); const mid3 = ctx => render('index.html'); ``` The data will be passed to the template engine. Note that some plugins might pass additional data as well. ## send() Send the data to the front-end. It is the method used by default with [the raw returns](#raw-return): ```js const mid1 = ctx => send('Hello 世界'); const mid2 = ctx => 'Hello 世界'; ``` However it supports many more data types: String, object, Array or Buffer: ```js const mid1 = ctx => send('Hello 世界'); const mid2 = ctx => send('
Hello 世界
'); const mid4 = ctx => send({ foo: 'bar' }); const mid3 = ctx => send(new Buffer('whatever')); ``` It also has the advantage that it can be chained, unlike just returning the string: ```js const mid1 = ctx => status(201).send({ resource: 'foobar' }); const mid2 = ctx => status(404).send('Not found'); const mid3 = ctx => status(500).send({ error: 'our fault' }); ``` ## status() Sets the status of the response. If no reply is done, it will become final and send that response message as the body: ```js const mid1 = ctx => status(404); // The same as: const mid2 = ctx => status(404).send('Not found'); ``` ## type() Set the `Content-Type` header for the response. It can be a explicit MIME type like these: ```js const mid1 = ctx => type('text/html').send('Hello
'); const mid2 = ctx => type('application/json').send(JSON.stringify({ foo: 'bar' })); const mid3 = ctx => type('image/png').send(...); ``` Or you can also write their more friendly names for an equivalent result: ```js const mid1 = ctx => type('.html'); const mid2 = ctx => type('html'); const mid3 = ctx => type('json'); const mid4 = ctx => type('application/json'); const mid5 = ctx => type('png'); ``` # ErrorsIf you happen to stumble here, this bit of the documentation is outdated and follows some old code. Please help us improve the project and the docs so we can make it into the official release.There are many type of errors that can occur with server.js and here we try to explain them and how to fix them. They are divided by category: where/why they are originated. We also overview here how to handle errors. You have to [first define it](#define-an-error), then [throw the error](#throw-the-error) and finally [handle the error](#error-handling). ### Define an error To define an error in your code the best way to do it is to use the package `human-error` (by the author of server), since it's made to combine perfectly with server.js. In the future we might integrate it, but so far they are kept separated. To define an error, create a different file that will contain all or part of your errors, here called `errors.js` for our site `mycat.com`: ```js // errors.js const errors = require('human-error')(); // <-- notice this errors['/mycat/nogithubsecret'] = ` There is no github secret set up. Make sure you have saved it in your '.env', and if you don't have access go see Tom and he'll explain what to do next. https://mycat.com/guide/setup/#github `; module.exports = errors; ``` ### Throw the error Now let's use it, to do so we'll just need to import this file and throw the corresponding error: ```js const server = require('server'); const HumanError = require('./errors'); server(ctx => { if (!ctx.options.githubsecret) { throw new HumanError('/mycat/nogithubsecret'); } }); ``` Try it! Run the code with `node .` and try accessing [http://localhost:3000/](http://localhost:3000). You should see a `server error` on the front-end and the proper description in the back-end. ### Error handling Now this was an error for the developers where we want to be explicit and show the error clearly. For users thought things change a bit and are greatly improved by server's error handling. First let's deal with super type checking: ```js const route = get('/post/:id', ctx => { if (!/^\d+$/.test(ctx.params.id)) { throw new HumanError('/mycat/type/invalid', { base: '/post' }); } }); // Handle a wrong id error and redirect to a 404 const handle = error('/mycat/type/invalid', async ctx => { return redirect(`/${ctx.error.base || ''}?message=notfound`); }); // Handle all type errors in the namespace "mycat" const handleType = error('/mycat/type', () => { return redirect(`/${ctx.error.base || ''}?message=notfound`); }); // Handle all kind of unhandled errors in the namespace "mycat" const handleAll = error('/mycat', () => { return status(500); }); ``` Let's say that someone is trying to access something they don't have access to. Like deleting a comment that is not theirs: ```js // comments.js module.exports = [ ... del('/comment/:id', async ctx => { const comment = await db.comment.findOne({ _id: ctx.params.id }); if (!comment.author.equals(ctx.user._id)) { throw new HumanError('/mycat/auth/unauthorized', { user: ctx.user._id }); } }) ]; ``` Later on you can handle this specific error, we could log these specific kind of errors, etc. ## Native ### /server/native/portused This happens when you try to launch `server` in a port that is already being used by another process. It can be another server process or a totally independent process. To fix it you can do: - Check that there are no other terminals running this process already. - Change the port for the server such as `server({ port: 5000 });`. - Find out what process is already using the port and stop it. In Linux: `fuser -k -n tcp 3000`. Example on when this error is happening: ```js const server = require('server'); // DO NOT DO THIS: server(3000); server(3000); ``` To fix it, invoke it with a different port: ```js const server = require('server'); server(2000); server(3000); ``` ## Options These errors are related to server's options. ### /server/options/portnotanumber ## Core These errors occur when handling a specific part of server.js. ### /server/core/missingmiddleware This will normally happen if you are trying to create a `server` middleware from an `express` middleware but forget to actually pass express' middleware. This error happens when you call `modern()` with an empty or falsy value: ```js const { modern } = server.utils; const middle = modern(); // Error ``` ### /server/core/invalidmiddleware This happens when you try to call `modern()` with an argument that is not an old-style middleware. The first and only argument for `modern()` is a function with `express`' middleware signature. This error should also tell you dynamically which type of argument you passed. ```js const { modern } = server.utils; const middle = modern('hello'); ``` # Plugins
If you happen to stumble here, this bit of the documentation is under active construction and should not be used at all. Please help us improve the project and the docs.## Create a plugin ### API Here comes the big one, plugins. First, a wish list. I'd like for a plugin to have this API available: ```js module.exports = { // This is working right now (highly unstable) // String name: 'whatever', // Object config: {} // Function, Array init: () => {}, // Function, Array before: () => {}, // Function, Array after: () => {}, // Function, Array final: () => {}, // Not working yet but desirable: // Function (named 'whatever' like the plugin), Object with { name: fn } pairs router: () => {}, // Function (named 'whatever' like the plugin), Object with { name: fn } pairs reply: () => {} }; ``` Now, I am not 100% it makes sense to open `router` and `reply` right now. I think there are some situations where it'd be really useful, like sending a PDF back for example: ```js // Send a pdf from server server(ctx => pdf('./readme.pdf')); ``` But there are many options here and doing it one way might limit some other options. So my first question: #### Simple example: database Why are these useful? Isn't is enough with middleware? Well no, for instance for database connections it is really useful. Let's say we develop a `@server/mongoose` and install it with `npm install @server/mongoose`. Afterwards, just passing the options and we have access to the db through the context: ```js server({ mongoose: 'url for mongodb (or in .env)' }, get('/', ctx => 'Hello world'), get('/sales', hasUser, ctx => ctx.db.sales.find({ user: ctx.user.id })) }); ``` This can be applied to anything that has to be connected or configured once initially and later on can be used in the middleware. #### Advanced example: sass It also opens up to new possibilities, let's see a small example with `sass`. Let's say that we want to make a sass plugin that rebuilds the whole thing on each request for dev and only once on production: ```js module.exports = { name: 'sass', options: { __root: 'source', source: { default: 'style/style.scss', type: String, file: true }, destination: { default: 'public/style.css', type: String } }, init: async ctx => { // If `ctx.options.sass.destination` exists and was not generated by @server/sass // throw an early error and ask for the file to be (re)moved // Remove the `ctx.options.sass.destination` file if (ctx.options.env === 'production') { // Compile everything and store it in `ctx.options.sass.destination` } }, // This will only get called in dev+test, since `style.css` will be found in production before: get('/style.css', async ctx => { // Reply with the whole `ctx.options.sass.source` compiled dynamically }) }; ``` To use it is really simple. First `npm install @server/sass`, then if your options are the default ones you won't even need to write any specific javascript for it. Let's say though that we want to change our source file, which is the *root* option: ```js server({ sass: './front/style.sass' }, ctx => render('index')); ``` That's it, with the flexibility of plugins you wouldn't need any more code to have a sass plugin. This is why I think plugins can be really awesome if they are built and documented properly. **What plugin would you like to see?** ### Options A small exploration about how the options for plugins might look like **for a developer of a plugin**. Now I've written quite a few and have a better idea of the possibilities and limitations of them. I will be using `log` as an example. For the simple way with defaults: ```js plugin.options = { level: { default: 'info' }, reporter: { default: process.stdout }, __root: 'level' }; ``` There are no mandatory fields, however setting a default is strongly recommended. The last root bit would make both of these usages equivalent when using the plugin `log`: ```js server({ log: 'info' }); server({ log: { level: 'info' } }); ``` The `.env` is also quite straightforward in this situation thanks to the `__root` option. Both of these are equivalent as well: ```bash # Single option LOG=info # Multiple options (note: cannot do a function here though!) LOG_LEVEL=info ``` Now you might want all bells and whistles going on. For instance, let's define the type of the parameter. This will add a small validate function internally: ```js log.options = { level: { default: 'info', type: String } }; ``` List of advanced options with their defaults inspired by Mongoose. First, options to use the correct variable: - `default`: the default to set in case it is not set. Leave it unset and it won't have a value if it is not explicitly set. - `env: NAME || true`: defines the name for that variable in the environment variables. If set to false, it will not accept it through the environment. - `arg: NAME || true`: defines the key of the value for options in `server(OPTIONS)`. If set to false it will not accept it from the options object (some arguments that MUST only be accepted through the environment). - `inherit: NAME || false`: passing a name, it inherits the value from a global variable by this order of preference: [validate:] specific environment > global environment > specific argument > global argument > specific default > global default. - `find: FN || false`: a function that receives the options passed on the main function, then all of the environment and finally the default. It returns the wanted value. - `extend: true || false`: extend the default value for the unwritten properties with the default props if they are not set. Value passed: `{ main: 'a', second: 'b' }`, `{ default: { second: 'c', third: 'd' }, extend: true }` => `{ main: 'a', second: 'b', third: 'd' }`. Then you can perform several validations: - `required: FN || false`: make sure the option is set before proceeding. This doesn't make sense when `default` is set. - `type: false`: define the type of the variable. A type or an array of types. Will only check the primitive types `Boolean`, `Number`, `String`, `Array`, `Object`. Can be also an array of types. - `enum: ['a', 'b']`: the variable should be within the list. - `validate: FN || false`: defines a function to validate the value. It will accept first the current value, then all the currently set values and must return `true` for a valid value or `false` otherwise. Note: this is done AFTER any of the other specific checks like type check or the enumerate check. > Note: all of the functions described here can be either synchronous or asynchronous by returning a Promise (or using the `async` keyword). TODO: check the engine for `mongoose` to see if it makes sense to extract the validation part. ### Routes for Plugins I have long been wondering whether the routes and reply should be extensible. There are advantages and disadvantages to this. On one hand, we can stick to more traditional requests workflow: get, post, put, delete and socket seem like a really good scope. But then, if you think about plugins and the possibilities there is so much more we can do and make a nice abstraction layer. For example, let's say you are [making a LINE bot](https://github.com/line/line-bot-sdk-nodejs) and there is a LINE plugin for server. This plugin can behave as normal: ```js server( .. post('/line', ctx => { ctx.line.sendMessage('Hello world'); }) ); ``` This is the traditional way. But if we don't limit ourselves to that, we could be doing it one abstraction level up where the implementation details are invisible: ```js const { line } = server.router; const { message } = server.reply; server( line(ctx => { return message('Hello world'); }) ); ``` One of the big issues here would be namespacing. You might want to have a `line` router but also a `line` reply, and the same with some of the others I can think right now: `sms`, `email`, etc. In the example above, you might be tempted to name your reply `message()`, but so might other plugin. Possible solutions: for the plugins you don't get the router from the main server router, but you get it from the plugin: ```js const line = require('@server/line'); server( line.router(ctx => { return line.reply.message(); }) ); ``` This feels too verbose for the otherwise succint server sintax, but right now seems like the best solution. Another solution would be namespacing the whole server in general. This would also ease one of the pain points I am finding more and more frequently: having to import every route/reply I want to use manually: ```js const server = require('server'); const { router, reply } = server; server( router.get('/', ctx => reply.render(...)), router.post('/', ctx => reply.json(...)) ); ``` While it is a bit more verbose, it cuts down on the import logic (favoring smaller files), makes things more explicit and clear and it's really compatible with this idea of plugins: ```js const server = require('server'); const { router, reply } = server; server( router.get('/', ctx => reply.render(...)), router.post('/', ctx => reply.json(...)), router.line(ctx => reply.line(...)), // OR router.line(ctx => reply.line.message(...)) ); ``` # Testing
If you happen to stumble here, this bit of the documentation is outdated and follows some old code. Please help us improve the project and the docs so we can make it into the official release.There's a small test suite included, but you probably want to use something more specific to your use-case. Testing that a middleware correctly handles the lack of a user: ```js // auth/errors.js (more info in /documentation/errors/) const error = require('server/error'); error['/app/auth/nouser'] = 'You must be authenticated to do this'; module.exports = error; ``` Our main module: ```js // auth/needuser.js const AuthError = require('./errors'); module.exports = ctx => { if (!ctx.user) { throw new AuthError('/app/auth/nouser', { status: 403, public: true }); } }; ``` Then to test this module: ```js // auth/needuser.test.js const run = require('server/test/run'); const needuser = require('./needuser'); describe('auth/needuser.js', () => { it('returns a server error without a user', async () => { const res = await run(needuser).get('/'); expect(res.status).toBe(403); }); it('works with a mocked user', async () => { const mockuser = ctx => { ctx.user = {}; }; const res = await run(mockuser, needuser).get('/'); expect(res.status).toBe(200); }); }); ``` ## run() This function accepts the same arguments as `server()`, however it will return an API that you can use to test any middleware (and, by extension, any route) that you want. The API that it returns so far is this: ```js const run = require('server/test/run'); const api = run(TOTEST); api.get.then(res => { ... }); api.post.then(res => { ... }); api.put.then(res => { ... }); api.del.then(res => { ... }); ``` ## Disable CSRF For testing POST, PUT and DELETE methods you might want to disable CSRF. To do that, just pass it the appropriate option: ```js run({ security: { csrf: false } }, TOTEST); ``` This API accepts as arguments: ```js api.get(URL, OPTIONS); ``` It is using [`request`](https://github.com/request/request) underneath, so the options are the same as for this module. There are few small differences: - It will generate the port randomly from [1024](https://stackoverflow.com/q/413807/938236) to [49151](https://stackoverflow.com/a/113237/938236). However, there is a chance of collision that grows [faster than expected](https://en.wikipedia.org/wiki/Birthday_problem) as your number of tests grows. There's mitigation code going on to avoid collisions so until the tens of thousands of tests it should be fine. - The URLs will be made local internally to `http://localhost:${port}` unless you fully qualify them (which is not recommended).