Practical Example
Just want to try it by yourself?
Skip to the Quickstart or head over to our Migration Guide to migrate your existing project to Kita.
No matter how good we are at explaining, nothing is better than an example to prove to you how powerful Kita is in just a few lines of code.
Below is the simplest Fastify server you’ve ever seen:
import { Kita } from '@kitajs/runtime';
import fastify from 'fastify';
fastify().register(Kita).listen({ port: 1227 });2
3
4
Just by creating a file at src/routes/index.ts and exporting a get function, you already have a GET route registered in your Fastify server.
export function get() {
return { hello: 'world' };
}2
3
Running the Server
As Kita runs at compile time, you need to run the kita build command before running or compiling your code:
npx kita buildNow, run your server:
$ npx ts-node src/index.ts
# npx tsc && node dist/index.js is also valid2
Testing the Route
Done! Your server is already running and you can access the GET route at http://localhost:1227.
$ curl http://localhost:1227
{ "hello": "world" }2
But wait, there’s more! Kita, along with Scalar, Kita generates a graphical interface for you to visualize, edit, and test your API:
Click here to launch a STATIC DEMO of the Scalar EditorOr view the generated OpenAPI json
{
"openapi": "3.1.0",
"info": {
"title": "API Reference",
"description": "Powered by [Scalar](https://scalar.com/) & Generated by [KitaJS](https://kitajs.org/)",
"version": "x.x.x"
},
"components": {
"schemas": {
"GetIndexResponse": {
"additionalProperties": false,
"properties": { "hello": { "type": "string" } },
"required": ["hello"],
"type": "object"
}
}
},
"paths": {
"/": {
"get": {
"operationId": "getIndex",
"responses": {
"2XX": {
"description": "Default Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/GetIndexResponse" }
}
}
}
}
}
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Let’s change something!
Life isn’t perfect, and eventually, you’ll receive new specifications to implement… Let’s do this? Notice how simple it is to transform your routes into something more complex.
We’ll change the method from
GETtoPOST.We’ll add a
Bodycontaining aname: stringfield (which must have a minimum of 5 characters) and anage: numberfield (which must be greater than 18).We’ll also use the
Authorizationheader to validate whether the user has permission to create a new user.We’ll also use a
Queryof?fail=<boolean>which has a default value offalse.Lets also return the newly created user with an automatically generated
id.We can also return a
Forbiddenstatus if the Authorization is invalid and a400error if the request comes with?fail=true.It’s always recommended to add descriptions for the fields, so we’ll do that too.
The final result should look something like this:
import type { HttpErrors } from '@fastify/sensible';
import type { Body, Header, Query } from '@kitajs/runtime';
interface PostBody {
/** @minLength 5 */
name: string;
/** @minimum 18 */
age: number;
}
/**
* Creates a new user
*
* @operationId createUser
*/
export function post(
body: Body<PostBody>,
fail: Query<boolean> = false,
authorization: Header<'authorization'>,
errors: HttpErrors
) {
if (authorization !== 'please let me in') {
throw errors.forbidden('Invalid Authorization');
}
if (fail) {
throw errors.badRequest('You asked for it');
}
return {
id: Math.floor(Math.random() * 1000),
name: body.name,
age: body.age
};
}2
3
4
5
Yes, you read it right! Just by using typing and annotations, your route is already documented, being validated at runtime, and secure against attacks like Prototype Pollution.
What happened here?
I know you might not fully understand all the power that is now in your hands. Let me break down everything you just did:
By changing the function name from
gettopost, the route is automatically changed toPOST.By adding a
bodyparameter of typeBody<PostBody>, Kita understands that the request body needs to be passed there, as well as Kita reads the typing of thePostBody, validates the body at runtime to ensure it’s in the format ofPostBody, and generates the necessary documentation.The
PostBodyinterface has only 2 fields,nameandage, so ONLY these fields will be present at runtime, extra fields present in theJSONwill be ignored.By annotating with
@minLength 5and@minimum 18on thenameandagefields, respectively, Kita ensures that these conditions will be respected at runtime.By adding a
failparameter of typeQuery<boolean>and the default value offalse, Kita understands that it needs to extract the value from the query string (?fail=true) and that if not specified, it should use the valuefalse.By adding an
authorizationparameter of typeHeader<'authorization'>, Kita understands that it needs to extract the value from theAuthorizationheader and that this value is mandatory.Inside the function, by throwing any errors coming from the
errorsparameter, Kita understands that it’s an HTTP error and will return the correct status as well as document it in OpenAPI.By returning an object with the
id,name, andagefields, Kita understands that this is the return of the function and generates the necessary documentation and validation.Yes, if you return an object different from the specified typing, Kita will throw an error at runtime because your clients expect a different signature than what is being returned.
Result
First, you need to run the kita build again to re-perform the static analysis of your code and regenerate the necessary files:
$ npx kita build
$ npx ts-node src/index.ts2
After running the server, you can already access the POST route.
Since this is just an practical (non hosted) example, see the interface generated by Scalar:
Click here to launch a STATIC DEMO of the Scalar EditorOr view the generated OpenAPI json
{
"openapi": "3.1.0",
"info": {
"title": "API Reference",
"description": "Powered by [Scalar](https://scalar.com/) & Generated by [KitaJS](https://kitajs.org/)",
"version": "x.x.x"
},
"components": {
"schemas": {
"HttpError": {
"type": "object",
"properties": {
"statusCode": { "type": "number" },
"code": { "type": "string" },
"error": { "type": "string" },
"message": { "type": "string" }
}
},
"CreateUserResponse": {
"additionalProperties": false,
"properties": {
"age": { "type": "number" },
"id": { "type": "number" },
"name": { "type": "string" }
},
"required": ["id", "name", "age"],
"type": "object"
}
}
},
"paths": {
"/": {
"post": {
"operationId": "createUser",
"description": "Creates a new user",
"requestBody": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"age": { "minimum": 18, "type": "number" },
"name": { "minLength": 5, "type": "string" }
},
"required": ["name", "age"],
"type": "object"
}
}
},
"required": true
},
"parameters": [
{
"schema": { "type": "boolean" },
"in": "query",
"name": "fail",
"required": false
},
{
"schema": { "type": "string" },
"in": "header",
"name": "authorization",
"required": true
}
],
"responses": {
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HttpError" }
}
}
},
"403": {
"description": "Default Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HttpError" }
}
}
},
"2XX": {
"description": "Default Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CreateUserResponse" }
}
}
}
}
}
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95