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 build
Now, run your server:
$ npx ts-node src/index.ts
# npx tsc && node dist/index.js is also valid
2
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
GET
toPOST
.We’ll add a
Body
containing aname: string
field (which must have a minimum of 5 characters) and anage: number
field (which must be greater than 18).We’ll also use the
Authorization
header to validate whether the user has permission to create a new user.We’ll also use a
Query
of?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
Forbidden
status if the Authorization is invalid and a400
error 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
get
topost
, the route is automatically changed toPOST
.By adding a
body
parameter 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
PostBody
interface has only 2 fields,name
andage
, so ONLY these fields will be present at runtime, extra fields present in theJSON
will be ignored.By annotating with
@minLength 5
and@minimum 18
on thename
andage
fields, respectively, Kita ensures that these conditions will be respected at runtime.By adding a
fail
parameter 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
authorization
parameter of typeHeader<'authorization'>
, Kita understands that it needs to extract the value from theAuthorization
header and that this value is mandatory.Inside the function, by throwing any errors coming from the
errors
parameter, 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
, andage
fields, 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.ts
2
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