Zero allows you to define your API endpoints as individual functions. Each such function resides in it's own file.
If your function resides in ./api/login.js
file, it's exposed at http://<SERVER>/api/login
. Inspired by good ol' PHP days.
// hello.js
module.exports = function(req, res) {
res.send("Hello");
};
The above file exports a function. This function accepts Request and Response objects. Zero uses Express.js under the hood, so these objects are same as Express.
This means res.send()
, res.json()
will work just fine.
When the user passes query parameters to your function like this: /post?id=1
. You can access them from Request object like this: req.query['id']
.
Your exported function is called all for HTTP methods. Consider the following code:
// submit.js
module.exports = function(req, res) {
console.log(req.body);
res.send({ body: req.body });
};
If you send POST request to /submit
with json
or urlencoded
body. It will be parsed and populated in req.body
.
Zero decides routes based on file structure. Most projects also require dynamic routes like /user/luke
and /user/anakin
. Where luke
and anakin
are parameters. Zero natively supports this type of routes: any file or folder that starts with $ is considered a dynamic route.
So if you create ./user/$username.js
and then from browser visit /user/luke
, Zero will send that request to $username.js
file and set req.params
to {username: 'luke'}
. Code for this:
/*
project/
└── user/
└── $username.js <- this file
*/
module.exports = function(req, res) {
console.log(req.params); // = {username: 'luke'} when user visits /user/luke
res.send({ params: req.params });
};
Parameters apply to folder-names too. Another example: if you want to cater /user/luke/messages
route, you can handle this with following directory structure:
project/
└── user/
└── $username/
└── index.js
└── messages.js
index.js
handles/user/:username
routes.messages.js
handles/user/:username/messages
routes.
Tip: $
is used by Bash for variables. So it might be confusing when you do cd $username
or mkdir $username
and nothing happens. The right way to do this is escaping the $
ie. cd \$username
or mkdir \$username
.
fetch()
allows you to do network requests from your API code.
fetch
is a zero
-specific feature for Node.js. It works similar to how it works in browser. Relative URLs (like /api/messages
) also work.
If you want to fetch
data from a login-protected path, you should set the credentials
of the request to "include"
.
fetch(url, {
credentials: "include"
});
If you have a messages API that only works if the user is logged in, like this:
// api/messages.js
// sends user's messages from database.
module.exports = async (req, res) => {
if (req.user) {
var msgs = await DB.find({ user: req.user.id });
res.json(msgs);
} else {
res.sendStatus(403);
}
};
And you want to fetch these messages from another API endpoint, like this:
// api/user.js
module.exports = async (req, res) => {
var messages = await fetch("/api/messages", {
credentials: "include"
}).then(resp => resp.json());
// ... fetch other info
res.send({
profile: profile,
messages: messages
});
};
Zero automatically forwards credentials even for nested fetch()
requests.
Zero supports writing API endpoints in TypeScript. Any file with .ts
extension will be parsed as TypeScript. Here is an example handler:
// hello.ts
import * as express from "express";
function handler(req: express.Request, res: express.Response) {
res.send("Hello TypeScript");
}
export default handler;
Zero manages sessions on your behalf. You just need to specify where the session data should be stored. Currently Zero supports Redis and MongoDB backends.
By default the session data is stored in browser's cookie itself (as opposed to a database). This is fine for initial development but should be replaced with a better session store (see below).
Zero reads credentials from environment variables. Zero also loads variables from .env
file in your project root, if it's present.
You can provide a Connection String / URL to your store by setting SESSION_REDIS_URL
environment variable.
Alternatively, you can provide connection credentials using
SESSION_REDIS_HOST
, SESSION_REDIS_PASSWORD
, SESSION_REDIS_PORT
environment variables.
Zero reads credentials from environment variables. Zero also loads variables from .env
file in your project root, if present.
To use MongoDB as your session store, you need to provide MongoDB's connection string in SESSION_MONGODB_URL
environment variable.
Zero reads AWS credentials from environment variables AWS_ACCESS_KEY_ID
(or AWS_ID
) and AWS_SECRET_ACCESS_KEY
(or AWS_SECRET
). You must also specify the name of the table by setting the environment variable SESSION_DYNAMODB_TABLE
.
To use DynamoDB TTL, enable it on the table and select the expires
field.
You can specify when the session should expire by setting SESSION_TTL
in seconds. By default this TTL is set to 1 year from login.
Here is a very simple example of how to create a basic login system.
First, let's create a basic HTML form:
<html>
<body>
<form action="/login" method="POST">
<label for="username"><b>Username</b></label>
<input
name="username"
type="text"
placeholder="Enter Username"
required
/>
<br />
<label for="password"><b>Password</b></label>
<input
name="password"
type="password"
placeholder="Enter Password"
required
/>
<br />
<button type="submit">Login</button>
</form>
</body>
</html>
The form takes username
and password
and sends POST
to /login
API.
Let's create that login API in file ./login.js
. Add the following code:
// login.js
// This would ideally come from database.
// Don't forget to hash your passwords.
const PASSWORDS = { luke: "abcd" };
module.exports = (req, res) => {
const { username, password } = req.body;
if (password && PASSWORDS[username] === password) {
req.login({ id: username }, function(err) {
if (err) res.sendStatus(403);
else res.redirect("/user");
});
} else {
res.sendStatus(403);
}
};
- This checks
req.body
for user-submitted username and password. - Then it checks if the password matches with the saved password.
- It calls
req.login()
with any data it wants to save in session for this user. You can store name, email, etc here. This object is automatically populated inreq.user
on all future requests by this user. This way you can display user-specific data based onreq.user
.
Example:
// user.js
module.exports = (req, res) => {
if (req.user) res.send(`Hello ${req.user.id}`);
else res.sendStatus(403);
};
This user
object is also passed to your React pages as props:
export default props => <h1>Hello {props.user ? props.user.id : "World"}</h1>;
Because there is no single Express's app
instance, you probably want to know how to add middlewares. A workaround is to wrap your API handler with the middleware, here is a middleware to enable CORS (cross-origin resource sharing):
// ./api/user.js
var middlewareCORS = (req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
);
next();
};
module.exports = (req, res) => {
middlewareCORS(req, res, () => {
// your real handler:
res.send("Hello");
});
};
You probably want to keep all your common middlewares in a file and import it to each handler.
Keeping all our middlewares in a separate file:
// ./common/middlewares.js
// MIDDLEWARES
var logOriginalUrl = function(req, res, next) {
console.log("url", req.originalUrl);
next();
};
var logMethod = function(req, res, next) {
console.log("method", req.method);
next();
};
// Call each middleware and then our handler
module.exports = (req, res, handler) => {
logMethod(req, res, () => {
logOriginalUrl(req, res, () => {
handler(req, res);
});
});
};
and then use these middlewares in our handler:
// ./user.js
const middlewares = require("./common/middlewares");
module.exports = (rq, rs) =>
middlewares(rq, rs, (req, res) => {
res.send("Some User Info");
});