After two months of work, I have finally released the Alpha version of Stampede, a framework, or a very powerful eco-system based on Deno (a recently released TypeScript runtime). As I am very busy working full-time, it took more time than imagined to finally complete the initial version.
One of the coolest aspects of Stampede is that it already has a scaffolded Authentication system, tons of autowired modules which will be very useful once you start adding new features, as well as dozens of internal micro-libraries which speed up the whole development process.
Let’s take a look at the features below and a small tutorial on setting up your local environment and adding new features to your next project built with Stampede:
Link to the repository: https://github.com/bashovski/stampede
Features
- CLI - generate modules instantly and make progress without fuss!
- Convenient M(V)CS structure - strong emphasis on separation of concerns
- Autowired modules and loaders - implicitly run codebase without piling up large imports
- Authentication system ready for use
- Modified Koa Router utilization - easy-to-use Routing, HTTP request handlers, etc.
- Deployment configuration which is production-ready
- View layer written in Vue.js - includes auth pages, guards, etc.
- DenoDB ORM along with PostgreSQL
- Pre-written migrations with Umzug and Sequelize (there's raw SQL as well)
- Feature Flags - toggle which features should be running even during server runtime
- Autowired config - adding and consuming environment variables kept elegant
- Multi-level logger with DataDog support (allows usage of facets and tags)
- Custom Mail module/internal micro-library compatible with SendGrid
- Validators - validate and restrict invalid user input with large variety of options
- Factories - populate your database with false records (powered by faker.js)
- Route Guards for your Vue SPA - modify the behavior of page access layer the way you want to
- Autowired CRON jobs
- Unit tests with bootstrappers
- Insomnia and Swagger doc for existing REST API endpoints
- and many more...
Setup/Installation
Stampede is made as a repository/template, so you can easily use it as a fundament for your next project.
In the image below, just click the ‘Use this template’ button.
Now you should pick an appropriate name for your project:
After creating and cloning your repository, we can already focus on using vital pieces of software which make Stampede very powerful at its very beginning.
Installing the CLI
Stampede’s CLI is used primarily to generate a few very consistent modules directly from templates. You can always update your templates inside /assets/module_templates directory.
Initially, CLI provides generation of models, routings, services and controllers. Later, new features will be added.
You can always check out the CLI repo: https://github.com/bashovski/stampede-cli
To install the CLI, as a MacOS user, run these commands:
brew tap bashovski/stampede
brew install stampede
stampede --help # Test if it works
You should be able to run the CLI normally. For other operating systems, check the official documentation/repo from the link above.
Setting up the environment variables
The next step is to create an .env file and update variables inside of it. In this example, we’ll only update mandatory ones. In the code sample below, we’ll create one using .env.example file as an example
cp .env.example .env
The following four environment variables are considered mandatory:
DB_HOST="0.0.0.0"
DB_PASSWORD=""
DB_USERNAME="postgres"
DB_NAME="stampede"
Update them with your DB’s connection data, so we can later run migrations with Umzug.
The next step is to install Node dependencies, as Umzug can only be ran using Node. Please note that we won’t use Node at any other point, only while running migrations as running Node beats the purpose of using Deno. My own sophisticated migrations tool is in the Stampede Roadmap, but we are still far away from reaching that stage.
npm i
Once we have installed Node dependencies, we are ready to go.
Let’s first run the server (the first command), and later we’ll serve the UI.
./scripts/run # serves the API
./scripts/ui # serves the UI
To have both running simultaneously, open another tab/window in the terminal and run the second command from the root project directory.
If you did everything as it’s been shown in the previous steps, you have managed to have everything up and running. The migrations have been executed and the database is up-to-date with model schemas.
You should be receiving this response at: localhost:80/
For UI, you ‘ll need to install node modules by cd-ing into the ui directory and running ‘npm i’ command from it. After that you can serve UI.
Creating your own models, services and controllers
I personally consider this the most important part when it comes to not only understanding how Stampede works, but also seeing how easy it is to deliver swiftly and yet avoid a bunch of code written for no good reason.
Almost everything is autowired in the Stampede Framework, hence it’s important to develop a sense for what’s and what’s not already on the server.
Once you create a module (whose category considers them to be repeated), you don’t need to explicitly import and mention its usage. It’s already there.
Models, services, controllers, CRON jobs, factories, migrations, configuration files, routes and tests are autowired types of modules.
We’ll now create everything required for a model named Post — a replica of posts from literally any other website.
If you have installed a Stampede CLI, just run:
stampede model Post
stampede svc Post
stampede ctrl Post
These three commands will create a model named Post, a service named PostService and a controller named PostController. Interestingly, when you’ve created a model, you also created something else: a routing module for the Post model (PostRouting.ts)
All of them will be placed at their respective subdirectories: /models, /routes, /services and /controllers.
Let’s add each required field and a function to generate/persist one record of a model named Post:
In the method generate(), we’ll need to know the Post owner and post’s data, hence we are passing the user’s ID and fields (derived from the request’s body).
This generate() function will be invoked from PostService.ts later:
import { DataTypes } from 'https://raw.githubusercontent.com/eveningkid/denodb/abed3063dd92436ceb4f124227daee5ee6604b2d/mod.ts';
import Model from '../lib/Model.ts';
import Validator, { ValidationRules } from "../lib/Validator.ts";
import { v4 } from "https://deno.land/std/uuid/mod.ts";
class Post extends Model {
static table = 'posts';
static timestamps = true;
static fields = {
id: {
type: DataTypes.UUID,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
allowNull: false
},
title: {
type: DataTypes.STRING,
allowNull: false
},
content: {
type: DataTypes.STRING,
allowNull: false
},
thumbsUp: {
type: DataTypes.INTEGER,
allowNull: false
}
};
public static async generate(userId: string, fields: any): Promise<any> {
const rules: ValidationRules = {
title: {
required: true,
minLength: 8,
maxLength: 256
},
content: {
required: true,
minLength: 3,
maxLength: 4096
}
};
const { success, invalidFields } = await Validator.validate(rules, fields);
if (!success) {
return { invalidFields };
}
const post = await Post.create({
id: v4.generate(),
userId,
...fields,
thumbsUp: 0
});
return { post };
}
}
export default Post;
Let’s now create a migration for the model above inside /db/migrations directory:
const { Sequelize } = require('sequelize');
const tableName = 'posts';
async function up(queryInterface) {
await queryInterface.createTable(tableName, {
id: {
type: Sequelize.UUID,
primaryKey: true
},
user_id: {
type: Sequelize.UUID,
allowNull: false
},
title: {
type: Sequelize.STRING,
allowNull: false
},
content: {
type: Sequelize.STRING,
allowNull: false
},
thumbs_up: {
type: Sequelize.INTEGER,
allowNull: false
},
created_at: {
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
allowNull: false
},
updated_at: {
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
allowNull: false
}
});
}
async function down(queryInterface) {
await queryInterface.dropTable(tableName);
}
module.exports = { up, down };
After adding those two, we should specify two routes for posts. Using one of them we are going to use to fetch all posts and create a new post and persist it into the database.
import Router from '../lib/Router.ts';
import PostController from '../controllers/PostController.ts';
import AuthMiddleware from '../middleware/AuthMiddleware.ts';
Router.get('/posts', PostController.index);
Router.post('/posts', AuthMiddleware.authenticateUser, PostController.create);
The first route doesn’t require the user to be logged in.
Now, let’s add a controller for our model. It handles request data and transports it into other layers of the server (services, helpers, model methods, etc.)
mport Controller from './Controller.ts';
import PostService from '../services/PostService.ts';
import Logger from '../lib/Logger.ts';
/**
* @class PostController
* @summary Handles all Post-related HTTP requests
*/
class PostController extends Controller {
/**
* @summary Handles index request
* @param ctx
*/
public static async index(ctx: any): Promise<void> {
try {
const { response, error } : any = await PostService.index();
(response || error).send(ctx.response);
} catch(error) {
Logger.error(error);
super.sendDefaultError(ctx);
}
}
public static async create(ctx: any): Promise<void> {
try {
const userId = await super.getUserId(ctx);
const fields = await (ctx.request.body().value) || {};
const { response, error } : any = await PostService.create(userId, fields);
(response || error).send(ctx.response);
} catch(error) {
Logger.error(error);
super.sendDefaultError(ctx);
}
}
}
/**
* @exports Post
*/
export default PostController;
Now let’s add a service, which already knows the input data and prepares the request response, server state mutations and resource persistence:
import Service, { ServiceResult } from './Service.ts';
import HttpResponse from '../http/HttpResponse.ts';
import Post from '../models/Post.ts';
import HttpError from '../http/HttpError.ts';
class PostService extends Service {
/**
* @summary Index of all Post REST resources
* @returns {ServiceResult}
*/
public static async index(): Promise<ServiceResult> {
return {
response: new HttpResponse(200, {
posts : await Post.all()
})
};
}
public static async create(userId: string, fields: any): Promise<ServiceResult> {
const { invalidFields, post } = await Post.generate(userId, fields);
if (invalidFields) {
return {
error: new HttpError(400, invalidFields)
};
}
if (!post) {
return {
error: new HttpError(500, 'Internal Server Error')
};
}
return {
response: new HttpResponse(200, {
message: 'Successfully created a new Post'
})
};
}
}
export default PostService;
Once you’ve added these, you ’ll be ready to restart the server and the new migration will be executed and you’ll be able to create new posts and list them all. Congrats!
The Stampede Framework at this stage is in alpha. I’ll keep on improving and stabilizing the framework and also provide guides on upgrading to the newer versions.
I am looking forward to seeing you use Stampede soon!
Top comments (1)
I just want to say, this is the most complete web framework I've seen for Deno so far, and I think it's highly underrated. Thanks for your efforts!
Is there a reason it's not listed on Deno's website under third-party packages?