Fast and type-safe full stack framework, for TypeScript
Even if you write both the frontend and backend in TypeScript, you can't statically type-check the API's sparsity.
We are always forced to write "Two TypeScript".
We waste a lot of time on dynamic testing using the browser and server.
Frourio is a framework for developing web apps quickly and safely in "One TypeScript".
Make sure you have npx installed (npx
is shipped by default since npm 5.2.0
)
$ npx create-frourio-app
Or starting with npm v6.1 you can do:
$ npm init frourio-app
Or with yarn:
$ yarn create frourio-app
server/types/index.ts
export type Task = {
id: number
label: string
done: boolean
}
server/api/tasks/index.ts
import { Task } from '$/types' // path alias $ -> server
export type Methods = {
get: {
query: {
limit: number
}
resBody: Task[]
}
}
server/api/tasks/controller.ts
import { defineController } from './$relay' // '$relay.ts' is automatically generated by frourio
import { getTasks } from '$/service/tasks'
export default defineController(() => ({
get: async ({ query }) => ({
status: 200,
body: (await getTasks()).slice(0, query.limit)
})
}))
server/api/tasks/index.ts
import { Task } from '$/types' // path alias $ -> server
export type Methods = {
post: {
reqBody: Pick<Task, 'label'>
status: 201
resBody: Task
}
}
server/api/tasks/controller.ts
import { defineController } from './$relay' // '$relay.ts' is automatically generated by frourio
import { createTask } from '$/service/tasks'
export default defineController(() => ({
post: async ({ body }) => {
const task = await createTask(body.label)
return { status: 201, body: task }
}
}))
server/api/tasks/_taskId@number/index.ts
import { Task } from '$/types' // path alias $ -> server
export type Methods = {
get: {
resBody: Task
}
}
server/api/tasks/_taskId@number/controller.ts
import { defineController } from './$relay' // '$relay.ts' is automatically generated by frourio
import { findTask } from '$/service/tasks'
export default defineController(() => ({
get: async ({ params }) => {
const task = await findTask(params.taskId)
return task ? { status: 200, body: task } : { status: 404 }
}
}))
Frourio can use hooks of Fastify.
There are four types of hooks, onRequest / preParsing / preValidation / preHandler.
Incoming Request
│
└─▶ Routing
│
404 ◀─┴─▶ onRequest Hook
│
4**/5** ◀─┴─▶ preParsing Hook
│
4**/5** ◀─┴─▶ Parsing
│
4**/5** ◀─┴─▶ preValidation Hook
│
4**/5** ◀─┴─▶ Validation
│
400 ◀─┴─▶ preHandler Hook
│
4**/5** ◀─┴─▶ User Handler
│
4**/5** ◀─┴─▶ Outgoing Response
Directory level hooks are called at the current and subordinate endpoints.
server/api/tasks/hooks.ts
import { defineHooks } from './$relay' // '$relay.ts' is automatically generated by frourio
export default defineHooks(() => ({
onRequest: [
(req, reply, done) => {
console.log('Directory level onRequest first hook:', req.url)
done()
},
(req, reply, done) => {
console.log('Directory level onRequest second hook:', req.url)
done()
}
],
preParsing: (req, reply, payload, done) => {
console.log('Directory level preParsing single hook:', req.url)
done()
}
}))
Controller level hooks are called at the current endpoint after directory level hooks.
server/api/tasks/controller.ts
import { defineHooks, defineController } from './$relay' // '$relay.ts' is automatically generated by frourio
import { getTasks, createTask } from '$/service/tasks'
export const hooks = defineHooks(() => ({
onRequest: (req, reply, done) => {
console.log('Controller level onRequest single hook:', req.url)
done()
},
preParsing: [
(req, reply, payload, done) => {
console.log('Controller level preParsing first hook:', req.url)
done()
},
(req, reply, payload, done) => {
console.log('Controller level preParsing second hook:', req.url)
done()
}
]
}))
export default defineController(() => ({
get: async ({ query }) => ({
status: 200,
body: (await getTasks()).slice(0, query.limit)
}),
post: async ({ body }) => {
const task = await createTask(body.label)
return { status: 201, body: task }
}
}))
Query, reqHeaders and reqBody are validated by specifying Class with class-validator.
The class needs to be exported from server/validators/index.ts
.
server/validators/index.ts
import { MinLength, IsString } from 'class-validator'
export class LoginBody {
@MinLength(5)
id: string
@MinLength(8)
pass: string
}
export class TokenHeader {
@IsString()
@MinLength(10)
token: string
}
server/api/token/index.ts
import { LoginBody, TokenHeader } from '$/validators'
export type Methods = {
post: {
reqBody: LoginBody
resBody: {
token: string
}
}
delete: {
reqHeaders: TokenHeader
}
}
$ curl -X POST -H "Content-Type: application/json" -d '{"id":"correctId","pass":"correctPass"}' http://localhost:8080/api/token
{"token":"XXXXXXXXXX"}
$ curl -X POST -H "Content-Type: application/json" -d '{"id":"abc","pass":"12345"}' http://localhost:8080/api/token -i
HTTP/1.1 400 Bad Request
$ curl -X POST -H "Content-Type: application/json" -d '{"id":"incorrectId","pass":"incorrectPass"}' http://localhost:8080/api/token -i
HTTP/1.1 401 Unauthorized
Frourio is complete in one directory, but not monolithic.
Client and server are just statically connected by a type and are separate projects.
So they can be deployed in different environments.
$ npm run build:client
$ npm run start:client
$ npm run build:server
$ npm run start:server
or
$ cd server
$ npm run build
$ npm run start
Frourio use frouriojs/velona for dependency injection.
server/api/tasks/index.ts
import { Task } from '$/types'
export type Methods = {
get: {
query?: {
limit?: number
message?: string
}
resBody: Task[]
}
}
server/service/tasks.ts
import { PrismaClient } from '@prisma/client'
import { depend } from 'velona' // dependency of frourio
import { Task } from '$/types'
const prisma = new PrismaClient()
export const getTasks = depend(
{ prisma: prisma as { task: { findMany(): Promise<Task[]> } } }, // inject prisma
async ({ prisma }, limit?: number) => // prisma is injected object
(await prisma.task.findMany()).slice(0, limit)
)
server/api/tasks/controller.ts
import { defineController } from './$relay'
import { getTasks } from '$/service/tasks'
const print = (text: string) => console.log(text)
export default defineController(
{ getTasks, print }, // inject functions
({ getTasks, print }) => ({ // getTasks and print are injected function
get: async ({ query }) => {
if (query?.message) print(query.message)
return { status: 200, body: await getTasks(query?.limit) }
}
})
)
server/test/server.test.ts
import fastify from 'fastify'
import controller from '$/api/tasks/controller'
test('dependency injection into controller', async () => {
let printedMessage = ''
const injectedController = controller.inject((deps) => ({
getTasks: deps.getTasks.inject({
prisma: {
task: {
findMany: () =>
Promise.resolve([
{ id: 0, label: 'task1', done: false },
{ id: 1, label: 'task2', done: false },
{ id: 2, label: 'task3', done: true },
{ id: 3, label: 'task4', done: true },
{ id: 4, label: 'task5', done: false }
])
}
}
}),
print: (text: string) => {
printedMessage = text
}
}))(fastify())
const limit = 3
const message = 'test message'
const res = await injectedController.get({
query: { limit, message }
})
expect(res.body).toHaveLength(limit)
expect(printedMessage).toBe(message)
})
$ npm test
PASS server/test/server.test.ts
✓ dependency injection into controller (4 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.67 s, estimated 8 s
Ran all test suites.
Frourio is licensed under a MIT License.