@projectinvicta/nails
v3.0.1
Published
A node.js webserver scaffold
Downloads
208
Readme
Nails: A Node Webservice Framework
Nails is a lightweight, configurable MVC-inspired backend framework for Node.js. With minimal dependencies, Nails offers a familiar syntax for developers coming from frameworks like Ruby on Rails or Django, while providing the flexibility of JavaScript.
Nails provides the essential building blocks to get your MVC-style application up and running quickly. The modules used in Nails can be easily extended to produce the custom functionality to fit your needs, and you are encouraged to do so.
Getting Started
There are two recommended ways to get started with Nails.
Using NPX (Recommended)
You can create a new Nails project without a global installation using npx:
npx @projectinvicta/nails init <app_name>Using Global Install
Alternatively, you can install the package globally:
npm install -g @projectinvicta/nails
nails init <app_name>After initializing your app, navigate into the new directory and start the server:
cd <app_name>
npm install
npm startGetting to know your Nails service
For your convenience, here is a quick outline of the main components of a nails service. Remember: each object comes with an example file to use for reference when building your service.
Named Exports
nails now provides a number of useful named exports. You can import them as follows:
import Nails, { Controller, Model, DataTypes, /* config types */ } from '@projectinvicta/nails';Config
Your configuration files are stored in app_name/config/.
Each default config file is annotated with comments documenting each field to help you tailor your service to your needs.
service.js
service.js contains information necessary to run your server. By default, it specifies the port and the location of important libraries.
export default {
...
SERVER_ROOT: "/path/to/my/nails/service",
PORT: 3333,
SSL_PORT: 3334,
}While most of these values don't need to be changed, feel free to add custom fields. The resulting config will be available to your service through the nails module:
import Nails from '@projectinvicta/nails';
const service_config = nails.configIf the config contains a custom field,
export default {
...
PORT: 3000,
yourCustomField: 'yourCustomValue'
}then service_config.yourCustomField as defined above will be equal to
'yourCustomValue'.
routes.js
routes.js is a list defining mappings from a url path to a Controller and
Action. Routes are now strongly typed (see lib/config.ts and lib/Router.ts). Each entry in the list is an array with three elements:
[method, path, options]
method is a string defining the HTTP request method of the route. Supported methods are GET, PUT, POST, DELETE, and ALL. All is a special case which matches all HTTP request methods. Lastly, WS routes will handle WebSocket connections.
path is a string or regular expression which matches the path of the
incoming request. If path is a string, then the request must match exactly*.
You can use route parameters to dynamically match parts of the path and assign
them to the params object. For example, if you define a route with the path:
'/users/:userId'
and your service receives a request with the path:
'/users/777'
then userId will be set in the params object:
{ userId: 777 }You can define :controller and :action as route parameters as well. Not only will those values be set in the params object, but the request will be routed to the matching controller and action. See express routes for more information on how route parameters work.
*requests for static assets will only match the prefix of the path.
options is a JSON object which modifies how the request will be routed:
- controller a String indicating the controller to route to.
- action a String indicating the action to route to.
- json a boolean indicating whether to render a JSON response rather than an HTML response. If true, nails will not attempt to render a view for this route. Instead, your service will respond with JSON for this route.
- public a boolean indicating whether this route is for static assets. If
true, the router will only attempt to match the prefix of the request path.
The child portion of the path will be forwarded to the public/ folder in
your service directory. For route:
['GET', '/public_url_path', {public: true}]if your service receives a request to:/public_url_path/js/index.jsthen the response will be the file:your_service_root_path/public/js/index.js - 0, 1, 2... a string which gives regex captures named keys in the params object. This will give your regex captures more meaningful named keys rather than indices. You can name your regex captures "controller" and/or "action" to dynamically route your request to the appropriate handler.
db.js
Quickly configure your database connection here. The initial setup uses a sqlite3 database file config/development.db
and an in-memory database in the test environment. Change the address to change the location and
version of your desired sql database. Check out Sequelize for more info.
Controller
Controllers are defined in app/controllers/. Each controller module should define a Controller subclass. The name will be used to match routes defined in config/routes.js for incoming requests. Methods on the controller can be used as actions, receiving params, request, and response as arguments.
For Example:
import { Controller } from '@projectinvicta/nails';
class HomeController extends Controller {
index(params, request, response) {
// default action
}
signin(params, request, response) {
// does something then renders a view
}
}
export default HomeController;
function helperMethod() {
// does something but does not have access to response
}defines a controller which will match any route to home#<action>. index and signin are actions which can be used to render a response to the client.
Local Routes
You can define a local routing table directly in the controller. Local routes take precidence over global routes. All local routes are prefixed with the controller name unless they start with '/'. For example, in HomeController the following route:
["get", "data", {action: 'getData', json: true}],
will accept GET requests to /home/data and respond with the json object returned by the getData function. If the route is changed to:
["get", "/data", {action: 'getData', json: true}],
it will accept GET requests to /data instead. All local routes are implicitly routed to their respective parent controllers.
import { Controller } from '@projectinvicta/nails';
export default class UsersController extends Controller {
routes = [
// Routes requests to /absolute/path
['get', '/absolute/path', {action: 'actionA'}],
// Routes requests to /users/relative/path
['get', './relative/path', {action: 'actionB'}],
// Routes requests to /users/relative/path
['get', 'relative/path', {action: 'actionB'}],
// If no action is provided, the last path segment
// is used as the action name.
['get', 'actionC']
]
// Handles requests to /absolute/path
actionA(request, response, params) {}
// Handles requests to /users/relative/path
actionB(request, response, params) {}
// Handles requests to /users/actionC
actionC(request, response, params) {}
}Actions
Actions are used to define how nails should respond to an incoming request. If no action has been defined for a route, nails will default to the index action.*
For example, HomeController#index will attempt to render the view defined in //app/views/home/index.jsx
The view each action searches for will always follow the pattern: //app/views/[controller name]/[action name].jsx
The file extension may differ based on which template engine you configure.
Depending on the return value, Nails will pass a different set of parameters to the view engine:
- undefined If there is no return statement in the action, Nails will pass the params obect to the rendering engine.
- Object If a generic object is returned, Nails will attempt to autorender the view immediately using the returned object instead of params.**
- Promise If a promise is returned, Nails will wait to autorender the view until the Promise resolves. If it resolves with no return value, the view is rendered using params. Otherwise, the view is rendered using the resolved value of the Promise**
*If a response has already been sent to the client, autorender will be skipped. **For JSON routes, the returned object will be rendered as stringified JSON.
Params
Params is a generic JSON object which represents the request details. Usually, Params will correspond to the query portion of your request.
For example, a GET request to //some/path?item0=a&item1=b will generate the params object:
{
item0: "a",
item1: "b"
}Request
Response
The response object provided by express.js. The #render() method has been overridden to allow for the rendering of views by name.
JSON Actions
JSON actions only return JSON objects in the response instead of text or HTML. These actions are ideal for building an API Server. There are three ways to designate JSON Actions:
Express Response Object
response.json({your: 'jsonresponse'})Simply use the express Response object directly
JSON Routes
You can configure an individual route to respond with JSON by setting the json option to true.
['get', '/your/json/route', {json: true}],API Controllers
By setting json to true, all actions in a controller will respond with JSON by default.
This can be overridden for individual routes by setting {json: false} in the route options.
import { Controller } from '@projectinvicta/nails';
class YourApiController extends Controller {
json = true;
action(params, request, response) {
return {your: 'jsonresponse'};
}
}Model
Models are programmatic representations of data you wish to persist in a database. Nails connects to your database of choice using the Sequelize ORM.
Sequelize Models
Sequelize models are subclasses of
Sequelize Models, and come with the count(), findAll(),
and create() methods, to name a few. You can define your own models by
extending an instance of the Model class provided by Nails. Model files must export the Model subclass as a default export and a named schema export.
They can also export an options object to provide ModelInitializationOptions as well as defer, finalize, and migrate functions.
import { Model, DataTypes } from '@projectinvicta/nails';
// REQUIRED
export const schema = {
name: {type: DataTypes.STRING, allowNull: false},
email: {type: DataTypes.STRING, allowNull: false}
};
// Optional
export const options = {
indexes: [
{
unique: true,
fields: ['email'],
},
],
};
// REQUIRED
export default class User extends Model {
someHelperMethod() {
// This method will be available on all instances of User and is
// an ideal way to simplify data manipulation.
}
};
// Optional
export async function defer(models) {
// define associations here
}
// Optional
export async function finalize(models) {
// any final model setup
}
// Optional
export async function migrate(queryInterface) {
// migration logic
}Model Library
Nails will store all instantialized models in a single object called MODELS. By accessing these models via the library, you can avoid circular dependencies and ensure all models have been fully initialized.
import Nails, { Model } from '@projectinvicta/nails';
// ...
class User extends Model {
// A helper method which depends on anoher model using the
// Nails Model Library rather than directly importing the model.
async findFriends() {
return nails.MODELS.Friend.findByUserId(this.id);
}
}This design pattern is not always necessary, but will help avoid circular dependencies.
View
Views are dynamic templates used to render an html response for a browser. Nails comes prepackaged with EJS templates. If no template engine is specified in the service config, Nails will default to EJS. Nails will always attempt to autorender your views unless a response has already been sent to the client.
React Frontend with Vite
This project uses Vite to build the frontend React application. The source files for the React app are located in the src directory. The server is pre-configured to serve the built React application.
To rebuild the frontend application, you can run the following command:
npm run buildTesting
This project uses Vitest for running tests. All test files are located in the spec folder. To run the tests, use the following command:
npm testFor more information on how to write tests with Vitest, please refer to the official documentation.
Stay tuned as nails evolves:
- Server/client redirects
- Custom Request middleware
- Fancy Logging
- Sessions
- Server security
Enjoy! Feature requests, bug reports, and comments are welcome on github.
