Getting Started Β Β Β Examples Β Β Β Authentication Β Β Β Validation Β Β Β Dependency Injection Β Β Β Testing
- π Batteries includes: Cors, dependency injection and body parsing are already set up.
- π Written in Typescript: No need to worry about inconsistent types.
- π€·ββοΈ Unopinionated: We don't force you to do anything.
- π Built on Express: Warp is compatible with all existing Express packages.
- β¨ Support for
async
/await
: Warp helps you escape the callback hell. - π₯ Easy to get started with: One file with a few lines of code is all you need.
- π Built in authentication: Warp has build in support for token authentication.
About a year ago I fell in love with Nest.js, however after building a couple of bigger projects with it I noticed, that it forces me to do things in counterintuitive ways while offering features that I hardly ever used. On the other hand, Express is great but really barebones. You have to set up body parsing, authentication and routing for every project.
Warp aims to be a combination of the great API that Nest.js offers while maintaining the simplicity of Express. Warp is a clever combination of a few standard packages, which together offer controller based routing, authentication, dependency injection, validation and reduce code duplication.
Warp CLI creates a simple TypeScript project with a very basic Warp API and test.
# Create warp project in current directory
npx create-warp-app
# Create warp project in (./my-api)
npx create-warp-app ./my-api
You can easily set up your own warp project. This guide assumes that you already have a Node.js project with TypeScript set up.
# Using npm
npm install @varld/warp
# Using yarn
yarn add @varld/warp
Warp uses decorators and reflection, those two features have to be enabled in the tsconfig.json
file.
{
"compilerOptions": {
...
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
import warp, { Controller, Get, Param } from '@varld/warp';
@Controller('/')
class GreetingController {
@Get('/:name')
greet(@Param('name') name: string) {
return `Hello ${name}`;
}
}
let app = warp({
controllers: [GreetingController]
});
app.listen(3000);
That's it! π Go ahead and visit http://localhost:3000/your-name
in a browser.
Now let's take a look at what the code above does exactly.
- On the first line, we import
warp
and a few decorators, which we are using later. - Nest, we create a controller-class. All controllers must have the
@Controller('/...')
decorator, with the base-path of the controller as the first argument. - Within the controller class we create an HTTP-Handler (called greet) using the
@Get('/...')
decorator. This decorator indicates, that the handler listens toGET
request under the path supplied in the first argument. Of course there are decorators for all other HTTP-Methods as well. - The
greet
has a single argument decorated with@Param('name')
. This indicates, that we want to extract apath-param
calledname
from the url. - Withing the
greet
method we return a string containing the supplied name. - Now we set up a warp-instance and tell it about our controller. The
warp
function returns a standard Express app. - Finally we tell the warp-instance to listen to port 3000.
Pretty simple, right. βοΈ
While Warp does not force you do adhere to a specific file structure, there is one that we think works very well.
/src
package.json
index.ts
/controllers
controllerName.ts
/services
serviceName.ts
/tests
testName.spec.ts
In the root of the project, there is an index.ts
-file, which create a Warp instance and sets everything up.
The controllers
directory houses all controllers. Each controller should have its own file named accordingly.
The services
directory contains all services. Each file should contain just one service.
If you are using authentication, you could extract the authenticator into its own file.
Warp has decorators for all popular HTTP-Methods. They all basically work the same.
@Controller('/')
class MyController {
@Get('/') // Listens to GET requests
getSomething() {
/*...*/
}
@Post('/') // Listens to POST requests
createSomething() {
/*...*/
}
@Put('/') // Listens to PUT requests
overrideSomething() {
/*...*/
}
@Patch('/') // Listens to PATCH requests
updateSomething() {
/*...*/
}
@Delete('/') // Listens to DELETE requests
deleteSomething() {
/*...*/
}
@Head('/') // Listens to HEAD requests
headRequest() {
/*...*/
}
@Options('/') // Listens to OPTIONS requests
optionsRequest() {
/*...*/
}
}
Warp is compatible with all existing Express middleware, however installing it is a bit different.
Global middleware is executed on every request. You can optionally specify if it should be executed before or after the request handler methods.
// Default behavior
let app = warp({
controllers: [
/*...*/
],
middleware: [
aMiddlewareFunction,
anotherOne
// ...
]
});
// Specify when the middleware should be executed
let app = warp({
controllers: [
/*...*/
],
middleware: [
{
// Will be executed before request handlers
execution: 'before',
middleware: setupMiddleware
},
{
// Will be executed after request handlers
execution: 'after',
middleware: cleanupMiddleware
}
// ...
]
});
Controller-based middleware will be executed before every handler method within a controller. This is useful if you want to perform an action for every handler of a controller.
@Controller('/', [
aMiddlewareFunction,
anotherOne
// ...
])
class MyController {
// ...
}
Handler-based middleware is specific to selected request handlers. Handler-based middleware can be specified using the second parameter of @Get
or any other method decorator or using the @Middleware()
decorator. You can also use bot variant together.
@Get('/:name', [
aMiddlewareFunction,
// ...
])
@Middleware(anotherMiddlewareFunction)
@Middleware([
aMiddlewareFunctionInAnArray,
oneMore,
// ...
])
handler() {
// ...
}
As the name suggests, guards are used to protect handlers. Guards are decorators, which receive a function that returns either true
or false
. If true
is returned, the handler will be executed normally. If false
is returned, the handler will not be executed and a Forbidden
-Error will sent to the client instead. The function in the guard receives the request
object as its first argument.
Guards are especially useful if you want to make a handler only accessible to users with special permissions.
@Get('/')
@Guard((req) => false)
handler() {
return `This will not be executed`;
}
By default, everything you return in a handler function will be sent to client with the 200
status code. If you return a string, that string will sent to the client as is. If you return an object, that object will be converted to JSON. Depending on the return type a matching Content-Type
-header will be chosen. You can use responses to manipulate this behavior.
Everything you provide to the JSON-Response will be converted to JSON. In addition to the response data you can also specify a custom status code and additional http-headers.
@Get('/')
handler() {
return new JSONResponse({
/* some data */
});
}
// With a statuscode and custom headers
@Get('/')
handler() {
return new JSONResponse({
/* some data */
}, 202, {
'Custom': 'Header'
});
}
File responses can be used to serve local files.
new FileResponse(filePath, [expressSendFileOptions, status, headers])
The next response can be used to call Express's next()
function. Which tells Express to continue to the next middleware.
new NextResponse()
Using the redirect response, you can redirect the client to a different url.
new RedirectResponse(location, [status, headers])
The render response can be used to access Express's internal template rendering. For this to work you must configure a view engine first.
import warp, { Controller, Get, Param } from '@varld/warp';
@Controller('/')
class MyController {
@Get('/')
render() {
return new RenderResponse('view name', {
/* view data */
});
}
}
let app = warp({
controllers: [MyController]
});
app.set('view engine', 'a view engine');
app.listen(3000);
The simple response behaves similar to just returning the value, however using the simple response you can optionally add a status code and custom headers.
new SimpleResponse(data, [status, headers])
Warp offers a variety of decorators you can use to get request data, like path parameters, query, cookies and more.
You can access query parameters using the @Query()
decorator. If you want to get a specific value from the query object you can also pass the value's name as a parameter.
@Controller('/')
class MyController {
// Get all query parameters as an object
@Get('/all')
handler(@Query() everything: any) {
// ...
}
// Get a single query parameter called name
@Get('/one')
handler(@Query('name') name: string) {
// ...
}
}
The header parameter behaves similar to the @Query()
. When @Header()
receives no argument you will get an object containing all header values, however you can optionally specify a header name as the first parameter.
@Controller('/')
class MyController {
// Get all headers as an object
@Get('/all')
handler(@Header() everything: any) {
// ...
}
// Get the "Content-Type" header
@Get('/one')
handler(@Header('Content-Type') contentType: string) {
// ...
}
}
Using the cookie parameter, you can easily access cookies. When no parameter is specified, you will get an array containing all cookies. Optionally you can specify a cookie's name in the first parameter.
@Controller('/')
class MyController {
// Get all cookies as an object
@Get('/all')
handler(@Cookie() everything: any) {
// ...
}
// Get a single cookie called token
@Get('/one')
handler(@Cookie('token') token: string) {
// ...
}
}
Warp also has decorators for setting and clearing cookies.
@Controller('/')
class MyController {
@Get('/')
handler(@SetCookie() setCookie: CookieSetter, @ClearCookie() clearCookie: CookieClearer) {
setCookie('name', 'value', {
/* options */
});
clearCookie('name');
}
}
You can use setCookie
to set cookies. The first param must be the cookies name, the second param is the cookies value using the third param, you can optionally specify additional options, like an expiration date.
clearCookie
can be used to remove a cookie. The first parameter is the cookies name.
Warp inherits support for parameters in the URL path from Express. You can use the @Param()
decorator to access all or one specify url parameter.
@Controller('/')
class MyController {
// Get all params as an object
@Get('/users/:userId/task/:taskId')
handler(@Param() everything: any) {
// ...
}
// Get a single param called userId
@Get('/users/:userId')
handler(@Param('userId') userId: string) {
// ...
}
}
In some cases you might want to access Express's native request object. You can do this using the @Req()
decorator. You should avoid using @Req()
if possible and use the decorators listed above instead.
@Controller('/')
class MyController {
@Get('/')
handler(@Req() req: Request) {
// ...
}
}
You might need to access Express's native response object. You can do this using the @Res()
decorator. In most cases you should use Warp's response classes instead.
@Controller('/')
class MyController {
@Get('/')
handler(@Res() res: Response) {
// ...
}
}
In addition to the built in parameter decorators, Warp also offers the ability to create custom parameters.
import { BaseParam /*...*/ } from '@varld/warp';
let CustomParam = () => {
BaseParam((req, res) => req.ip);
};
@Controller('/')
class MyController {
@Get('/')
handler(@CustomParam() ip: string) {
return `Your IP-Address is: ${ip}`;
}
}
Warp includes native support for cors. You can enable cors when creating a new Warp instance.
let app = warp({
controllers: [
/*...*/
],
cors: true
});
// ...
cors
is optional and can be a boolean
or a cors-options-object. By default the cors
value is false
and hance cors is disabled.
Warp has support for accessing, validating and transforming the request body using the @Body()
decorator.
@Controller('/')
class MyController {
@Post('/')
handler(@Body() body: any) {
// do something with the body
}
}
Request body validation is achieved using class validator. Class validator makes it easy to specify validation rules using decorators.
class MyBody {
@Length(5, 25)
title: string;
@Contains('hello')
text: string;
@IsInt()
@Min(0)
@Max(5)
rating: number;
}
@Controller('/')
class MyController {
@Post('/')
handler(@Body() body: MyBody) {
// The body has already been validated
// and can be used now.
}
}
If the body is not valid, a not acceptable error, including an array of validation-errors will be sent to the client. In this case the handler will not be executed.
Authentication is important for many APIs. Warp includes support for standard token authentication using a header or a query parameter. By default warp is configured to support bearer authentication.
Warp automatically extracts a token from the request object. If a token exists, the token will be handed to an async authenticator function, which you have to implement.
Warp expects you to return a user. This user can be of any type.
If no user is returned, warp will not execute any handlers and send an unauthorized error to the client.
let app = warp({
controllers: [
/*...*/
],
authenticator: async (token: string, req: Request) => {
// fetch user by token from database
let user = await db.getUserByToken(token);
return user;
}
});
Warp supports two types of authentication: global authentication, which protects every route of the warp instance and handler based authentication, using the @Authenticated()
decorator.
Global authentication:
let app = warp({
// ...
authentication: {
global: true
}
});
Handler based authentication:
@Controller('/')
class MyController {
@Get('/')
@Authenticated()
handler() {
// The user is authenticated
}
}
In authenticated routes, the user (from the authenticator function) can be accessed using the @User()
decorator.
@Controller('/')
class MyController {
@Get('/')
@Authenticated()
handler(@User() user: MyUser) {
// Do something with the user
}
}
By default, Warp will check the Authorization
header and the access_token
query parameter. Warp expects tokens in the Authorization
header to be prefixed with the bearer
keyword. This behavior can be altered using authentication
object when creating a warp instance.
let app = warp({
// ...
authentication: {
global: false,
header: 'Authorization',
headerScheme: 'bearer',
query: 'access_token'
}
});
You can override Warp's default error logger by providing a logger
function when creating a Wrap instance. The function receives the error as its first parameter.
let app = warp({
controllers: [...],
logger: (error: Error | HTTPException) => {
// do something with the error
}
});
Warp has support for dependency injection in controllers using typedi. Injectable classes must be marked using the @Service()
decorator.
@Service()
class MyService {
doSomething() {
return 'did something';
}
}
@Controller('/')
class MyController {
constructor(private myService: MyService) {}
@Get('/')
handler() {
return this.myService.doSomething();
}
}
Warp automatically catches errors thrown in middleware, param functions and http handlers. If the exception is unknown, Warp will return an internal server error. However, you can use Warp's builtin HTTP-Exceptions to specify which error should be returned to the client. Warp has an exception for all standard http errors.
import { GoneException } from '@varld/warp';
@Controller('/')
class MyController {
@Get('/')
gone() {
throw new GoneException();
}
}
The response sent to the client looks like this:
{
"status": "410",
"code": "gone"
}
The code can be customized using the exceptions first parameter: throw new GoneException('It is gone!')
.
For some usecases you might need custom HTTP-Exceptions. You can create those by extending the the HttpException
class.
import { HttpException } from '@varld/warp';
export class CustomException extends HttpException {
constructor() {
super(
{
code: 'A custom error',
additionalField: 'something'
},
418
);
}
}
As you can see, the super function expects two parameters, the first one can be a string
or an object which must contain the code
property, but can also contain additional properties. The second parameters is an HTTP-status-code.
Testing a warp app is very simple, since warp apps are basically just Express apps. The simples way to test an api built with warp is using supertest. Supertest has also been used to test the warp library itself, take a look at the tests to learn more.
test('serves basic api', async () => {
@Controller('/')
class IndexController {
@Get('/')
sendHello() {
return 'hello';
}
}
let app = warp({
controllers: [IndexController]
});
let response = await request(app).get('/');
expect(response.status).toEqual(200);
expect(response.text).toEqual('hello');
});
Warp was mostly inspired by Nest.js, Tachijs and Express. All of those libraries are amazing and I greatly appreciate their maintainers! Warp would not exist without those libraries.
Yeah! Nest.js is great, but it includes a lot of bloat that most people don't need or don't even know about, like Interceptors, or built in support for microservice. Warp aims be much simple, while offering the features most APIs need.
If you need a bunch of abstractions and enterprise-level features, then Nest.js is great. If you want to build an API (big or small) but don't need all of those features, then Warp is a great choice.
Sure! Warp is built on Express. Express is battle tested and very unopinionated, so you can build basically anything with Express. Since Express does not do that much by itself you will have to write a lot of boilerplate the get started. Warp comes will all of the basics, like body parsing and cors, already setup and extends Express with controllers, useful decorators and authentication.
Honestly, Warp is a really simple library. All it does is glueing a few other libraries together. All of those libraries are battle tested and used by thousands (sometimes even millions) of developers. In addition to that Warp is very well tested (99% coverage). So it is safe to say that you can use Warp in production. However, if you do encounter any problems or inconveniences feel free to open an issue or poll request on Warp's GitHub.
MIT Β© Tobias Herber