I'm fortunate to work in a company which doesn't just appreciate open source but even contributes to it by releasing some of the tools and libraries we use internally. One of these projects is Hans - a tool for creating fake APIs - which I haven't talked about yet. Well, it's about time!
What is Hans?
Hans is a small Node application for faking APIs - but not just a single REST API using HTTP. It allows for creating multiple APIs using different ports and protocols (like native WebSockets or even GraphQL) while still being easy to use. On top of that it is written entirely in TypeScript which allows for some neat abstractions and sharing models between your real application and your fake API. And, of course, everything is licensed under MIT.
Basic example
APIs are called "Apps" within Hans; apps are just simple TypeScript classes, decorated with the @App
decorator (which tells Hans the name and port of the app). Methods within these classes can be decorated with specific decorators to declare endpoints. This might sound confusing, but let's take a look at the example from the documentation:
// apps/Example.ts
import {Get, App, Response} from '@loremipsum/mocking-hans';
@App({
name: 'example',
port: 4999,
})
export class Example {
/**
* A simple text response
*/
@Get('/')
index() {
return new Response('Hello there!');
}
}
Starting Hans would now start an HTTP server on port 4999
. Opening localhost:4999
would properly simply show "Hello there!".
We could start as many applications this way as we want. Requests for every application are properly logged in the console:
// apps/FirstApp.ts
import {Get, App, Response} from '@loremipsum/mocking-hans';
@App({
name: 'firstApp',
port: 42000,
})
export class FirstApp {
@Get('/')
index() {
return new Response('Hello there!');
}
}
// apps/SecondApp.ts
import {Get, App, Response} from '@loremipsum/mocking-hans';
@App({
name: 'secondApp',
port: 43000,
})
export class SecondApp {
@Get('/')
index() {
return new Response('Hello there!');
}
}
All examples can be found in our GitHub repository!
Adapters
Behind the scenes Hans uses adapters for different protocols. But you don't need to know a lot about them - most things are done behind the scenes.
For example, if you want to create a GraphQL API you simply add the endpoint to your app:
@App({
name: 'firstApp',
port: 42000
})
export class FirstApp {
@Graphql('/graphql', `
type Query {
hello: String
}
`)
graphql() {
return {
hello: () => 'Hello world!'
}
}
}
Hans automatically starts GraphiQL (an in-browser IDE for exploring GraphQL) on localhost:42000/graphql
and provides a proper GraphQL endpoint:
Requests
The HTTP adapter uses Express for serving a HTTP server, hence we've got access to the request and response object within our endpoints:
import {Post, JsonResponse, App} from '@loremipsum/mocking-hans';
import {Request, Response} from 'express';
@App({
name: 'firstApp',
port: 42000
})
export class FirstApp {
@Post('create')
create(req: Request/*, res: Response*/) { // Keep in mind that method names are completely up to you!
return new JsonResponse({ username: req.body.data });
}
}
Sending a POST request to localhost:42000/create
with proper JSON payload now leads to the following result:
Responses
Hans helps in handling responses by providing an object-oriented approach to it. Endpoints which return a proper response will be handled directly by Hans, taking away the necessity of setting headers for certain responses yourself. Available responses are:
FileResponse
: for sending files (e.g.return new FileResponse('path_to_file.jpg');
) - ExampleJsonResponse
: for sending JSON (see above) - ExampleResponse
: for sending simple text responses (see above) - ExampleTemplateResponse
: replaces placeholders with actual values (e.g.return new TemplateResponse('Hello %who%', { who: 'world' });
which would return "Hello World") - ExampleXmlFromJsonResponse
- transforms JSON to XML and returns it - Example
Middleware
Some APIs might require things like authentication which can easily be implemented using Middleware in Hans using the Middleware
decorator. This decorator takes an array of functions and executes all of them before actually executing the endpoint, where all of these functions do have access to the Request and Response object and might even send a response (preventing the actual endpoint from ever being triggered).
In case of authentication we could say we simply check if a proper authorization
header is set:
import {Request, Response, NextFunction} from 'express';
export function IsAuthenticated(req: Request, res: Response, next: NextFunction) {
if (!req.headers.authorization) {
return res.status(403).json({error: 'You are not logged in!'});
}
next();
}
Which then would simply be attached to our endpoint:
@Get('/authenticated')
@Middleware([IsAuthenticated])
authenticated() {
return new JsonResponse({
message: 'Hello there'
});
}
Simply calling /authenticated
now would lead to an error:
$ curl localhost:42000/authenticated
{"error":"You are not logged in!"}
While we pseudo-authenticate - and hence bypassing our middleware - by adding a proper header:
curl -H "authorization: whatever" localhost:4200/authenticated
{"message":"Hello there"}
State
Applications might even share data between them using a global state which is injected in the constructor of every app. Let's assume we've got apps which simply increase a counter every time an endpoint is called:
// apps/FirstApp.ts
import {State, JsonResponse, Get, App} from '@loremipsum/mocking-hans';
@App({
name: 'firstApp',
port: 42000
})
export class FirstApp {
constructor(private state: State) {
}
@Get('/')
index() {
const counter = this.state.get('counter', 0); // Second argument is the fallback value
this.state.set('counter', counter + 1);
return new JsonResponse({
count: this.state.get('counter')
});
}
}
// apps/SecondApp.ts
import {State, JsonResponse, Get, App} from '@loremipsum/mocking-hans';
@App({
name: 'secondApp',
port: 43000
})
export class SecondApp {
constructor(private state: State) {
}
@Get('/')
index() {
const counter = this.state.get('counter', 0); // Second argument is the fallback value
this.state.set('counter', counter + 1);
return new JsonResponse({
count: this.state.get('counter')
});
}
}
Now sending a request to our first app first and then our second app will lead to the following result:
$ curl localhost:42000
{"count":1}
$ curl localhost:43000
{"count":2}
$ curl localhost:42000
{"count":3}
As we can see the counter is increasing which tells us that the store that holds the counter is properly shared.
Why do we need this?
Some time ago we've worked on a product which accumulated the data from multiple APIs and displayed everything in a single UI. Since the folks behind the API were a bit behind their deadlines we had nothing except some API specifications to work with. Waiting a bunch of weeks for them to get their work done was obviously no option - so we went for simply faking their entire API.
The first thing which came into mind was json-server which is another great tool for creating fake APIs. But it lacked two features we really needed:
- since the application which consumed the APIs was written in TypeScript and provided all the models from our API we definitely wanted to have good TypeScript support.
- json-server only supports HTTP, but some of the APIs we worked with used WebSockets.
What started as a necessity soon become something we continously used for our projects and, ultimately, became open source (after we've written some tests and cleaned up some things).
Hans now lives in a lot of our repositories, since it causes very little overhead and allows us to develop on applications without having to rely on real APIs (we even use it for internal UI demos) and it additionally helps a lot when it comes to integration tests.
Why "Mocking Hans"?
Honestly, I don't even know - but there's a line in the source code which describes it pretty accurate I guess:
// Let's be honest here: I've always wanted to name a class "Hans".
Feedback
Since we'd really happy to get some feedback on a tool we like to work with and think as a useful addition to our set of of tools feel free to give Hans a try and let us know what you think about it on GitHub or in the comments.