chore(docs): v5 getting started guide (#2807)

This commit is contained in:
David Luecke 2022-10-17 16:32:28 -07:00 committed by GitHub
parent 05529b34d7
commit 01dfa2a802
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1071 additions and 1724 deletions

View File

@ -136,11 +136,11 @@ export default defineConfig({
},
{
text: 'Crow v4 ',
link: releases
link: 'https://crow.docs.feathersjs.com'
},
{
text: 'Buzzard v3 ',
link: contributing
link: 'https://buzzard.docs.feathersjs.com'
}
]
},
@ -179,41 +179,36 @@ export default defineConfig({
text: 'Getting Started',
collapsible: true,
items: [
// {
// text: 'Getting Ready',
// link: '/guides/basics/setup.md'
// },
// {
// text: 'Quick start',
// link: '/guides/basics/starting.md'
// },
// {
// text: 'Generating an app',
// link: '/guides/basics/generator.md'
// },
// {
// text: 'Services',
// link: '/guides/basics/services.md'
// },
// {
// text: 'Hooks',
// link: '/guides/basics/hooks.md'
// },
// {
// text: 'Authentication',
// link: '/guides/basics/authentication.md'
// },
// {
// text: 'Building a frontend',
// link: '/guides/basics/frontend.md'
// },
{
text: 'Quick start',
link: '/guides/basics/starting.md'
},
{
text: 'Creating an app',
link: '/guides/basics/generator.md'
},
{
text: 'Services',
link: '/guides/basics/services.md'
},
{
text: 'Hooks',
link: '/guides/basics/hooks.md'
},
{
text: 'Schemas',
link: '/guides/basics/schemas.md'
},
{
text: 'Authentication',
link: '/guides/basics/authentication.md'
}
// {
// text: 'Writing Tests',
// link: '/guides/basics/testing.md'
// }
]
},
{
text: 'Frontend',
// collapsible: true,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 159 KiB

View File

@ -1,10 +1,14 @@
---
outline: deep
---
# Authentication
We now have a fully functional chat application consisting of [services](./services.md) and [hooks](./hooks.md). The services come with authentication enabled by default, so before we can use it we need to create a new user and learn how Feathers authentication works. We will look at authenticating our REST API, and then how to authenticate with Feathers in the browser. Finally, we will discuss how to add "Login with GitHub" functionality using OAuth 2.0.
We now have a fully functional chat application consisting of [services](./services.md) and [schemas](./schemas.md). The services come with authentication enabled by default, so before we can use it we need to create a new user and learn how Feathers authentication works. We will look at authenticating our REST API, and then how to authenticate with Feathers in the browser. Finally we will implement a "Login with GitHub".
## Registering a user
Although the frontend we will create [in the next chapter](./frontend.md) will allow to register new users, let's have a quick look at how the REST API can be used directly to register a new user. We can do this by sending a POST request to `http://localhost:3030/users` with JSON data like this as the body:
The HTTP REST API can be used directly to register a new user. We can do this by sending a POST request to `http://localhost:3030/users` with JSON data like this as the body:
```js
// POST /users
@ -24,27 +28,39 @@ curl 'http://localhost:3030/users/' \
[![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/6bcea48aac6c7494c2ad)
> __Note:__ Creating a user with the same email address will only work once, then fail since it already exists in the database.
<BlockQuote type="info">
For SQL databases, creating a user with the same email address will only work once, then fail since it already exists. With the default database selection, you can reset your database by removing the `feathers-chat.sqlite` file and running `npm run migrate` to initialise it again.
</BlockQuote>
This will return something like this:
```json
{
"_id": "<random id>",
"id": "<user id>",
"email": "hello@feathersjs.com",
"avatar": "https://s.gravatar.com/avatar/ffe2a09df37d7c646e974a2d2b8d3e03?s=60"
"avatar": "https://s.gravatar.com/avatar/ffe2a09df37d7c646e974a2d2b8d3e03?s=60"
}
```
Which means our user has been created successfully.
> __Note:__ The password is stored securely in the database but will never be included in a response to an external client.
<BlockQuote type="info">
## Get a token
The password is stored securely in the database but will never be included in an external response.
</BlockQuote>
## Logging in
By default, Feathers uses [JSON web token](https://jwt.io/) for authentication. It is an access token that is valid for a limited time (one day by default) that is issued by the Feathers server and needs to be sent with every API request that requires authentication. Usually a token is issued for a specific user and in our case we want a JWT for the user we just created.
> __Pro tip:__ If you are wondering why Feathers is using JWT for authentication, have a look at [this FAQ](../../help/faq.md#why-are-you-using-jwt-for-sessions).
<BlockQuote type="tip">
If you are wondering why Feathers is using JWT for authentication, have a look at [this FAQ](../../help/faq.md#why-are-you-using-jwt-for-sessions).
</BlockQuote>
Tokens can be created by sending a POST request to the `/authentication` endpoint (which is the same as calling the `create` method on the `authentication` service set up in `src/authentication`) and passing the authentication strategy you want to use. To get a token for an existing user through a username (email) and password login we can use the built-in `local` authentication strategy with a request like this:
@ -73,96 +89,36 @@ This will return something like this:
{
"accessToken": "<JWT for this user>",
"authentication": {
"strategy":"local"
"strategy": "local"
},
"user":{
"_id":"<user id>",
"email":"hello@feathersjs.com",
"avatar":"https://s.gravatar.com/avatar/ffe2a09df37d7c646e974a2d2b8d3e03?s=60",
"user": {
"id": "<user id>",
"email": "hello@feathersjs.com",
"avatar": "https://s.gravatar.com/avatar/ffe2a09df37d7c646e974a2d2b8d3e03?s=60"
}
}
```
The `accessToken` can now be used for other REST requests that require authentication by sending the `Authorization: Bearer <JWT for this user>` HTTP header.
The `accessToken` can now be used for other REST requests that require authentication by sending the `Authorization: Bearer <accessToken>` HTTP header. For example to create a new message:
> __Pro tip:__ For more information about the direct usage of the REST API see the [REST client API](../../api/client/rest.md) and for websockets the [Socket.io client API](../../api/client/socketio.md).
## Browser authentication
When using Feathers on the client, the authentication client does all those authentication steps for us automatically. It stores the access token as long as it is valid so that a user does not have to log in every time they visit our site and sends it with every request. It also takes care of making sure that the user is always authenticated again, for example after they went offline for a bit. Since we will need it in the [building a frontend chapter](./frontend.md) anyway, let's update the existing `public/index.html` file like this:
```html
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" />
<title>FeathersJS chat</title>
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" href="//unpkg.com/feathers-chat@4.0.0/public/base.css">
<link rel="stylesheet" href="//unpkg.com/feathers-chat@4.0.0/public/chat.css">
</head>
<body>
<div id="app" class="flex flex-column"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.12.0/moment.js"></script>
<script src="//unpkg.com/@feathersjs/client@^4.3.0/dist/feathers.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="client.js"></script>
</body>
</html>
```sh
curl 'http://localhost:3030/messages/' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <accessToken>' \
--data-binary '{ "text": "Hello from the console" }'
```
Create a new file `public/client.js` where we can now set up the Feathers client similar to the [getting started example](./starting.md). We also add a `login` method that first tries to use a stored token by calling `app.reAuthenticate()`. If that fails, we try to log in with email/password of our registered user:
<BlockQuote type="tip">
```js
// Establish a Socket.io connection
const socket = io();
Make sure to replace the `<accessToken>` in the above request. For more information about the direct usage of the REST API see the [REST client API](../../api/client/rest.md) and for websockets the [Socket.io client API](../../api/client/socketio.md).
// Initialize our Feathers client application through Socket.io
// with hooks and authentication.
const client = feathers();
client.configure(feathers.socketio(socket));
</BlockQuote>
// Use localStorage to store our login token
client.configure(feathers.authentication({
storage: window.localStorage
}));
## Login with GitHub
const login = async () => {
try {
// First try to log in with an existing JWT
return await client.reAuthenticate();
} catch (error) {
// If that errors, log in with email/password
// Here we would normally show a login page
// to get the login information
return await client.authenticate({
strategy: 'local',
email: 'hello@feathersjs.com',
password: 'supersecret'
});
}
};
OAuth is an open authentication standard supported by almost every major platform and what gets us the log in with Facebook, Google, GitHub etc. buttons. From the Feathers perspective the authentication flow is pretty similar. Instead of authenticating with the `local` strategy by sending a username and password, Feathers directs the user to authorize the application with the login provider. If it is successful Feathers authentication finds or creates the user in the `users` service with the information it got back from the provider and then issues a token for them.
const main = async () => {
const auth = await login();
console.log('User is authenticated', auth);
// Log us out again
await client.logout();
};
main();
```
If you now open the console and visit `http://localhost:3030` you will see that our user has been authenticated.
## GitHub login (OAuth)
OAuth is an open authentication standard supported by almost every major platform. It is what is being used by the login with Facebook, Google, GitHub etc. buttons in a web application. From the Feathers perspective the authentication flow is pretty similar. Instead of authenticating with the `local` strategy by sending a username and password, we direct the user to authorize the application with the login provider. If it is successful we find or create the user on the `users` service with the information we got back from the provider and issue a token for them.
Let's use GitHub as an example for how to set up a "Login with GitHub" button. First, we have to [create a new OAuth application on GitHub](https://github.com/settings/applications/new). You can put anything in the name, homepage and description fields. The callback URL __must__ be set
To allow login with GitHub, first, we have to [create a new OAuth application on GitHub](https://github.com/settings/applications/new). You can put anything in the name, homepage and description fields. The callback URL **must** be set to
```sh
http://localhost:3030/oauth/github/callback
@ -170,19 +126,22 @@ http://localhost:3030/oauth/github/callback
![Screenshot of the GitHub application screen](./assets/github-app.png)
> __Note:__ You can find your existing applications in the [GitHub OAuth apps developer settings](https://github.com/settings/developers).
<BlockQuote type="info">
You can find your existing applications in the [GitHub OAuth apps developer settings](https://github.com/settings/developers).
</BlockQuote>
Once you clicked "Register application" we have to update our Feathers app configuration with the client id and secret copied from the GitHub application settings:
![Screenshot of the created GitHub application client id and secret](./assets/github-keys.png)
Find the `authentication` section in `config/default.json` add a configuration section like this:
Find the `authentication` section in `config/default.json` replace the `<Client ID>` and `<Client Secret>` in the `github` section with the proper values:
```js
{
"authentication": {
"oauth": {
"redirect": "/",
"github": {
"key": "<Client ID>",
"secret": "<Client Secret>"
@ -194,98 +153,64 @@ Find the `authentication` section in `config/default.json` add a configuration s
}
```
This tells the OAuth strategy to redirect back to our index page after a successful login and already makes a basic login with GitHub possible. Because of the changes we made in the `users` service in the [services chapter](./services.md) we do need a small customization though. Instead of only adding `githubId` to a new user when they log in with GitHub we also include their email (if it is available), the display name to show in the chat and the avatar image from the profile we get back. We can do this by extending the standard OAuth strategy and registering it as a GitHub specific one and overwriting the `getEntityData` method:
This tells the OAuth strategy to redirect back to our index page after a successful login and already makes a basic login with GitHub possible. Because of the changes we made in the `users` service in the [services chapter](./services.md) we do need a small customization though. Instead of only adding `githubId` to a new user when they log in with GitHub we also include their email and the avatar image from the profile we get back. We can do this by extending the standard OAuth strategy and registering it as a GitHub specific one and overwriting the `getEntityData` method:
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
Update `src/authentication.js` as follows:
```js
const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication');
const { LocalStrategy } = require('@feathersjs/authentication-local');
const { expressOauth, OAuthStrategy } = require('@feathersjs/authentication-oauth');
class GitHubStrategy extends OAuthStrategy {
async getEntityData(profile) {
const baseData = await super.getEntityData(profile);
return {
...baseData,
// You can also set the display name to profile.name
name: profile.login,
// The GitHub profile image
avatar: profile.avatar_url,
// The user email address (if available)
email: profile.email
};
}
}
module.exports = app => {
const authentication = new AuthenticationService(app);
authentication.register('jwt', new JWTStrategy());
authentication.register('local', new LocalStrategy());
authentication.register('github', new GitHubStrategy());
app.use('/authentication', authentication);
app.configure(expressOauth());
};
```
:::
::: tab "TypeScript"
Update `src/authentication.ts` as follows:
```ts
import { ServiceAddons, Params } from '@feathersjs/feathers';
import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication';
import { LocalStrategy } from '@feathersjs/authentication-local';
import { expressOauth, OAuthStrategy, OAuthProfile } from '@feathersjs/authentication-oauth';
import { Application } from './declarations';
```ts{1,5,14-26,33}
import type { Params } from '@feathersjs/feathers'
import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication'
import { LocalStrategy } from '@feathersjs/authentication-local'
import { oauth, OAuthStrategy } from '@feathersjs/authentication-oauth'
import type { OAuthProfile } from '@feathersjs/authentication-oauth'
import type { Application } from './declarations'
declare module './declarations' {
interface ServiceTypes {
'authentication': AuthenticationService & ServiceAddons<any>;
authentication: AuthenticationService
}
}
class GitHubStrategy extends OAuthStrategy {
async getEntityData(profile: OAuthProfile, existing: any, params: Params) {
const baseData = await super.getEntityData(profile, existing, params);
const baseData = await super.getEntityData(profile, existing, params)
return {
...baseData,
// You can also set the display name to profile.name
name: profile.login,
// The GitHub profile image
avatar: profile.avatar_url,
// The user email address (if available)
email: profile.email
};
}
}
}
export default function(app: Application) {
const authentication = new AuthenticationService(app);
export const authentication = (app: Application) => {
const authentication = new AuthenticationService(app)
authentication.register('jwt', new JWTStrategy());
authentication.register('local', new LocalStrategy());
authentication.register('github', new GitHubStrategy());
authentication.register('jwt', new JWTStrategy())
authentication.register('local', new LocalStrategy())
authentication.register('github', new GitHubStrategy())
app.use('/authentication', authentication);
app.configure(expressOauth());
app.use('authentication', authentication)
app.configure(oauth())
}
```
:::
::::
> __Pro tip:__ For more information about the OAuth flow and strategy see the [OAuth API documentation](../../api/authentication/oauth.md).
<BlockQuote type="info">
When we set up the [authentication client in the browser](#browser-authentication) it can also already handle OAuth logins. To log in with GitHub, visit <a href="localhost:3030/oauth/github" target="_blank" rel="noreferrer">localhost:3030/oauth/github</a>. It will redirect to GitHub and ask to authorize our application. If everything went well, you will see a user with your GitHub email address being logged in the console.
For more information about the OAuth flow and strategy see the [OAuth API documentation](../../api/authentication/oauth.md).
> __Note:__ The authentication client will not use the token from the OAuth login if there is already another token logged in. See the [OAuth API](../../api/authentication/oauth.md) for how to link to an existing account.
</BlockQuote>
To log in with GitHub, visit
```
http://localhost:3030/oauth/github
```
It will redirect to GitHub and ask to authorize our application. If everything went well, we get redirected to our homepage with the Feathers logo with the token information in the location hash. This will be used by the Feathers authentication client to authenticate our user.
## What's next?
Sweet! We now have an API that can register new users with email/password or allow a log in via GitHub. This means we have everything we need to [create a frontend](./frontend.md) for our chat application.
Sweet! We now have an API that can register new users with email/password. This means we have everything we need for a frontend for our chat application. You have your choice of

View File

@ -1,363 +0,0 @@
# Building a frontend
As we have seen [when getting started](./starting.md), Feathers works great in the browser and comes with client services that allow to easily connect to a Feathers server.
In this chapter we will create a real-time chat frontend with signup and login using modern plain JavaScript. It will only work in the latest versions of Chrome, Firefox, Safari and Edge since we won't be using a transpiler like Webpack or Babel (which is also why there won't be a TypeScript option). The final version can be found [here](https://github.com/feathersjs/feathers-chat/).
![The Feathers chat application](./assets/feathers-chat.png)
> __Note:__ We will not be using a frontend framework so we can focus on what Feathers is all about. Feathers is framework agnostic and can be used with any frontend framework like React, VueJS or Angular. For more information see the [frameworks section](../frameworks.md).
## Set up the page
First, let's update `public/index.html` to initialize everything we need for the chat frontend:
```html
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" />
<title>FeathersJS chat</title>
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" href="//unpkg.com/feathers-chat@4.0.0/public/base.css">
<link rel="stylesheet" href="//unpkg.com/feathers-chat@4.0.0/public/chat.css">
</head>
<body>
<div id="app" class="flex flex-column"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.12.0/moment.js"></script>
<script src="//unpkg.com/@feathersjs/client@^4.3.0/dist/feathers.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="client.js"></script>
</body>
</html>
```
This will load our chat CSS style, add a container div `#app` and load several libraries:
- The browser version of Feathers (since we are not using a module loader like Webpack or Browserify)
- Socket.io provided by the chat API
- [MomentJS](https://momentjs.com/) to format dates
- A `client.js` for our code to live in
Lets create `public/client.js` where all the following code will live. Each of the following code samples should be added to the end of that file.
## Connect to the API
Well start with the most important thing first, the connection to our Feathers API that connects to our server using websockets and initializes the [authentication client](./authentication.md):
```js
/* global io, feathers, moment */
// Establish a Socket.io connection
const socket = io();
// Initialize our Feathers client application through Socket.io
// with hooks and authentication.
const client = feathers();
client.configure(feathers.socketio(socket));
// Use localStorage to store our login token
client.configure(feathers.authentication());
```
## Base HTML
Next, we have to define some static and dynamic HTML that we can insert into the page when we want to show the login page (which also doubles as the signup page) and the actual chat interface:
```js
// Login screen
const loginHTML = `<main class="login container">
<div class="row">
<div class="col-12 col-6-tablet push-3-tablet text-center heading">
<h1 class="font-100">Log in or signup</h1>
</div>
</div>
<div class="row">
<div class="col-12 col-6-tablet push-3-tablet col-4-desktop push-4-desktop">
<form class="form">
<fieldset>
<input class="block" type="email" name="email" placeholder="email">
</fieldset>
<fieldset>
<input class="block" type="password" name="password" placeholder="password">
</fieldset>
<button type="button" id="login" class="button button-primary block signup">
Log in
</button>
<button type="button" id="signup" class="button button-primary block signup">
Sign up and log in
</button>
<a class="button button-primary block" href="/oauth/github">
Login with GitHub
</a>
</form>
</div>
</div>
</main>`;
// Chat base HTML (without user list and messages)
const chatHTML = `<main class="flex flex-column">
<header class="title-bar flex flex-row flex-center">
<div class="title-wrapper block center-element">
<img class="logo" src="http://feathersjs.com/crow/feathers-logo-wide.png"
alt="Feathers Logo">
<span class="title">Chat</span>
</div>
</header>
<div class="flex flex-row flex-1 clear">
<aside class="sidebar col col-3 flex flex-column flex-space-between">
<header class="flex flex-row flex-center">
<h4 class="font-300 text-center">
<span class="font-600 online-count">0</span> users
</h4>
</header>
<ul class="flex flex-column flex-1 list-unstyled user-list"></ul>
<footer class="flex flex-row flex-center">
<a href="#" id="logout" class="button button-primary">
Sign Out
</a>
</footer>
</aside>
<div class="flex flex-column col col-9">
<main class="chat flex flex-column flex-1 clear"></main>
<form class="flex flex-row flex-space-between" id="send-message">
<input type="text" name="text" class="flex flex-1">
<button class="button-primary" type="submit">Send</button>
</form>
</div>
</div>
</main>`;
// Helper to safely escape HTML
const escape = str => str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;').replace(/>/g, '&gt;')
// Add a new user to the list
const addUser = user => {
const userList = document.querySelector('.user-list');
if(userList) {
// Add the user to the list
userList.innerHTML += `<li>
<a class="block relative" href="#">
<img src="${user.avatar}" alt="" class="avatar">
<span class="absolute username">${escape(user.name || user.email)}</span>
</a>
</li>`;
// Update the number of users
const userCount = document.querySelectorAll('.user-list li').length;
document.querySelector('.online-count').innerHTML = userCount;
}
};
// Renders a message to the page
const addMessage = message => {
// The user that sent this message (added by the populate-user hook)
const { user = {} } = message;
const chat = document.querySelector('.chat');
// Escape HTML to prevent XSS attacks
const text = escape(message.text);
if(chat) {
chat.innerHTML += `<div class="message flex flex-row">
<img src="${user.avatar}" alt="${user.name || user.email}" class="avatar">
<div class="message-wrapper">
<p class="message-header">
<span class="username font-600">${escape(user.name || user.email)}</span>
<span class="sent-date font-300">${moment(message.createdAt).format('MMM Do, hh:mm:ss')}</span>
</p>
<p class="message-content font-300">${text}</p>
</div>
</div>`;
// Always scroll to the bottom of our message list
chat.scrollTop = chat.scrollHeight - chat.clientHeight;
}
};
```
This will add the following variables and functions:
- `loginHTML` contains some static HTML for the login/signup page
- `chatHTML` contains the main chat page content (once a user is logged in)
- `addUser(user)` is a function to add a new user to the user list on the left
- `addMessage(message)` is a function to add a new message to the list. It will also make sure that we always scroll to the bottom of the message list as messages get added
## Displaying pages
Next, we'll add two functions to display the login and chat page, where we'll also add a list of the 25 newest chat messages and the registered users.
```js
// Show the login page
const showLogin = (error) => {
if(document.querySelectorAll('.login').length && error) {
document.querySelector('.heading').insertAdjacentHTML('beforeend', `<p>There was an error: ${error.message}</p>`);
} else {
document.getElementById('app').innerHTML = loginHTML;
}
};
// Shows the chat page
const showChat = async () => {
document.getElementById('app').innerHTML = chatHTML;
// Find the latest 25 messages. They will come with the newest first
const messages = await client.service('messages').find({
query: {
$sort: { createdAt: -1 },
$limit: 25
}
});
// We want to show the newest message last
messages.data.reverse().forEach(addMessage);
// Find all users
const users = await client.service('users').find();
// Add each user to the list
users.data.forEach(addUser);
};
```
- `showLogin(error)` will either show the content of loginHTML or, if the login page is already showing, add an error message. This will happen when you try to log in with invalid credentials or sign up with a user that already exists.
- `showChat()` does several things. First, we add the static chatHTML to the page. Then we get the latest 25 messages from the messages Feathers service (this is the same as the `/messages` endpoint of our chat API) using the Feathers query syntax. Since the list will come back with the newest message first, we need to reverse the data. Then we add each message by calling our `addMessage` function so that it looks like a chat app shouldwith old messages getting older as you scroll up. After that we get a list of all registered users to show them in the sidebar by calling addUser.
## Login and signup
Alright. Now we can show the login page (including an error message when something goes wrong) and if we are logged in call the `showChat` we defined above. Weve built out the UI, now we have to add the functionality to actually allow people to sign up, log in and also log out.
```js
// Retrieve email/password object from the login/signup page
const getCredentials = () => {
const user = {
email: document.querySelector('[name="email"]').value,
password: document.querySelector('[name="password"]').value
};
return user;
};
// Log in either using the given email/password or the token from storage
const login = async credentials => {
try {
if(!credentials) {
// Try to authenticate using an existing token
await client.reAuthenticate();
} else {
// Otherwise log in with the `local` strategy using the credentials we got
await client.authenticate({
strategy: 'local',
...credentials
});
}
// If successful, show the chat page
showChat();
} catch(error) {
// If we got an error, show the login page
showLogin(error);
}
};
```
- `getCredentials()` gets us the values of the username (email) and password fields from the login/signup page to be used directly with Feathers authentication.
- `login(credentials)` will either authenticate the credentials returned by getCredentials against our Feathers API using the local authentication strategy (e.g. username and password) or, if no credentials are given, try and use the JWT stored in localStorage. This will try and get the JWT from localStorage first where it is put automatically once you log in successfully so that we dont have to log in every time we visit the chat. Only if that doesnt work it will show the login page. Finally, if the login was successful it will show the chat page.
## Event listeners and real-time
In the last step we will add event listeners for all buttons and functionality to send new message and make the user and message list update in real-time.
```js
const addEventListener = (selector, event, handler) => {
document.addEventListener(event, async ev => {
if (ev.target.closest(selector)) {
handler(ev);
}
});
};
// "Signup and login" button click handler
addEventListener('#signup', 'click', async () => {
// For signup, create a new user and then log them in
const credentials = getCredentials();
// First create the user
await client.service('users').create(credentials);
// If successful log them in
await login(credentials);
});
// "Login" button click handler
addEventListener('#login', 'click', async () => {
const user = getCredentials();
await login(user);
});
// "Logout" button click handler
addEventListener('#logout', 'click', async () => {
await client.logout();
document.getElementById('app').innerHTML = loginHTML;
});
// "Send" message form submission handler
addEventListener('#send-message', 'submit', async ev => {
// This is the message text input field
const input = document.querySelector('[name="text"]');
ev.preventDefault();
// Create a new message and then clear the input field
await client.service('messages').create({
text: input.value
});
input.value = '';
});
// Listen to created events and add the new message in real-time
client.service('messages').on('created', addMessage);
// We will also see when new users get created in real-time
client.service('users').on('created', addUser);
// Call login right away so we can show the chat window
// If the user can already be authenticated
login();
```
- `addEventListener` is a helper function that lets us add listeners to elements that get added or removed dynamically
- We also added click event listeners for three buttons. `#login` will get the credentials and just log in with those. Clicking `#signup` will signup and log in at the same time. It will first create a new user on our API and then log in with that same user information. Finally, `#logout` will forget the JWT and then show the login page again.
- The `#submit` button event listener gets the message text from the input field, creates a new message on the messages service and then clears out the field.
- Next, we added two `created` event listeners. One for `messages` which calls the `addMessage` function to add the new message to the list and one for `users` which adds the user to the list via `addUser`. This is how Feathers does real-time and everything we need to do in order to get everything to update automatically.
- To kick our application off, we call `login()` which as mentioned above will either show the chat application right away (if we signed in before and the token isnt expired) or the login page.
## Using the chat application
Thats it. We now have a plain JavaScript real-time chat frontend with login and signup. This example demonstrates many of the basic principles of how you interact with a Feathers API. You can log in with your GitHub account by following the "Login with GitHub" button, with the email (`hello@feathersjs.com`) and password (`supersecret`) from the user we registered in the [authentication chapter](./authentication.md) or sign up and log in with a different email address.
If you run into an issue, remember you can find the complete working example at
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
The [feathersjs/feathers-chat](https://github.com/feathersjs/feathers-chat) repository
:::
::: tab "TypeScript"
The [feathersjs/feathers-chat-ts](https://github.com/feathersjs/feathers-chat-ts) repository
:::
::::
## What's next?
In the final chapter, we'll look at [how to write automated tests for our API](./testing.md).

View File

@ -1,175 +1,81 @@
# Generating an app
---
outline: deep
---
In the [getting started chapter](./starting.md) we created a Feathers application in a single file to get a better understanding how Feathers itself works. The Feathers CLI allows us to initialize a new Feathers server with a recommended structure and helps with generating things we commonly need like authentication, a database connection, new services or hooks (more about hooks in a little bit). It can be installed via:
# Creating an app
```sh
npm install @feathersjs/cli -g
```
In the [quick start](./starting.md) we created a Feathers application in a single file to get a better understanding of how Feathers itself works.
> __Important:__ As mentioned when [getting ready](./starting.md), `@feathersjs/cli` also requires Node version 10 or later. If you already have it installed, `feathers --version` should show `4.1.0` or later.
<img style="margin: 2em;" src="/img/main-character-coding.svg" alt="Getting started">
The Feathers CLI allows us to initialize a new Feathers server with a recommended structure and generate things we commonly need like authentication, a database connection or new services.
## Generating the application
Let's create a new directory for our app and in it, generate a new application:
You can create a new Feathers application by running `npm create feathers <name>`. To create a new Feathers application called `feathers-chat` we can run:
```sh
mkdir feathers-chat
cd feathers-chat/
feathers generate app
npm create feathers@pre feathers-chat
```
First, choose if you want to use JavaScript or TypeScript. When presented with the project name, just hit enter, or enter a name (no spaces). Next, write a short description of your application. All other questions should be confirmed with the default selection by hitting Enter.
If you never ran the command before you might be ask to confirm the package installation by pressing enter.
Once you confirm the last prompt, the final selection should look like this:
<BlockQuote type="warning">
Since the generated application is using modern features like ES modules, the Feathers CLI requires Node 16 or newer. The `feathers --version` command should show `5.0.0-pre.31` or later.
</BlockQuote>
First, choose if you want to use JavaScript or TypeScript. When presented with the project name, just hit enter, or enter a name (no spaces). Next, write a short description for your application. Confirm the next questions with the default selection by pressing Enter. When asked about authentication methods, let's include GitHub as well so we can look at adding a "Log In with Github" button.
Once you confirm the last prompt, the final selection should look similar to this:
![feathers generate app prompts](./assets/generate-app.png)
> __Important:__ If you are following this guide for the first time we recommend to not change any of those settings other than the TypeScript/JavaScript selection. Otherwise you might run into things that are not covered here.
<BlockQuote type="warning" label="Note">
## The generated files
`SQLite` creates an SQL database in a file so we don't need to have a database server running. For any other selection, the database you choose has to be available at the connection string.
Let's have a brief look at the files that have been generated:
</BlockQuote>
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
* `config/` contains the configuration files for the app
* `default.json` contains the basic application configuration
* `production.json` files override `default.json` when in production mode by setting `NODE_ENV=production`. For details, see the [configuration API documentation](../../api/configuration.md)
* `node_modules/` our installed dependencies which are also added in the `package.json`
* `public/` contains static files to be served. A sample favicon and `index.html` (which will show up when going directly to the server URL) are already included.
* `src/` contains the Feathers server code.
* `hooks/` contains our custom [hooks](../basics/hooks.md)
* `services/` contains our [services](../basics/services.md)
* `users/` is a service that has been generated automatically to allow registering and authenticating users
* `users.class.js` is the service class
* `users.hooks.js` initializes Feathers hooks for this service
* `users.service.js` registers this service on our Feathers application
* `middleware/` contains any [Express middleware](http://expressjs.com/en/guide/writing-middleware.html)
* `models/` contains database model files
* `users.model.js` sets up our user collection for NeDB
* `app.js` configures our Feathers application like we did in the [getting started chapter](../basics/starting.md)
* `app.hooks.js` registers hooks that apply to every service
* `authentication.js` sets up Feathers authentication system
* `channels.js` sets up Feathers [event channels](../../api/channels.md)
* `index.js` loads and starts the application
* `test/` contains test files for the app, hooks and services
* `services/` has our service tests
* `users.test.js` contains some basic tests for the `users` service
* `app.test.js` tests that the index page appears, as well as 404 errors for HTML pages and JSON
* `authentication.test.js` includes some tests that basic authentication works
* `.editorconfig` is an [EditorConfig](http://editorconfig.org/) setting to help developers define and maintain consistent coding styles among different editors and IDEs.
* `.eslintrc.json` contains defaults for linting your code with [ESLint](http://eslint.org/docs/user-guide/getting-started).
* `.gitignore` specifies [intentionally untracked files](https://git-scm.com/docs/gitignore) which [git](https://git-scm.com/), [GitHub](https://github.com/) and other similar projects ignore.
* `package.json` contains [information](https://docs.npmjs.com/files/package.json) about our NodeJS project like its name or dependencies.
* `README.md` has installation and usage instructions
:::
::: tab "TypeScript"
* `config/` contains the configuration files for the app
* `default.json` contains the basic application configuration
* `production.json` files override `default.json` when in production mode by setting `NODE_ENV=production`. For details, see the [configuration API documentation](../../api/configuration.md)
* `node_modules/` our installed dependencies which are also added in the `package.json`
* `public/` contains static files to be served. A sample favicon and `index.html` (which will show up when going directly to the server URL) are already included.
* `src/` contains the Feathers server code.
* `hooks/` contains our custom [hooks](../basics/hooks.md)
* `services/` contains our [services](../basics/services.md)
* `users/` is a service that has been generated automatically to allow registering and authenticating users
* `users.class.ts` is the service class
* `users.hooks.ts` initializes Feathers hooks for this service
* `users.service.ts` registers this service on our Feathers application
* `middleware/` contains any [Express middleware](http://expressjs.com/en/guide/writing-middleware.html)
* `models/` contains database model files
* `users.model.ts` sets up our user collection for NeDB
* `app.ts` configures our Feathers application like we did in the [getting started chapter](../basics/starting.md)
* `app.hooks.ts` registers hooks that apply to every service
* `authentication.ts` sets up Feathers authentication system
* `channels.ts` sets up Feathers [event channels](../../api/channels.md)
* `declarations.ts` contains TypeScript declarations for our app
* `index.ts` loads and starts the application
* `test/` contains test files for the app, hooks and services
* `services/` has our service tests
* `users.test.ts` contains some basic tests for the `users` service
* `app.test.ts` tests that the index page appears, as well as 404 errors for HTML pages and JSON
* `authentication.test.ts` includes some tests that basic authentication works
* `.editorconfig` is an [EditorConfig](http://editorconfig.org/) setting to help developers define and maintain consistent coding styles among different editors and IDEs.
* `.gitignore` specifies [intentionally untracked files](https://git-scm.com/docs/gitignore) which [git](https://git-scm.com/), [GitHub](https://github.com/) and other similar projects ignore.
* `tsconfig.json` the TypeScript [compiler configuration](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html)
* `package.json` contains [information](https://docs.npmjs.com/files/package.json) about our NodeJS project like its name or dependencies.
* `README.md` has installation and usage instructions
:::
::::
Sweet! We generated our first Feathers application in a new folder called `feathers-chat` so we need to go there.
## Configure functions
The most important pattern used in the generated application to split things up into individual files are _configure functions_ which are functions that are exported from a file and take the Feathers [app object](../../api/application.md) and then use it to e.g. register services. Those functions are then passed to [app.configure](../../api/application.md#configurecallback).
For example, have a look at the following files:
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
`src/services/index.js` looks like this:
```js
const users = require('./users/users.service.js');
// eslint-disable-next-line no-unused-vars
module.exports = function (app) {
app.configure(users);
};
```sh
cd feathers-chat
```
It uses another configure function exported from `src/services/users/users.service.js`. The export from `src/services/index.js` is in turn used in `src/app.js` as:
```js
// ...
const services = require('./services');
// ...
app.configure(authentication);
// Set up our services (see `services/index.js`)
app.configure(services);
// ...
```
:::
::: tab "TypeScript"
`src/services/index.ts` looks like this:
```ts
import { Application } from '../declarations';
import users from './users/users.service';
// Don't remove this comment. It's needed to format import lines nicely.
export default function (app: Application) {
app.configure(users);
}
```
It uses another configure function exported from `src/services/users/users.service.ts`. The export from `src/services/index.js` is in turn used in `src/app.ts` as:
```ts
// ...
import services from './services';
// ...
app.configure(authentication);
// Set up our services (see `services/index.js`)
app.configure(services);
// ...
```
:::
::::
This is how the generator splits things up into separate files and any documentation example that uses the `app` object can be used in a configure function. You can create your own files that export a configure function and `require`/`import` and `app.configure` them in `app.js`.
> __Note:__ Keep in mind that the order in which configure functions are called might matter, e.g. if it is using a service, that service has to be registered first.
## Running the server and tests
The server can now be started by running
The server can be started by running
<LanguageBlock global-id="ts">
```sh
npm run compile
npm start
```
</LanguageBlock>
<LanguageBlock global-id="js">
```sh
npm start
```
After that, you can see a welcome page at `http://localhost:3030`.
</LanguageBlock>
After that, you will see the Feathers logo at
```
http://localhost:3030
```
<BlockQuote type="warning" label="Note">
You can exit the running process by pressing **CTRL + C**
</BlockQuote>
The app also comes with a set of basic tests which can be run with
@ -183,8 +89,12 @@ There is also a handy development command that restarts the server automatically
npm run dev
```
You can keep this command running throughout the rest of this guide, it will reload all our changes automatically.
<BlockQuote type="warning" label="Note">
Keep this command running throughout the rest of this guide so it will reload all our changes automatically.
</BlockQuote>
## What's next?
In this chapter we installed the Feathers CLI, scaffolded a new Feathers application and learned how it splits things up into separate files. In [the next chapter](./services.md) we will learn more about Feathers services and databases.
In this chapter we we created a new Feathers application. To learn more about the generated files and what you can do with the CLI, have a look at the CLI guide after finishing the Getting Started guide. In [the next chapter](./services.md) we will learn more about Feathers services and databases.

View File

@ -1,104 +1,65 @@
---
outline: deep
---
# Hooks
As we have seen in the [services chapter](./services.md), Feathers services are a great way to implement data storage and modification. Technically, we could implement our entire app with services but very often we need similar functionality across multiple services. For example, we might want to check for all services if a user is allowed to even use it or add the current date to all data that we are saving. With just using services we would have to implement this again every time.
As we have seen in the [quick start](./starting.md) and when we created our messages service in [the services chapter](./services.md), Feathers services are a great way to implement data storage and modification. Technically, we could write our entire app with services but very often we need similar functionality across multiple services. For example, we might want to check for all services if a user is allowed to access it. With just services, we would have to write this every time.
This is where Feathers hooks come in. Hooks are pluggable middleware functions that can be registered __before__, __after__ or on __errors__ of a service method. You can register a single hook function or create a chain of them to create complex work-flows. In this chapter we will learn more about hooks and create workflows to process new chat messages.
This is where Feathers hooks come in. Hooks are pluggable middleware functions that can be registered **around**, **before**, **after** or on **errors** of a service method without having to change the original code.
Just like services themselves, hooks are *transport independent*. They are usually also service agnostic, meaning they can be used with *any* service. This pattern keeps your application logic flexible, composable, and much easier to trace through and debug.
Just like services themselves, hooks are _transport independent_. They are usually also service independent, meaning they can be used with *any* service. This pattern keeps your application logic flexible, composable, and much easier to trace through and debug.
Hooks are commonly used to handle things like validation, authorization, logging, sending emails and more.
> __Note:__ A full overview of the hook API can be found in the [hooks API documentation](../../api/hooks.md).
<BlockQuote type="tip">
Hooks are commonly used to handle things like validation, authorization, logging, populating related entities, sending notifications and more.
A full overview of the hook API can be found in the [hooks API documentation](../../api/hooks.md). For the general design pattern behind hooks see [this blog post](https://blog.feathersjs.com/design-patterns-for-modern-web-apis-1f046635215).
> __Pro tip:__ For the general design pattern behind hooks see [this blog post](https://blog.feathersjs.com/design-patterns-for-modern-web-apis-1f046635215). A more Feathers specific overview can be found [here](https://blog.feathersjs.com/api-service-composition-with-hooks-47af13aa6c01).
</BlockQuote>
## Quick example
## Generating a hook
Here is a quick example for a hook that adds a `createdAt` property to the data before calling the actual `create` service method:
Let's generate a hook that logs the total runtime of a service method to the console.
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
```js
const createdAt = async context => {
context.data.createdAt = new Date();
return context;
};
app.service('messages').hooks({
before: {
create: [ createdAt ]
}
});
```sh
npx feathers generate hook
```
:::
::: tab "TypeScript"
```ts
import { HookContext } from '@feathersjs/feathers';
const createdAt = async (context: HookContext) => {
context.data.createdAt = new Date();
return context;
};
We call our hook `log-runtime` and confirm the type with enter to make it an `around` hook.
app.service('messages').hooks({
before: {
create: [ createdAt ]
}
});
<LanguageBlock global-id="ts">
Now update `src/hooks/log-runtime.ts` as follows:
</LanguageBlock>
<LanguageBlock global-id="js">
Now update `src/hooks/log-runtime.js` as follows:
</LanguageBlock>
```ts{4-9}
import type { HookContext, NextFunction } from '../declarations'
export const logRuntime = async (context: HookContext, next: NextFunction) => {
const startTime = Date.now()
// Run everything else (other hooks and service call)
await next()
const duration = Date.now() - startTime
console.log(
`Calling ${context.method} on ${context.path} took ${duration}ms`
)
}
```
:::
::::
In this hook, we store the start time and then run all other hooks and the service method by calling `await next()`. After that we can calculate the duration in milliseconds by subtracting the start time from the current time and log the information to the console.
## Hook functions
A hook function is a function that takes the [hook context](#hook-context) as the parameter and returns that context or nothing. Hook functions run in the order they are registered and will only continue to the next once the current hook function completes. If a hook function throws an error, all remaining hooks (and the service call if it didn't run yet) will be skipped and the error will be returned.
A hook function is an `async` function that takes the [hook `context`](#hook-context) and a `next` function as the parameter. If the hook should only run on **error**, **before** or **after** the service method, it does not need a `next` function. However since we need to do both, get the start time before and the end time after, we created an `around` hook.
A common pattern the generator uses to make hooks more re-usable (e.g. making the `createdAt` property name from the example above configurable) is to create a wrapper function that takes those options and returns a hook function:
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
```js
const setTimestamp = name => {
return async context => {
context.data[name] = new Date();
return context;
}
}
app.service('messages').hooks({
before: {
create: [ setTimestamp('createdAt') ],
update: [ setTimestamp('updatedAt') ]
}
});
```
:::
::: tab "TypeScript"
```ts
import { HookContext } from '@feathersjs/feathers';
const setTimestamp = (name: string) => {
return async (context: HookContext) => {
context.data[name] = new Date();
return context;
}
}
app.service('messages').hooks({
before: {
create: [ setTimestamp('createdAt') ],
update: [ setTimestamp('updatedAt') ]
}
});
```
:::
::::
Now we have a re-usable hook that can set the timestamp on any property.
Hooks run in the order they are registered and if a hook function throws an error, all remaining hooks (and the service call if it didn't run yet) will be skipped and the error will be returned.
## Hook context
@ -110,318 +71,123 @@ Read-only properties are:
- `context.service` - The service this hook is currently running on
- `context.path` - The path (name) of the service
- `context.method` - The service method name
- `context.type` - The hook type (`before`, `after` or `error`)
- `context.type` - The hook type
Writeable properties are:
- `context.params` - The service method call `params`. For external calls, `params` usually contains:
- `context.params.query` - The query (e.g. query string for REST) for the service call
- `context.params.provider` - The name of the transport (which we will look at in the next chapter) the call has been made through. Usually `rest` or `socketio`. Will be `undefined` for internal calls.
- `context.params.query` - The query (e.g. from the query string) for the service call
- `context.params.provider` - The name of the transport the call has been made through. Usually `rest` or `socketio`. Will be `undefined` for internal calls.
- `context.id` - The `id` for a `get`, `remove`, `update` and `patch` service method call
- `context.data` - The `data` sent by the user in a `create`, `update` and `patch` service method call
- `context.data` - The `data` sent by the user in a `create`, `update` and `patch` and custom service method call
- `context.error` - The error that was thrown (in `error` hooks)
- `context.result` - The result of the service method call (in `after` hooks)
- `context.result` - The result of the service method call (available after calling `await next()` or in `after` hooks)
> __Note:__ For more information about the hook context see the [hooks API documentation](../../api/hooks.md).
<BlockQuote type="tip">
For more information about the hook context see the [hooks API documentation](../../api/hooks.md).
</BlockQuote>
## Registering hooks
In a Feathers application generated by the CLI, hooks are being registered in a `.hooks` file in an object in the following format:
In a Feathers application generated by the CLI, hooks are being registered in the `<servicename>.ts` file. The hook registration object is an object with `{ around, before, after, error }` and a list of hooks per method like `{ all: [], find: [], create: [] }`.
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
```js
module.exports = {
before: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
<LanguageBlock global-id="ts">
after: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
To log the runtime of our `messages` service calls we can update `src/services/messages/messages.ts` like this:
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};
```
:::
::: tab "TypeScript"
```ts
export default {
before: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
</LanguageBlock>
<LanguageBlock global-id="js">
after: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
To log the runtime of all `messages` service calls we can update `src/services/messages/messages.js` like this:
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};
```
:::
::::
</LanguageBlock>
This makes it easy to see at one glance in which order hooks are executed and for which method.
```ts{16,33}
import { authenticate } from '@feathersjs/authentication'
> __Note:__ `all` is a special keyword which means those hooks will run before the method specific hooks in this chain.
import { hooks as schemaHooks } from '@feathersjs/schema'
## Processing messages
import {
messageDataValidator,
messageQueryValidator,
messageResolver,
messageDataResolver,
messageQueryResolver,
messageExternalResolver
} from './messages.schema'
Cool. Now that we learned about hooks we can add two hooks to our messages service that help sanitize new messages and add information about the user that sent it.
import type { Application } from '../../declarations'
import { MessageService, getOptions } from './messages.class'
import { logRuntime } from '../../hooks/log-runtime'
### Sanitize new message
export * from './messages.class'
export * from './messages.schema'
When creating a new message, we automatically sanitize our input, add the user that sent it and include the date the message has been created before saving it in the database. In this specific case it is a *before* hook. To create a new hook we can run:
```sh
feathers generate hook
```
Let's call this hook `process-message`. We want to pre-process client-provided data. Therefore, in the next prompt asking for what kind of hook, choose `before` and press Enter.
Next a list of all our services is displayed. For this hook, only choose the `messages` service. Navigate to the entry with the arrow keys and select it with the space key, then confirm with enter.
A hook can run before any number of [service methods](./services.md). For this specific hook, only select `create`. After confirming the last prompt you should see something like this:
A hook was generated and wired up to the selected service. Now it's time to add some code.
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
Update `src/hooks/process-message.js` to look like this:
```js
module.exports = function (options = {}) { // eslint-disable-line no-unused-vars
return async context => {
const { data } = context;
// Throw an error if we didn't get a text
if(!data.text) {
throw new Error('A message must have a text');
// A configure function that registers the service and its hooks via `app.configure`
export const message = (app: Application) => {
// Register our service on the Feathers application
app.use('messages', new MessageService(getOptions(app)), {
// A list of all methods this service exposes externally
methods: ['find', 'get', 'create', 'update', 'patch', 'remove'],
// You can add additional custom events to be sent to clients here
events: []
})
// Initialize hooks
app.service('messages').hooks({
around: {
all: [
logRuntime,
authenticate('jwt')
]
},
before: {
all: [
schemaHooks.validateQuery(messageQueryValidator),
schemaHooks.validateData(messageDataValidator),
schemaHooks.resolveQuery(messageQueryResolver),
schemaHooks.resolveData(messageDataResolver)
]
},
after: {
all: [schemaHooks.resolveResult(messageResolver), schemaHooks.resolveExternal(messageExternalResolver)]
},
error: {
all: []
}
})
}
// The logged in user
const { user } = context.params;
// The actual message text
// Make sure that messages are no longer than 400 characters
const text = data.text.substring(0, 400);
// Update the original data (so that people can't submit additional stuff)
context.data = {
text,
// Set the user id
userId: user._id,
// Add the current date
createdAt: new Date().getTime()
};
return context;
};
};
```
:::
::: tab "TypeScript"
Update `src/hooks/process-message.ts` to look like this:
```js
// Use this hook to manipulate incoming or outgoing data.
// For more information on hooks see: http://docs.feathersjs.com/api/hooks.html
import { Hook, HookContext } from '@feathersjs/feathers';
export default () : Hook => {
return async (context: HookContext) => {
const { data } = context;
// Throw an error if we didn't get a text
if(!data.text) {
throw new Error('A message must have a text');
}
// The authenticated user
const user = context.params.user;
// The actual message text
const text = data.text
// Messages can't be longer than 400 characters
.substring(0, 400);
// Override the original data (so that people can't submit additional stuff)
context.data = {
text,
// Set the user id
userId: user!._id,
// Add the current date
createdAt: new Date().getTime()
};
// Best practice: hooks should always return the context
return context;
};
// Add this service to the service type index
declare module '../../declarations' {
interface ServiceTypes {
messages: MessageService
}
}
```
:::
::::
This validation code includes:
Now every time a our messages service is accessed successfully the name, method and runtime will be logged.
1. A check if there is a `text` in the data and throws an error if not
2. Truncate the message's `text` property to 400 characters
3. Update the data submitted to the database to contain:
- The new truncated text
- The currently authenticated user id (so we always know who sent it)
- The current (creation) date
<BlockQuote type="tip">
### Populate the message sender
In the `process-message` hook we are currently just adding the user's `_id` as the `userId` property in the message. We want to show more information about the user that sent it in the UI, so we'll need to populate more data in the message response.
We can do this by creating another hook called `populate-user` which is an `after` hook on the `messages` service for `all` methods:
```sh
feathers generate hook
```
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
Update `src/hooks/populate-user.js` to:
```js
/* eslint-disable require-atomic-updates */
module.exports = function (options = {}) { // eslint-disable-line no-unused-vars
return async context => {
// Get `app`, `method`, `params` and `result` from the hook context
const { app, method, result, params } = context;
// Function that adds the user to a single message object
const addUser = async message => {
// Get the user based on their id, pass the `params` along so
// that we get a safe version of the user data
const user = await app.service('users').get(message.userId, params);
// Merge the message content to include the `user` object
return {
...message,
user
};
};
// In a find method we need to process the entire page
if (method === 'find') {
// Map all data to include the `user` information
context.result.data = await Promise.all(result.data.map(addUser));
} else {
// Otherwise just update the single result
context.result = await addUser(result);
}
return context;
};
};
```
:::
::: tab "TypeScript"
Update `src/hooks/populate-user.ts` to:
`all` is a special keyword which means those hooks will run before the method specific hooks. Method specific hooks can be registered based on their name, e.g. to only log the runtime for `find` and `get`:
```ts
// Use this hook to manipulate incoming or outgoing data.
// For more information on hooks see: http://docs.feathersjs.com/api/hooks.html
import { Hook, HookContext } from '@feathersjs/feathers';
export default (): Hook => {
return async (context: HookContext) => {
// Get `app`, `method`, `params` and `result` from the hook context
const { app, method, result, params } = context;
// Function that adds the user to a single message object
const addUser = async (message: any) => {
// Get the user based on their id, pass the `params` along so
// that we get a safe version of the user data
const user = await app.service('users').get(message.userId, params);
// Merge the message content to include the `user` object
return {
...message,
user
};
};
// In a find method we need to process the entire page
if (method === 'find') {
// Map all data to include the `user` information
context.result.data = await Promise.all(result.data.map(addUser));
} else {
// Otherwise just update the single result
context.result = await addUser(result);
}
return context;
};
}
app.service('messages').hooks({
around: {
all: [authenticate('jwt')],
find: [logRuntime],
get: [logRuntime]
}
// ...
})
```
:::
::::
> __Note:__ `Promise.all` makes sure that all asynchronous operations complete before returning all the data.
> __Pro tip:__ This is one way to associate data in Feathers. For more information about associations see [this FAQ](../../help/faq.md#how-do-i-do-associations).
## Hooks vs. extending services
In the [previous chapter](./services.md) we extended our user service to add a user avatar. This could also be put in a hook instead but made a good example to illustrate how to extend an existing service. There are no explicit rules when to use a hook or when to extend a service but here are some guidelines.
Use a hook when
- The functionality can be used in more than one place (e.g. validation, permissions etc.)
- It is not a core responsibility of the service and the service can work without it (e.g. sending an email after a user has been created)
Extend a service when
- The functionality is only needed in this one place
- The service could not function without it
Create your own (custom) service when
- Multiple services are combined together (e.g. reports)
- The service does something other than talk to a database (e.g. another API, sensors etc.)
</BlockQuote>
## What's next?
In this chapter we learned how Feathers hooks can be used as middleware for service method calls to validate and manipulate incoming and outgoing data without having to change our service. We now have a fully working chat application. Before we [create a frontend for it](./frontend.md) though, let's first look at how [authentication works with Feathers](./authentication.md).
In this chapter we learned how Feathers hooks can be used as middleware for service method calls without having to change our service. Here we just logged the runtime of a service method to the console but you can imagine that hooks can be useful for many other things like more advanced logging, sending notifications or checking user permissions.
You may also have noticed above that there are already some hooks like `schemaHooks.validateQuery` or `schemaHooks.resolveResult` registered on our service. This brings us to the next chapter on how to define our data model with [schemas and resolvers](./schemas.md).

View File

@ -0,0 +1,288 @@
# Schemas and resolvers
In Feathers, schemas and resolvers allow us to define, validate and secure our data model and types.
<img style="margin: 2em;" src="/img/professor-bird-server.svg" alt="Professor bird at work">
As we've briefly seen in the [previous chapter about hooks](./hooks.md), there were a few hooks registered already to validate schemas and resolve data. Schema validators and resolvers are used with those hooks to modify data in the hook context. Similar to how Feathers services are transport independent, schemas and resolvers are database independent. It comes in two main parts:
- [TypeBox](../../api/schema//typebox.md) or [JSON schema](../../api/schema//schema.md) to define a schema. This allows us to do things like:
- Ensure data is valid and always in the right format
- Automatically get up to date TypeScript types from schema definitions
- Create a typed client that can be used in React, Vue etc. apps
- Automatically generate API documentation
- Validate query string queries and convert them to the correct type
- [Resolvers](../../api/schema/resolvers.md) - Resolve schema properties based on a context (usually the [hook context](./hooks.md)). This can be used for many different things like:
- Populating associations
- Securing queries and e.g. limiting requests to the logged in user
- Safely hiding sensitive data for external clients
- Adding read- and write permissions on the property level
- Hashing passwords and validating dynamic password policies
In this chapter we will look at the generated schemas and resolvers and update them with the information we need for our chat application.
## Feathers schemas
While schemas and resolvers can be used outside of a Feather application, you will usually encounter them in a Feathers context where they come in four kinds:
- **Result** schemas and resolvers that define the data that is being returned. This is also where associated data would be declared
- **Data** schemas and resolvers handle the data from the `create`, `update` and `patch` service methods and can be used to add things like default or calculated values (like the created or updated at date) before saving to the database
- **Query** schemas and resolvers validate and convert the query string and can also be used for additional limitations like only allowing a user to see and modify their own data
- **External** resolvers that return a safe version of the data (e.g. hiding a users password) that can be sent to external clients
## Adding a user avatar
Let's extend our existing users schema to add an `avatar` property so that our users can have a profile image.
<LanguageBlock global-id="ts">
First we need to update the `src/services/users/users.schema.ts` file with the schema property for the avatar and a resolver property that sets a default avatar using Gravatar based on the email address:
</LanguageBlock>
<LanguageBlock global-id="js">
First we need to update the `src/services/users/users.schema.js` file with the schema property for the avatar and a resolver property that sets a default avatar using Gravatar based on the email address:
</LanguageBlock>
```ts{1,16-17,36,47-57,70-74}
import crypto from 'crypto'
import { resolve } from '@feathersjs/schema'
import { Type, getDataValidator, getValidator, querySyntax } from '@feathersjs/typebox'
import type { Static } from '@feathersjs/typebox'
import { passwordHash } from '@feathersjs/authentication-local'
import type { HookContext } from '../../declarations'
import { dataValidator, queryValidator } from '../../schemas/validators'
// Main data model schema
export const userSchema = Type.Object(
{
id: Type.Number(),
email: Type.String(),
password: Type.Optional(Type.String()),
githubId: Type.Optional(Type.Number()),
avatar: Type.String()
},
{ $id: 'User', additionalProperties: false }
)
export type User = Static<typeof userSchema>
export const userResolver = resolve<User, HookContext>({
properties: {}
})
export const userExternalResolver = resolve<User, HookContext>({
properties: {
// The password should never be visible externally
password: async () => undefined
}
})
// Schema for the basic data model (e.g. creating new entries)
export const userDataSchema = Type.Pick(
userSchema,
['email', 'password', 'githubId', 'avatar'],
{
$id: 'UserData',
additionalProperties: false
}
)
export type UserData = Static<typeof userDataSchema>
export const userDataValidator = getDataValidator(userDataSchema, dataValidator)
export const userDataResolver = resolve<User, HookContext>({
properties: {
password: passwordHash({ strategy: 'local' }),
avatar: async (value, user) => {
// If the user passed an avatar image, use it
if (value !== undefined) {
return value
}
// Gravatar uses MD5 hashes from an email address to get the image
const hash = crypto.createHash('md5').update(user.email.toLowerCase()).digest('hex')
// Return the full avatar URL
return `https://s.gravatar.com/avatar/${hash}?s=60`
}
}
})
// Schema for allowed query properties
export const userQueryProperties = Type.Pick(userSchema, ['id', 'email', 'githubId'])
export const userQuerySchema = querySyntax(userQueryProperties)
export type UserQuery = Static<typeof userQuerySchema>
export const userQueryValidator = getValidator(userQuerySchema, queryValidator)
export const userQueryResolver = resolve<UserQuery, HookContext>({
properties: {
// If there is a user (e.g. with authentication), they are only allowed to see their own data
id: async (value, user, context) => {
// We want to be able to get a list of all users but
// only let a user modify their own data otherwise
if (context.params.user && context.method !== 'find') {
return context.params.user.id
}
return value
}
}
})
```
## Handling messages
Next we can look at the messages service schema. We want to include the date when the message was created as `createdAt` and the id of the user who sent it as `userId`. When we get a message back, we also want to populate the `user` with the user data from `userId` so that we can show e.g. the user image and email.
<LanguageBlock global-id="ts">
Update the `src/services/messages/messages.schema.ts` file like this:
</LanguageBlock>
<LanguageBlock global-id="js">
Update the `src/services/messages/messages.schema.js` file like this:
</LanguageBlock>
```ts{7,14-16,23-26,43-49,56,66-74}
import { resolve } from '@feathersjs/schema'
import { Type, getDataValidator, getValidator, querySyntax } from '@feathersjs/typebox'
import type { Static } from '@feathersjs/typebox'
import type { HookContext } from '../../declarations'
import { dataValidator, queryValidator } from '../../schemas/validators'
import { userSchema } from '../users/users.schema'
// Main data model schema
export const messageSchema = Type.Object(
{
id: Type.Number(),
text: Type.String(),
createdAt: Type.Number(),
userId: Type.Number(),
user: Type.Ref(userSchema)
},
{ $id: 'Message', additionalProperties: false }
)
export type Message = Static<typeof messageSchema>
export const messageResolver = resolve<Message, HookContext>({
properties: {
user: async (_value, message, context) => {
// Associate the user that sent the message
return context.app.service('users').get(message.userId)
}
}
})
export const messageExternalResolver = resolve<Message, HookContext>({
properties: {}
})
// Schema for creating new entries
export const messageDataSchema = Type.Pick(messageSchema, ['text'], {
$id: 'MessageData',
additionalProperties: false
})
export type MessageData = Static<typeof messageDataSchema>
export const messageDataValidator = getDataValidator(messageDataSchema, dataValidator)
export const messageDataResolver = resolve<Message, HookContext>({
properties: {
userId: async (_value, _message, context) => {
// Associate the record with the id of the authenticated user
return context.params.user.id
},
createdAt: async () => {
return Date.now()
}
}
})
// Schema for allowed query properties
export const messageQueryProperties = Type.Pick(
messageSchema,
['id', 'text', 'createdAt', 'userId'],
{
additionalProperties: false
}
)
export const messageQuerySchema = querySyntax(messageQueryProperties)
export type MessageQuery = Static<typeof messageQuerySchema>
export const messageQueryValidator = getValidator(messageQuerySchema, queryValidator)
export const messageQueryResolver = resolve<MessageQuery, HookContext>({
properties: {
userId: async (value, user, context) => {
// We want to be able to get a list of all messages but
// only let a user access their own messages otherwise
if (context.params.user && context.method !== 'find') {
return context.params.user.id
}
return value
}
}
})
```
## Creating a migration
Now that our schemas and resolvers have everything we need, we also have to update the database with those changes. For SQL databases this is done with migrations. Migrations are a best practise for SQL databases to roll out and undo changes to the data model. Every change we make in a schema will need its corresponding migration step.
<BlockQuote type="warning">
If you choose MongoDB you do **not** need to create a migration.
</BlockQuote>
Initially, every database service will automatically add a migration that creates a table for it with an `id` and `text` property. Our users service also already added a migration to add the email and password fields for logging in. The migration for the changes we made in this chapter needs to
- Add the `avatar` string field to the `users` table
- Add the `createdAt` number field to the `messages` table
- Add the `userId` number field to the `messages` table and reference it with the `id` in the `users` table
To create a new migration with the name `chat` run
```
npm run migrate:make -- chat
```
You should see something like
```
Created Migration: /path/to/feathers-chat/migrations/20220622012334_chat.(ts|js)
```
Open that file and update it as follows
```ts{4-11,15-22}
import type { Knex } from 'knex'
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('users', (table) => {
table.string('avatar')
})
await knex.schema.alterTable('messages', (table) => {
table.bigint('createdAt')
table.bigint('userId').references('id').inTable('users')
})
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('users', (table) => {
table.dropColumn('avatar')
})
await knex.schema.alterTable('messages', (table) => {
table.dropColumn('createdAt')
table.dropColumn('userId')
})
}
```
We can run the migrations on the current database with
```
npm run migrate
```
## What's next?
In this chapter we learned about schemas and implemented all the things we need for our chat application. In the next chapter we will learn about [authentication](./authentication.md) and add a "Login with GitHub".

View File

@ -1,15 +1,19 @@
---
outline: deep
---
# Services
Services are the heart of every Feathers application. You probably remember the service we created in the [getting started chapter](./starting.md) to create and find messages. In this chapter we will dive more into services and update the existing user service in our chat application to include an avatar image.
Services are the heart of every Feathers application. You probably remember the service we made in the [quick start](./starting.md) to create and find messages. In this chapter we will dive more into services and create a database backed service for our chat messages.
## Feathers services
In general, a service is an object or instance of [a class](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Classes) that implements certain methods. Services provide a uniform, protocol independent interface for how to interact with any kind of data like:
In general, a service is an object or instance of a class that implements certain methods. Services provide a uniform, protocol independent interface for how to interact with any kind of data like:
- Reading and/or writing from a database
- Interacting with the file system
- Call another API
- Call other services like
- Calling another API
- Calling other services like
- Sending an email
- Processing a payment
- Returning the current weather for a location, etc.
@ -18,7 +22,7 @@ Protocol independent means that to a Feathers service it does not matter if it h
### Service methods
Service methods are [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) methods that a service can implement. Feathers service methods are:
Service methods are [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) methods that a service can implement. The Feathers service methods are:
- `find` - Find all data (potentially matching a query)
- `get` - Get a single data entry by its unique identifier
@ -26,102 +30,91 @@ Service methods are [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_an
- `update` - Update an existing data entry by completely replacing it
- `patch` - Update one or more data entries by merging with the new data
- `remove` - Remove one or more existing data entries
- `setup` - Called when the application is started
- `teardown` - Called when the application is shut down
Below is an example of Feathers service interface as a class and a normal object:
Below is an example of Feathers service interface as a class:
:::: tabs :options="{ useUrlFragment: false }"
```ts
import type { Application, Id, NullableId, Params } from '@feathersjs/feathers'
::: tab "JavaScript class"
```js
class MyService {
async find(params) {}
async get(id, params) {}
async create(data, params) {}
async update(id, data, params) {}
async patch(id, data, params) {}
async remove(id, params) {}
}
app.use('/my-service', new MyService());
```
:::
::: tab "TypeScript class"
```typescript
import { Application, Id, NullableId, Params, Service } from '@feathersjs/feathers';
class MyService implements Service<any> {
async find(params: Params) {}
async get(id: Id, params: Params) {}
async create(data: any, params: Params) {}
async update(id: NullableId, data: any, params: Params) {}
async patch(id: NullableId, data: any, params: Params) {}
async remove(id: NullableId, params: Params) {}
async setup(path: string, app: Application) {}
async teardown(path: string, app: Application) {}
}
app.use('/my-service', new MyService());
```
:::
::: tab "Object"
```js
const myService = {
async find(params) {
return [];
},
async get(id, params) {},
async create(data, params) {},
async update(id, data, params) {},
async patch(id, data, params) {},
async remove(id, params) {}
}
app.use('/my-service', myService);
```
:::
::::
The parameters for service methods are:
- `id` - The unique identifier for the data
- `data` - The data sent by the user (for creating, updating and patching)
- `params` (*optional*) - Additional parameters, for example the authenticated user or the query
- `data` - The data sent by the user (for `create`, `update`, `patch` and custom methods)
- `params` - Additional parameters, for example the authenticated user or the query
> __Note:__ A service does not have to implement all those methods but must have at least one.
For `setup` and `teardown` (which are only called once on application startup and shutdown) we additionally have
<!-- -->
- `path` - The path the service is registered on
- `app` - The [Feathers application](./../../api/application.md)
> __Pro tip:__ For more information about service, service methods and parameters see the [Service API documentation](../../api/services.md).
Usually those methods can be used for most API functionality but it is also possible to add your own [custom service methods](../../api/services.md#custom-methods).
<BlockQuote type="info">
A service does not have to implement all those methods but must have at least one. For more information about services, service methods, and parameters see the [Service API documentation](../../api/services.md).
</BlockQuote>
When used as a REST API, incoming requests get mapped automatically to their corresponding service method like this:
| Service method | HTTP method | Path |
|---|---|---|
| `service.find({ query: {} })` | GET | /messages |
| `service.find({ query: { unread: true } })` | GET | /messages?unread=true |
| `service.get(1)` | GET | /messages/1 |
| `service.create(body)` | POST | /messages |
| `service.update(1, body)` | PUT | /messages/1 |
| `service.patch(1, body)` | PATCH | /messages/1 |
| `service.remove(1)` | DELETE | /messages/1 |
| Service method | HTTP method | Path |
| ------------------------------------------- | ----------- | --------------------- |
| `service.find({ query: {} })` | GET | /messages |
| `service.find({ query: { unread: true } })` | GET | /messages?unread=true |
| `service.get(1)` | GET | /messages/1 |
| `service.create(body)` | POST | /messages |
| `service.update(1, body)` | PUT | /messages/1 |
| `service.patch(1, body)` | PATCH | /messages/1 |
| `service.remove(1)` | DELETE | /messages/1 |
### Registering services
As we have seen, a service can be registered on the Feathers application by calling [app.use(name, service)](../../api/application.md#use-path-service) with a name and the service instance:
A service can be registered on the Feathers application by calling [app.use(name, service[, options])](../../api/application.md#use-path-service) with a name and the service instance:
```js
const app = feathers();
```ts
import { feathers, type Params } from '@feathersjs/feathers'
class MessageService {
async get(name: string, params: Params) {
return {
message: `You have to do ${name}`
}
}
}
type ServiceTypes = {
messages: MessageService
}
const app = feathers<ServiceTypes>()
// Register the message service on the Feathers application
app.use('messages', new MessageService());
app.use('messages', new MessageService())
// Or with additional options like which methods should be made available
app.use('messages', new MessageService(), {
methods: ['get']
})
```
To get the service object and use the service methods (and events) we can use [app.service(name)](../../api/application.md#service-path):
```js
const messageService = app.service('messages');
const messages = await messageService.find();
const messageService = app.service('messages')
const messages = await messageService.find()
```
### Service events
@ -135,152 +128,46 @@ A registered service will automatically become a [NodeJS EventEmitter](https://n
| `service.patch()` | `service.on('patched')` |
| `service.remove()` | `service.on('removed')` |
This is how Feathers does real-time and how we updated the messages automatically by listening to `app.service('messages').on('created')`.
This is how Feathers does real-time.
```js
app.service('messages').on('created', (data) => {
console.log('New message created', data)
})
```
## Database adapters
Now that we have all those service methods we could go ahead and implement any kind of custom logic using any backend. Very often, that means creating, reading, updating and removing data from a database.
Now that we have all those service methods we could go ahead and implement any kind of custom logic using any backend, similar to what we did in the [quick start guide](./starting.md). Very often, that means creating, reading, updating and removing data from a database.
Writing all that code yourself for every service is pretty repetitive and cumbersome though which is why Feathers has a collection of pre-built services for different databases. They offer most of the basic functionality and can always be fully customized (as we will see in a bit). Feathers database adapters support a common [usage API](../../api/databases/common.md), pagination and [querying syntax](../../api/databases/querying.md) for many popular databases and NodeJS ORMs:
Writing all that code yourself for every service is pretty repetitive and cumbersome, which is why Feathers has a collection of pre-built services for different databases. They offer most of the basic functionality and can always be customized to your needs. Feathers database adapters support a common [usage API](../../api/databases/common.md), pagination and [querying syntax](../../api/databases/querying.md) for many popular databases. The following database adapters are maintained as part of Feathers core:
| Database | Adapter |
|---|---|
| In memory | [feathers-memory](https://github.com/feathersjs-ecosystem/feathers-memory), [feathers-nedb](https://github.com/feathersjs-ecosystem/feathers-nedb) |
| Localstorage, AsyncStorage | [feathers-localstorage](https://github.com/feathersjs-ecosystem/feathers-localstorage) |
| Filesystem | [feathers-nedb](https://github.com/feathersjs-ecosystem/feathers-nedb) |
| MongoDB | [feathers-mongodb](https://github.com/feathersjs-ecosystem/feathers-mongodb), [feathers-mongoose](https://github.com/feathersjs-ecosystem/feathers-mongoose) |
| MySQL, PostgreSQL, MariaDB, SQLite, MSSQL | [feathers-knex](https://github.com/feathersjs-ecosystem/feathers-knex), [feathers-sequelize](https://github.com/feathersjs-ecosystem/feathers-sequelize), [feathers-objection](https://github.com/feathersjs-ecosystem/feathers-objection) |
| Elasticsearch | [feathers-elasticsearch](https://github.com/feathersjs-ecosystem/feathers-elasticsearch) |
- [SQL](../../api/databases/knex.md) for databases like PostgreSQL, SQLite, MySQL, MariaDB, MSSQL
- [MongoDB](../../api/databases/mongodb.md) for MongoDB
- [Memory](../../api/databases/memory.md) for in-memory data
> __Pro tip:__ Each one of the linked adapters has a complete standalone REST API example in their readme.
<BlockQuote type="tip">
In this guide we will use [NeDB](https://github.com/feathersjs-ecosystem/feathers-nedb/) which is a database that writes to the filesystem and does not require any additional setup. The users service that was created when we [generated our application](./generator.md) is already using it. In larger applications you probably want to choose something like PostgreSQL or MongoDB but NeDB is great for this guide because it gets us started quickly without having to learn and install a database system.
There are also many other community maintained database integrations which you can explore on the [ecosystem page](https://github.com/feathersjs/awesome-feathersjs#database). Since they are not part of Feathers core they are not covered in the guides here though.
> __Note:__ NeDB stores its data in our application directory under a `data/` folder. It uses a JSON append-only file format. This means that if you look at the database files directly you might see the same entry multiple times but it will always return the correct data.
</BlockQuote>
If you went with the default selection, we will use **SQLite** which writes the database to a file and does not require any additional setup. The user service that was created when we [generated our application](./generator.md) is already using it. If you decide to use another SQL database like PostgreSQL or MySQL, you will need to change the database connection settings in the configuration.
## Generating a service
In our [newly generated](./generator.md) `feathers-chat` application, we can create database backed services with the following command:
```sh
feathers generate service
npx feathers generate service
```
For this service we will also use NeDB which we can just confirm by pressing enter. We will use `messages` as the service name and can confirm all other prompts with the defaults by pressing enter:
The name for our service is `message` (this is used for variable names etc.) and for the path we use `messages`. Anything else we can confirm with the default:
![feathers generate service prompts](./assets/generate-service.png)
This is it, we now have a database backed messages service with authentication enabled.
## Customizing a service
Feathers has two ways for customizing existing database adapter services. Either by using hooks, which we will look at [in the next chapter](./hooks.md) or by extending the adapter service class. Let's extend our existing `users` service to add a link to the [Gravatar](http://en.gravatar.com/) image associated with the user's email address so we can display a user avatar. We will then add that data to the database by calling the original (`super.create`) method.
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
Update `src/services/users/users.class.js` with the following:
```js
// This is the database adapter service class
const { Service } = require('feathers-nedb');
// We need this to create the MD5 hash
const crypto = require('crypto');
// The Gravatar image service
const gravatarUrl = 'https://s.gravatar.com/avatar';
// The size query. Our chat needs 60px images
const query = 's=60';
// Returns the Gravatar image for an email
const getGravatar = email => {
// Gravatar uses MD5 hashes from an email address (all lowercase) to get the image
const hash = crypto.createHash('md5').update(email.toLowerCase()).digest('hex');
// Return the full avatar URL
return `${gravatarUrl}/${hash}?${query}`;
};
exports.Users = class Users extends Service {
create (data, params) {
// This is the information we want from the user signup data
const { email, password, githubId, name } = data;
// Use the existing avatar image or return the Gravatar for the email
const avatar = data.avatar || getGravatar(email);
// The complete user
const userData = {
email,
name,
password,
githubId,
avatar
};
// Call the original `create` method with existing `params` and new data
return super.create(userData, params);
}
};
```
:::
::: tab "TypeScript"
Update `src/services/users/users.class.ts` with the following:
```ts
import crypto from 'crypto';
import { Params } from '@feathersjs/feathers';
import { Service, NedbServiceOptions } from 'feathers-nedb';
import { Application } from '../../declarations';
// The Gravatar image service
const gravatarUrl = 'https://s.gravatar.com/avatar';
// The size query. Our chat needs 60px images
const query = 's=60';
// Returns the Gravatar image for an email
const getGravatar = (email: string) => {
// Gravatar uses MD5 hashes from an email address (all lowercase) to get the image
const hash = crypto.createHash('md5').update(email.toLowerCase()).digest('hex');
// Return the full avatar URL
return `${gravatarUrl}/${hash}?${query}`;
}
// A type interface for our user (it does not validate any data)
interface UserData {
_id?: string;
email: string;
password: string;
name?: string;
avatar?: string;
githubId?: string;
}
export class Users extends Service<UserData> {
constructor(options: Partial<NedbServiceOptions>, app: Application) {
super(options);
}
create (data: UserData, params?: Params) {
// This is the information we want from the user signup data
const { email, password, githubId, name } = data;
// Use the existing avatar image or return the Gravatar for the email
const avatar = data.avatar || getGravatar(email);
// The complete user
const userData = {
email,
name,
password,
githubId,
avatar
};
// Call the original `create` method with existing `params` and new data
return super.create(userData, params);
}
}
```
:::
::::
Now we can sign up users with email and password and it will automatically set an avatar image for them. If they have no gravatar, it will return a placeholder image.
> __Note:__ We are keeping `githubId` from the original data so that we can add a "Login with GitHub" button in the [authentication](./authentication.md) chapter.
## What's next?
In this chapter we learned about services as Feathers core concept for abstracting data operations. We also saw how a service sends events which we will use later to create real-time applications. After that, we generated a messages service and updated our users service to include an avatar image. Next, we will look at [Hooks](./hooks.md) which is the other key part of how Feathers works.
In this chapter we learned about services as a Feathers core concept for abstracting data operations. We also saw how a service sends events which we will use later to create real-time applications. After that, we generated a messages service. Next, we will [look at Feathers hooks](./hooks.md) as a way to create middleware for services.

View File

@ -1,59 +0,0 @@
# Getting ready
Alright then! Let's learn what Feathers is all about. First we'll have a look at what we are going to do in this guide, what you should already know and what needs to be installed to use Feathers.
## What we will do
In this guide we will get a [quick start](./starting.md) by creating our first simple Feathers REST and real-time API and a website to use it from scratch. Then we will learn about the [Feathers CLI](./generator.md) and the core concepts of [services](./services.md), [hooks](./hooks.md) and [authentication](./authentication.md) by building a chat application that allows users to sign up, log in (including with GitHub) and send and receive messages in real-time. It will look like this:
![The Feathers chat application](./assets/feathers-chat.png)
You can find the final version at
<LanguageBlock global-id="js">
The [feathersjs/feathers-chat](https://github.com/feathersjs/feathers-chat) repository
</LanguageBlock>
<LanguageBlock global-id="ts">
The [feathersjs/feathers-chat-ts](https://github.com/feathersjs/feathers-chat-ts) repository
</LanguageBlock>
## Prerequisites
Feathers works with NodeJS v10.0.0 and later. We recommend using the latest available version from the [NodeJS website](https://nodejs.org/en/). On MacOS and other Unix systems the [Node Version Manager](https://github.com/creationix/nvm) is a good way to quickly install the latest version of NodeJS and keep it up to date.
After successful installation, the `node` and `npm` commands should be available on the terminal and show something similar when running the following commands:
```
$ node --version
v12.0.0
```
```
$ npm --version
6.9.0
```
> __Note:__ Running NodeJS and npm should not require admin or root privileges.
Feathers does work in the browser and supports IE 10 and up. The browser examples used in the guides will however only work in the most recent versions of Chrome, Firefox, Safari and Edge.
## What you should know
In order to get the most out of this guide you should have reasonable JavaScript experience using [ES6](http://es6-features.org/) and later as well as [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) and some experience with NodeJS and the JavaScript features it supports like the [module system](https://nodejs.org/api/modules.html). You can read more about `async/await` in [this blog post](https://blog.risingstack.com/mastering-async-await-in-nodejs/). Some familiarity with HTTP and [REST APIs](https://en.wikipedia.org/wiki/Representational_state_transfer) as well as websockets is also helpful but not necessary.
Feathers works standalone but also provides [an integration](../../api/express.md) with [Express](http://expressjs.com/). This guide does not require any in-depth knowledge of Express but some experience with Express can be helpful in the future.
## What we won't cover
Although Feathers works with many databases, this guide will only use [NeDB](https://github.com/louischatriot/nedb/) which is a file-system based database so there is no need to run a database server. More information about specific databases can be found in the [databases API](../../api/databases/adapters.md).
This guide will also only focus on Feathers core functionality. Once you are finished with the guide, have a look at the [ecosystem page](https://github.com/feathersjs/awesome-feathersjs) for more advanced plugins.
## What's next?
All set up and good to go? Let's [install Feathers and create our first app](./starting.md).

View File

@ -1,6 +1,30 @@
---
outline: deep
---
# Quick start
Now that we are [ready to roll](./setup.md) we can create our first Feathers application. In this quick start guide we'll create our first Feathers REST and real-time API server and a simple website to use it from scratch. It will show how easy it is to get started with Feathers even without a generator or boilerplate.
Alright then! Let's learn Feathers. In this quick start guide we'll create our first Feathers API server and a simple website to use it. You'll see how easy it is to get started with Feathers in just a single file without additional boilerplate or tooling. If you want to jump right into creating a complete application you can go to the [Creating An App](./generator.md) chapter.
<img style="margin: 2em;" src="/img/main-character-bench.svg" alt="Getting started">
Feathers works with all [currently active releases](https://github.com/nodejs/Release#release-schedule). All guides are assuming the languages features from the most current stable NodeJS release which you can get from the [NodeJS website](https://nodejs.org/en/).
After successful installation, the `node` and `npm` commands should be available on the terminal:
```
$ node --version
```
```
$ npm --version
```
<BlockQuote type="warning" label="Important">
Running NodeJS and npm should not require admin or root privileges.
</BlockQuote>
Let's create a new folder for our application:
@ -11,126 +35,75 @@ cd feathers-basics
Since any Feathers application is a Node application, we can create a default [package.json](https://docs.npmjs.com/files/package.json) using `npm`:
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
```sh
npm init --yes
```
:::
::: tab "TypeScript"
<LanguageBlock global-id="ts">
```sh
# Install TypeScript and its NodeJS wrapper globally
npm i typescript ts-node -g
npm init --yes
# Install TypeScript and its NodeJS wrapper
npm i typescript ts-node --save-dev
# Also initialize a TS configuration file that uses modern JavaScript
tsc --init --target es2018
npx tsc --init --target es2020
```
:::
::::
</LanguageBlock>
<LanguageBlock global-id="js">
```sh
npm init --yes
```
</LanguageBlock>
## Installing Feathers
Feathers can be installed like any other Node module by installing the [@feathersjs/feathers](https://www.npmjs.com/package/@feathersjs/feathers) package through [npm](https://www.npmjs.com). The same package can also be used with a module loader like Webpack or Browserify and in React Native.
Feathers can be installed like any other Node module by installing the [@feathersjs/feathers](https://www.npmjs.com/package/@feathersjs/feathers) package through [npm](https://www.npmjs.com). The same package can also be used with module loaders like Vite, Webpack, and in React Native.
```sh
npm install @feathersjs/feathers --save
npm install @feathersjs/feathers@pre --save
```
> __Note:__ All Feathers core modules are in the `@feathersjs` namespace.
<BlockQuote label="note">
All Feathers core modules are in the `@feathersjs` namespace.
</BlockQuote>
## Our first app
Now we can create a Feathers application with a simple messages service that allows to create new messages and find all existing ones.
Now we can create a Feathers application with a simple `messages` service that allows us to create new messages and find all existing ones.
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
Create a file called `app.js` with the following content:
<LanguageBlock global-id="ts">
```js
const feathers = require('@feathersjs/feathers');
const app = feathers();
// A messages service that allows to create new
// and return all existing messages
class MessageService {
constructor() {
this.messages = [];
}
async find () {
// Just return all our messages
return this.messages;
}
async create (data) {
// The new message is the data merged with a unique identifier
// using the messages length since it changes whenever we add one
const message = {
id: this.messages.length,
text: data.text
}
// Add new message to the list
this.messages.push(message);
return message;
}
}
// Register the message service on the Feathers application
app.use('messages', new MessageService());
// Log every time a new message has been created
app.service('messages').on('created', message => {
console.log('A new message has been created', message);
});
// A function that creates new messages and then logs
// all existing messages
const main = async () => {
// Create a new message on our message service
await app.service('messages').create({
text: 'Hello Feathers'
});
await app.service('messages').create({
text: 'Hello again'
});
// Find all existing messages
const messages = await app.service('messages').find();
console.log('All messages', messages);
};
main();
```
:::
::: tab "TypeScript"
Create a file called `app.ts` with the following content:
</LanguageBlock>
<LanguageBlock global-id="js">
Create a file called `app.mjs` with the following content:
</LanguageBlock>
```ts
import feathers from '@feathersjs/feathers';
import { feathers } from '@feathersjs/feathers'
// This is the interface for the message data
interface Message {
id?: number;
text: string;
id?: number
text: string
}
// A messages service that allows to create new
// A messages service that allows us to create new
// and return all existing messages
class MessageService {
messages: Message[] = [];
messages: Message[] = []
async find () {
async find() {
// Just return all our messages
return this.messages;
return this.messages
}
async create (data: Pick<Message, 'text'>) {
async create(data: Pick<Message, 'text'>) {
// The new message is the data text with a unique identifier added
// using the messages length since it changes whenever we add one
const message: Message = {
@ -139,21 +112,26 @@ class MessageService {
}
// Add new message to the list
this.messages.push(message);
this.messages.push(message)
return message;
return message
}
}
const app = feathers();
// This tells TypeScript what services we are registering
type ServiceTypes = {
messages: MessageService
}
const app = feathers<ServiceTypes>()
// Register the message service on the Feathers application
app.use('messages', new MessageService());
app.use('messages', new MessageService())
// Log every time a new message has been created
app.service('messages').on('created', (message: Message) => {
console.log('A new message has been created', message);
});
console.log('A new message has been created', message)
})
// A function that creates messages and then logs
// all existing messages on the service
@ -161,40 +139,42 @@ const main = async () => {
// Create a new message on our message service
await app.service('messages').create({
text: 'Hello Feathers'
});
})
// And another one
await app.service('messages').create({
text: 'Hello again'
});
})
// Find all existing messages
const messages = await app.service('messages').find();
const messages = await app.service('messages').find()
console.log('All messages', messages);
};
console.log('All messages', messages)
}
main();
main()
```
:::
::::
<LanguageBlock global-id="ts">
We can run it with
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
```sh
node app.js
npx ts-node app.ts
```
:::
::: tab "TypeScript"
```sh
ts-node app.ts
```
:::
::::
And should see
</LanguageBlock>
<LanguageBlock global-id="js">
We can run it with
```sh
node app.mjs
```
</LanguageBlock>
We will see something like this:
```sh
A new message has been created { id: 0, text: 'Hello Feathers' }
@ -203,122 +183,61 @@ All messages [ { id: 0, text: 'Hello Feathers' },
{ id: 1, text: 'Hello again' } ]
```
Here we implemented only `find` and `create` but a service can also have a few other methods, specifically `get`, `update`, `patch` and `remove`. We will learn more about service methods and events throughout this guide but this sums up some of the most important concepts that Feathers is built on.
Here we implemented only `find` and `create` but a service can also have a few other methods, specifically `get`, `update`, `patch` and `remove`. We will learn more about service methods and events throughout this guide, but this sums up some of the most important concepts upon which Feathers is built.
## An API server
Ok, so we created a Feathers application and a service and we are listening to events but it is only a simple NodeJS script that prints some output and then exits. What we really want is hosting it as an API webserver. This is where Feathers transports come in. A transport takes a service like the one we created above and turns it into a server that other clients (like a web- or mobile application) can talk to.
We created a Feathers application and a service and we are listening to events. However, this is only a simple NodeJS script that prints some output and then exits. What we really want is to host it as an API webserver. This is where Feathers transports come in. A transport takes a service like the one we created above and turns it into a server that other clients (like a web- or mobile application) can talk to.
In the following example we will take our existing service and use
- `@feathersjs/express` which uses Express to automatically turn our services into a REST API
- `@feathersjs/socketio` which uses Socket.io to do the same as a websocket real-time API (as we will see in a bit this is where the `created` event we saw above comes in handy)
- `@feathersjs/koa` which uses [KoaJS](https://koajs.com/) to automatically turn our services into a REST API
- `@feathersjs/socketio` which uses Socket.io to do the same as a WebSocket, real-time API (as we will see in a bit this is where the `created` event we saw above comes in handy).
<LanguageBlock global-id="ts">
```sh
npm install @feathersjs/socketio @feathersjs/express --save
npm install @feathersjs/socketio@pre @feathersjs/koa@pre koa-static @types/koa-static --save
```
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
Then update `app.ts` with the following content:
Update `app.js` with the following content:
</LanguageBlock>
<LanguageBlock global-id="js">
```js
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const socketio = require('@feathersjs/socketio');
Run
// A messages service that allows to create new
// and return all existing messages
class MessageService {
constructor() {
this.messages = [];
}
async find () {
// Just return all our messages
return this.messages;
}
async create (data) {
// The new message is the data merged with a unique identifier
// using the messages length since it changes whenever we add one
const message = {
id: this.messages.length,
text: data.text
}
// Add new message to the list
this.messages.push(message);
return message;
}
}
// Creates an ExpressJS compatible Feathers application
const app = express(feathers());
// Parse HTTP JSON bodies
app.use(express.json());
// Parse URL-encoded params
app.use(express.urlencoded({ extended: true }));
// Host static files from the current folder
app.use(express.static(__dirname));
// Add REST API support
app.configure(express.rest());
// Configure Socket.io real-time APIs
app.configure(socketio());
// Register an in-memory messages service
app.use('/messages', new MessageService());
// Register a nicer error handler than the default Express one
app.use(express.errorHandler());
// Add any new real-time connection to the `everybody` channel
app.on('connection', connection =>
app.channel('everybody').join(connection)
);
// Publish all events to the `everybody` channel
app.publish(data => app.channel('everybody'));
// Start the server
app.listen(3030).on('listening', () =>
console.log('Feathers server listening on localhost:3030')
);
// For good measure let's create a message
// So our API doesn't look so empty
app.service('messages').create({
text: 'Hello world from the server'
});
```sh
npm install @feathersjs/socketio@pre @feathersjs/koa@pre koa-static --save
```
:::
::: tab "TypeScript"
Update `app.ts` with the following content:
Then update `app.mjs` with the following content:
```js
import feathers from '@feathersjs/feathers';
import '@feathersjs/transport-commons';
import express from '@feathersjs/express';
import socketio from '@feathersjs/socketio';
</LanguageBlock>
```ts{2-4,42-55,59-62,64-67}
import { feathers } from '@feathersjs/feathers'
import { koa, rest, bodyParser, errorHandler } from '@feathersjs/koa'
import serveStatic from 'koa-static'
import socketio from '@feathersjs/socketio'
// This is the interface for the message data
interface Message {
id: number;
text: string;
id?: number
text: string
}
// A messages service that allows to create new
// A messages service that allows us to create new
// and return all existing messages
class MessageService {
messages: Message[] = [];
messages: Message[] = []
async find () {
async find() {
// Just return all our messages
return this.messages;
return this.messages
}
async create (data: Pick<Message, 'text'>) {
async create(data: Pick<Message, 'text'>) {
// The new message is the data text with a unique identifier added
// using the messages length since it changes whenever we add one
const message: Message = {
@ -327,147 +246,183 @@ class MessageService {
}
// Add new message to the list
this.messages.push(message);
this.messages.push(message)
return message;
return message
}
}
// Creates an ExpressJS compatible Feathers application
const app = express(feathers());
// This tells TypeScript what services we are registering
type ServiceTypes = {
messages: MessageService
}
// Express middleware to parse HTTP JSON bodies
app.use(express.json());
// Express middleware to parse URL-encoded params
app.use(express.urlencoded({ extended: true }));
// Express middleware to to host static files from the current folder
app.use(express.static(__dirname));
// Add REST API support
app.configure(express.rest());
// Creates an ExpressJS compatible Feathers application
const app = koa<ServiceTypes>(feathers())
// Use the current folder for static file hosting
app.use(serveStatic('.'))
// Register the error handle
app.use(errorHandler())
// Parse JSON request bodies
app.use(bodyParser())
// Register REST service handler
app.configure(rest())
// Configure Socket.io real-time APIs
app.configure(socketio());
app.configure(socketio())
// Register our messages service
app.use('/messages', new MessageService());
// Express middleware with a nicer error handler
app.use(express.errorHandler());
app.use('messages', new MessageService())
// Add any new real-time connection to the `everybody` channel
app.on('connection', connection =>
app.channel('everybody').join(connection)
);
app.on('connection', (connection) => app.channel('everybody').join(connection))
// Publish all events to the `everybody` channel
app.publish(data => app.channel('everybody'));
app.publish((_data) => app.channel('everybody'))
// Start the server
app.listen(3030).on('listening', () =>
console.log('Feathers server listening on localhost:3030')
);
app
.listen(3030)
.then(() => console.log('Feathers server listening on localhost:3030'))
// For good measure let's create a message
// So our API doesn't look so empty
app.service('messages').create({
text: 'Hello world from the server'
});
})
```
:::
::::
Now you can run the server via
<LanguageBlock global-id="ts">
We can start the server with
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
```sh
node app.js
npx ts-node app.ts
```
:::
::: tab "TypeScript"
</LanguageBlock>
<LanguageBlock global-id="js">
We can start the server with
```sh
ts-node app.ts
node app.mjs
```
:::
::::
> __Note:__ The server will stay running until you stop it by pressing Control + C in the terminal.
</LanguageBlock>
And visit `http://localhost:3030/messages` to see an array with the one message we created on the server.
<BlockQuote type="info">
> __Pro Tip:__ The built-in [JSON viewer in Firefox](https://developer.mozilla.org/en-US/docs/Tools/JSON_viewer) or a browser plugin like [JSON viewer for Chrome](https://chrome.google.com/webstore/detail/json-viewer/gbmdgpbipfallnflgajpaliibnhdgobh) makes it nicer to view JSON responses in the browser.
The server will stay running until you stop it by pressing **Control + C** in the terminal.
This is the basic setup of a Feathers API server. The `app.use` calls probably look familiar if you have used Express before. The `app.configure` calls set up the Feathers transport to host the API. `app.on('connection')` and `app.publish` are used to set up event channels which send real-time events to the proper clients (everybody that is connected to our server in this case). You can learn more about channels after finishing this guide in the [channels API](../../api/channels.md).
</BlockQuote>
And in the browser visit
```
http://localhost:3030/messages
```
to see an array with the one message we created on the server.
<BlockQuote>
The built-in [JSON viewer in Firefox](https://developer.mozilla.org/en-US/docs/Tools/JSON_viewer) or a browser plugin like [JSON viewer for Chrome](https://chrome.google.com/webstore/detail/json-viewer/gbmdgpbipfallnflgajpaliibnhdgobh) makes it nicer to view JSON responses in the browser.
</BlockQuote>
This is the basic setup of a Feathers API server.
- The `app.use` calls probably look familiar if you have used something like Koa or Express before.
- `app.configure` calls set up the Feathers transport to host the API.
- `app.on('connection')` and `app.publish` are used to set up event channels, which send real-time events to the proper clients (everybody that is connected to our server in this case). You can learn [more about the channels API](../../api/channels.md) after finishing this guide.
## In the browser
Now we can look at one of the really cool features of Feathers. It works the same way in a web browser! This means that we could take [our first app example](#our-first-app) from above and run it just the same as a website. Since we already have a server running however, let's go a step further and create a Feathers app that talks to our messages service on the server using a real-time Socket.io connection. In the same folder, add the following `index.html` page:
Now we can look at one of the really cool features of Feathers: **It works the same in a web browser!** This means that we could take [our first app example](#our-first-app) from above and run it just the same in a website. Since we already have a server running, however, let's go a step further and create a Feathers app that talks to our `messages` service on the server using a real-time Socket.io connection.
In the same folder, add the following `index.html` page:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Feathers Example</title>
<link rel="stylesheet" href="//unpkg.com/feathers-chat@4.0.0/public/base.css">
<link rel="stylesheet" href="//unpkg.com/feathers-chat@4.0.0/public/chat.css">
</head>
<body>
<main id="main" class="container">
<h1>Welcome to Feathers</h1>
<form class="form" onsubmit="sendMessage(event.preventDefault())">
<input type="text" id="message-text" placeholder="Enter message here">
<button type="submit" class="button button-primary">Send message</button>
</form>
<head>
<meta charset="UTF-8" />
<title>Feathers Example</title>
<link rel="stylesheet" href="//unpkg.com/feathers-chat@4.0.0/public/base.css" />
<link rel="stylesheet" href="//unpkg.com/feathers-chat@4.0.0/public/chat.css" />
</head>
<body>
<main id="main" class="container">
<h1>Welcome to Feathers</h1>
<form class="form" onsubmit="sendMessage(event.preventDefault())">
<input type="text" id="message-text" placeholder="Enter message here" />
<button type="submit" class="button button-primary">Send message</button>
</form>
<h2>Here are the current messages:</h2>
</main>
<h2>Here are the current messages:</h2>
</main>
<script src="//unpkg.com/@feathersjs/client@^4.3.0/dist/feathers.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js"></script>
<script type="text/javascript">
// Set up socket.io
const socket = io('http://localhost:3030');
// Initialize a Feathers app
const app = feathers();
// Register socket.io to talk to our server
app.configure(feathers.socketio(socket));
<script src="//unpkg.com/@feathersjs/client@^5.0.0-pre.24/dist/feathers.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script type="text/javascript">
// Set up socket.io
const socket = io('http://localhost:3030')
// Initialize a Feathers app
const app = feathers()
// Form submission handler that sends a new message
async function sendMessage () {
const messageInput = document.getElementById('message-text');
// Register socket.io to talk to our server
app.configure(feathers.socketio(socket))
// Create a new message with the input field value
await app.service('messages').create({
text: messageInput.value
});
// Form submission handler that sends a new message
async function sendMessage() {
const messageInput = document.getElementById('message-text')
messageInput.value = '';
}
// Create a new message with the input field value
await app.service('messages').create({
text: messageInput.value
})
// Renders a single message on the page
function addMessage (message) {
document.getElementById('main').innerHTML += `<p>${message.text}</p>`;
}
const main = async () => {
// Find all existing messages
const messages = await app.service('messages').find();
messageInput.value = ''
}
// Add existing messages to the list
messages.forEach(addMessage);
// Renders a single message on the page
function addMessage(message) {
document.getElementById('main').innerHTML += `<p>${message.text}</p>`
}
// Add any newly created message to the list in real-time
app.service('messages').on('created', addMessage);
};
const main = async () => {
// Find all existing messages
const messages = await app.service('messages').find()
main();
</script>
</body>
// Add existing messages to the list
messages.forEach(addMessage)
// Add any newly created message to the list in real-time
app.service('messages').on('created', addMessage)
}
main()
</script>
</body>
</html>
```
If you now go to `http://localhost:3030` you will see a simple website that allows to create new messages. It is possible to open the page in two tabs and see new messages show up on either side in real-time. You can verify that the messages got created by visiting `http://localhost:3030/messages`.
If you now in the browser go to
```
http://localhost:3030
```
you will see a simple website that allows creating new messages. It is possible to open the page in two tabs and see new messages show up on either side in real-time. You can verify that the messages got created by visiting
```
http://localhost:3030/messages
```
You'll see the JSON response including all current messages.
## What's next?
In this chapter we created our first Feathers application and a service that allows to create new messages, store them in-memory and return all messages. We then hosted that service as a REST and real-time API server and used Feathers in the browser to connect to that server and create a website that can send new messages, show all existing messages and update with new messages in real-time.
In this chapter we created our first Feathers application and a service that allows creating new messages, storing them in memory, and retrieving them. We then hosted that service as a REST and real-time API server and used Feathers in the browser to connect to that server and create a website that can send new messages and show all existing messages in real-time.
Even though we are using just NodeJS and Feathers from scratch without any additional tools, it was not a lot of code for what we are getting. In the [next chapter](./generator.md) we will look at the Feathers CLI which can create a similar Feathers application with a recommended file structure and things like authentication and database connections set up for us automatically.
Even though we are using just NodeJS and Feathers from scratch without any additional tools, we didn't write a lot of code. In the [next chapter](./generator.md) we will look at the Feathers CLI which can create a similar Feathers application with a recommended file structure, models, database connections, authentication and more.

View File

@ -1,3 +1,7 @@
---
outline: deep
---
# Writing tests
The best way to test an application is by writing tests that make sure it behaves to clients as we would expect. Feathers makes testing your application a lot easier because the services we create can be tested directly instead of having to fake HTTP requests and responses. In this chapter we will implement unit tests for our users and messages services.
@ -14,7 +18,7 @@ This should already pass but it won't be testing any of the functionality we add
When testing database functionality, we want to make sure that the tests use a different database. We can achieve this by updating the test environment configuration in `config/test.json` with the following content:
```js
```json
{
"nedb": "../test/data"
}
@ -32,19 +36,10 @@ npm install shx --save-dev
Now we can update the `scripts` section of our `package.json` to the following:
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
```json
"scripts": {
"test": "npm run eslint && npm run mocha",
"eslint": "eslint src/. test/. --config .eslintrc.json",
"start": "node src/",
"clean": "shx rm -rf test/data/",
"mocha": "npm run clean && NODE_ENV=test mocha test/ --recursive --exit"
}
```
:::
::: tab "TypeScript"
<LanguageBlock global-id="ts">
```json
"scripts": {
"test": "npm run compile && npm run mocha",
@ -55,8 +50,24 @@ Now we can update the `scripts` section of our `package.json` to the following:
"compile": "shx rm -rf lib/ && tsc"
},
```
:::
::::
</LanguageBlock>
<LanguageBlock global-id="js">
```json
"scripts": {
"test": "npm run eslint && npm run mocha",
"eslint": "eslint src/. test/. --config .eslintrc.json",
"start": "node src/",
"clean": "shx rm -rf test/data/",
"mocha": "npm run clean && NODE_ENV=test mocha test/ --recursive --exit"
}
```
</LanguageBlock>
On Windows the `mocha` command should look like this:
@ -70,28 +81,14 @@ This will make sure that the `test/data` folder is removed before every test run
To test the `messages` and `users` services (with all hooks wired up), we could use any REST API testing tool to make requests and verify that they return correct responses.
But there is a much faster, easier and complete approach. Since everything on top of our own hooks and services is already provided (and tested) by Feathers, we can require the [application](../../api/application.md) object and use the [service methods](../../api/services.md) directly. We "fake" authentication by setting `params.user` manually.
There is a much faster, easier and complete approach. Since everything on top of our own hooks and services is already provided (and tested) by Feathers, we can require the [application](../../api/application.md) object and use the [service methods](../../api/services.md) directly. We "fake" authentication by setting `params.user` manually.
By default, the generator creates a service test file that only tests that the service exists.
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
E.g. like this in `test/services/users.test.js`:
```js
const assert = require('assert');
const app = require('../../src/app');
describe('\'users\' service', () => {
it('registered the service', () => {
const service = app.service('users');
<LanguageBlock global-id="ts">
assert.ok(service, 'Registered the service');
});
});
```
:::
::: tab "TypeScript"
E.g. like this in `test/services/users.test.ts`:
```ts
@ -106,14 +103,12 @@ describe('\'users\' service', () => {
});
});
```
:::
::::
We can then add similar tests that use the service. The first test below verifies that users can be created, the profile image gets set and the password gets encrypted. The second verifies that the password does not get sent to external requests:
</LanguageBlock>
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
Replace `test/services/users.test.js` with the following:
<LanguageBlock global-id="js">
E.g. like this in `test/services/users.test.js`:
```js
const assert = require('assert');
@ -125,35 +120,19 @@ describe('\'users\' service', () => {
assert.ok(service, 'Registered the service');
});
it('creates a user, encrypts password and adds gravatar', async () => {
const user = await app.service('users').create({
email: 'test@example.com',
password: 'secret'
});
// Verify Gravatar has been set as we'd expect
assert.equal(user.avatar, 'https://s.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=60');
// Makes sure the password got encrypted
assert.ok(user.password !== 'secret');
});
it('removes password for external requests', async () => {
// Setting `provider` indicates an external request
const params = { provider: 'rest' };
const user = await app.service('users').create({
email: 'test2@example.com',
password: 'secret'
}, params);
// Make sure password has been removed
assert.ok(!user.password);
});
});
```
:::
::: tab "TypeScript"
</LanguageBlock>
We can then add similar tests that use the service. The first test below verifies that users can be created, the profile image gets set and the password gets encrypted. The second verifies that the password does not get sent to external requests:
<LanguageBlock global-id="ts">
Replace `test/services/users.test.ts` with the following:
```ts
@ -193,52 +172,61 @@ describe('\'users\' service', () => {
});
});
```
:::
::::
We take a similar approach for the messages service test. We create a test-specific user from the `users` service, then pass it as `params.user` when creating a new message and validates that message's content:
</LanguageBlock>
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
Update `test/services/messages.test.js` as follows:
<LanguageBlock global-id="js">
Replace `test/services/users.test.js` with the following:
```js
const assert = require('assert');
const app = require('../../src/app');
describe('\'messages\' service', () => {
describe('\'users\' service', () => {
it('registered the service', () => {
const service = app.service('messages');
const service = app.service('users');
assert.ok(service, 'Registered the service');
});
it('creates and processes message, adds user information', async () => {
// Create a new user we can use for testing
it('creates a user, encrypts password and adds gravatar', async () => {
const user = await app.service('users').create({
email: 'messagetest@example.com',
password: 'supersecret'
email: 'test@example.com',
password: 'secret'
});
// The messages service call params (with the user we just created)
const params = { user };
const message = await app.service('messages').create({
text: 'a test',
additional: 'should be removed'
// Verify Gravatar has been set as we'd expect
assert.equal(user.avatar, 'https://s.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=60');
// Makes sure the password got encrypted
assert.ok(user.password !== 'secret');
});
it('removes password for external requests', async () => {
// Setting `provider` indicates an external request
const params = { provider: 'rest' };
const user = await app.service('users').create({
email: 'test2@example.com',
password: 'secret'
}, params);
assert.equal(message.text, 'a test');
// `userId` should be set to passed users it
assert.equal(message.userId, user._id);
// Additional property has been removed
assert.ok(!message.additional);
// `user` has been populated
assert.deepEqual(message.user, user);
// Make sure password has been removed
assert.ok(!user.password);
});
});
```
:::
::: tab "TypeScript"
</LanguageBlock>
We take a similar approach for the messages service test. We create a test-specific user from the `users` service, then pass it as `params.user` when creating a new message and validates that message's content:
<LanguageBlock global-id="ts">
Update `test/services/messages.test.ts` as follows:
```ts
@ -276,8 +264,52 @@ describe('\'messages\' service', () => {
});
});
```
:::
::::
</LanguageBlock>
<LanguageBlock global-id="js">
Update `test/services/messages.test.js` as follows:
```js
const assert = require('assert');
const app = require('../../src/app');
describe('\'messages\' service', () => {
it('registered the service', () => {
const service = app.service('messages');
assert.ok(service, 'Registered the service');
});
it('creates and processes message, adds user information', async () => {
// Create a new user we can use for testing
const user = await app.service('users').create({
email: 'messagetest@example.com',
password: 'supersecret'
});
// The messages service call params (with the user we just created)
const params = { user };
const message = await app.service('messages').create({
text: 'a test',
additional: 'should be removed'
}, params);
assert.equal(message.text, 'a test');
// `userId` should be set to passed users it
assert.equal(message.userId, user._id);
// Additional property has been removed
assert.ok(!message.additional);
// `user` has been populated
assert.deepEqual(message.user, user);
});
});
```
</LanguageBlock>
Run `npm test` one more time, to verify that all tests are passing.
@ -291,21 +323,10 @@ npm install nyc --save-dev
Now we have to update the `scripts` section of our `package.json` to:
:::: tabs :options="{ useUrlFragment: false }"
::: tab "JavaScript"
```js
"scripts": {
"test": "npm run eslint && npm run coverage",
"coverage": "nyc npm run mocha",
"eslint": "eslint src/. test/. --config .eslintrc.json",
"dev": "nodemon src/",
"start": "node src/",
"clean": "shx rm -rf test/data/",
"mocha": "npm run clean && NODE_ENV=test mocha test/ --recursive --exit"
},
```
:::
::: tab "TypeScript"
<LanguageBlock global-id="ts">
For TypeScript we also have to install the TypeScript reporter:
```sh
@ -337,8 +358,26 @@ And then update the `package.json` like this:
"compile": "shx rm -rf lib/ && tsc"
},
```
:::
::::
</LanguageBlock>
<LanguageBlock global-id="js">
```js
"scripts": {
"test": "npm run eslint && npm run coverage",
"coverage": "nyc npm run mocha",
"eslint": "eslint src/. test/. --config .eslintrc.json",
"dev": "nodemon src/",
"start": "node src/",
"clean": "shx rm -rf test/data/",
"mocha": "npm run clean && NODE_ENV=test mocha test/ --recursive --exit"
},
```
</LanguageBlock>
On Windows, the `coverage` command looks like this:
@ -359,4 +398,4 @@ This will print out some additional coverage information.
## What's next?
Thats it - our chat guide is completed! We now have a fully-tested REST and real-time API, with a plain JavaScript frontend including login and signup. Follow up in the [Feathers API documentation](../../api/index.md) for more details about using Feathers, or start building your own first Feathers application!
Thats it - our chat guide is completed! We now have a fully-tested REST and real-time API, with a plain JavaScript frontend including login and signup. Follow up in the [Feathers API documentation](../../api/) for more details about using Feathers, or start building your own first Feathers application!

View File

@ -1,22 +1,26 @@
---
outline: deep
---
# Getting started with Feathers
Welcome to the Feathers guides! This is the place to find all the resources to get started with Feathers.
<img style="margin: 2em 0;" src="/crow/key-image-horizontal.png" alt="Feathers key image">
<img style="margin: 2em 0;" src="/img/main-character-starting.svg" alt="Setting up">
## The Feathers guide
The Feathers guide will walk you through all the important parts of Feathers. After [setting up](./basics/setup.md), the [quick start](./basics/starting.md) gets you up and running with a Feathers REST API and real-time website in less than 15 minutes from scratch to give you an idea what Feathers is about.
The Feathers guide will walk you through all the important parts of Feathers. The [quick start](./basics/starting.md) gets you up and running with a Feathers API and real-time website in less than 15 minutes from scratch to give you an idea what Feathers is about.
In the next parts we will [generate an application](./basics/generator.md) and then walk through Feathers core concepts like services, hooks and authentication by building a complete real-time chat application with a REST API and a website that can register users and send and receive messages in real-time. We will also add a login with GitHub and write unit tests for our API.
In the next parts we will [generate an application](./basics/generator.md) and then walk through Feathers core concepts like services, hooks and authentication by building a complete real-time chat application with an API and a website that can register users and send and receive messages in real-time. We will also add a login with GitHub and write unit tests for our API.
[Get started with the Feathers guide >](./basics/setup.md)
[Get started with the Feathers guide >](./basics/starting.md)
## Follow up with
[The API documentation >](../api/index.md)
[The API documentation >](../api/)
[The cookbook for common tasks and patterns >](../cookbook/index.md)
[The cookbook for common tasks and patterns >](../cookbook/)
[The Awesome FeathersJS Ecosystem >](https://github.com/feathersjs/awesome-feathersjs)