GithubHelp home page GithubHelp logo

frourio's Introduction


frourio

Frourio is a perfectly type-checkable REST framework for TypeScript.




Why frourio ?

Even if you write both the front and server 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 Docker.

Why frourio ?


Frourio is a framework for developing web apps quickly and safely in "One TypeScript".

Architecture of create-frourio-app


Benchmarks

Machine: Linux fv-az18 5.4.0-1026-azure #26~18.04.1-Ubuntu SMP Thu Sep 10 16:19:25 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux | 2 vCPUs | 7GB.
Method: autocannon -c 100 -d 40 -p 10 localhost:3000 (two rounds; one to warm-up, one to measure).

Framework Version Requests/sec Latency
fastify 3.5.1 38,018 2.54
frourio 0.17.0 36,815 2.62
nest-fastify 7.4.4 31,960 3.04
micro 9.3.4 29,672 3.28
express 4.17.1 8,239 11.98
nest 7.4.4 7,311 13.54
frourio-express 0.17.0 7,235 13.67

Benchmarks taken using https://github.com/frouriojs/benchmarks. This is a synthetic, "hello world" benchmark that aims to evaluate the framework overhead.

Table of Contents

Install

Make sure you have npx installed (npx is shipped by default since npm 5.2.0)

$ npx create-frourio-app <my-project>

Or starting with npm v6.1 you can do:

$ npm init frourio-app <my-project>

Or with yarn:

$ yarn create frourio-app <my-project>

Express.js mode

Frourio uses fastify.js as its HTTP server.
If you choose express.js in create-frourio-app, please refer to the following repositories.
GitHub: frourio-express

Note: frourio is 5x faster than frourio-express

Environment

Frourio requires TypeScript 3.9 or higher.
If the TypeScript version of VSCode is low, an error is displayed during development.

Entrypoint

server/index.ts

import Fastify from 'fastify'
import server from './$server' // '$server.ts' is automatically generated by frourio

const fastify = Fastify()

server(fastify, { basePath: '/api/v1' })
fastify.listen(3000)

Controller

$ npm run dev

Case 1 - Define GET: /tasks?limit={number}

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)
  })
}))

Case 2 - Define POST: /tasks

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 }
  }
}))

Case 3 - Define GET: /tasks/{taskId}

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 }
  }
}))

Hooks

Frourio can use hooks of fastify.js.
There are four types of hooks, onRequest / preParsing / preValidation / preHandler.

Lifecycle

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

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

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 }
  }
}))

Login with fastify-auth

$ cd server
$ npm install fastify-auth
$ cd server
$ yarn add fastify-auth

server/index.ts

import Fastify from 'fastify'
import fastifyAuth from 'fastify-auth'
import server from './$server' // '$server.ts' is automatically generated by frourio

const fastify = Fastify()

fastify.register(fastifyAuth).after(() => {
  server(fastify, { basePath: '/api/v1' })
})
fastify.listen(3000)

server/api/user/hooks.ts

import { defineHooks } from './$relay' // '$relay.ts' is automatically generated by frourio
import { getUserIdByToken } from '$/service/user'

// Export the User in hooks.ts to receive the user in controller.ts
export type User = {
  id: string
}

export default defineHooks((fastify) => ({
  preHandler: fastify.auth([
    (req, _, done) => {
      const user =
        typeof req.headers.token === 'string' &&
        getUserIdByToken(req.headers.token)

      if (user) {
        // eslint-disable-next-line
        // @ts-expect-error
        req.user = user
        done()
      } else {
        done(new Error('Unauthorized'))
      }
    }
  ])
}))

server/api/user/controller.ts

import { defineController } from './$relay'
import { getUserNameById } from '$/service/user'

export default defineController(() => ({
  get: async ({ user }) => ({ status: 200, body: await getUserNameById(user.id) })
}))

Validation

Path parameter

Path parameter can be specified as string or number type after @.
(Default is string | number)

server/api/tasks/_taskId@number/index.ts

import { Task } from '$/types'

export type Methods = {
  get: {
    resBody: Task
  }
}

server/api/tasks/_taskId@number/controller.ts

import { defineController } from './$relay'
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 }
  }
}))
$ curl http://localhost:8080/api/tasks
[{"id":0,"label":"sample task","done":false}]

$ curl http://localhost:8080/api/tasks/0
{"id":0,"label":"sample task","done":false}

$ curl http://localhost:8080/api/tasks/1 -i
HTTP/1.1 404 Not Found

$ curl http://localhost:8080/api/tasks/abc -i
HTTP/1.1 400 Bad Request

URL query

Properties of number or number[] are automatically validated.

server/api/tasks/index.ts

import { Task } from '$/types'

export type Methods = {
  get: {
    query?: {
      limit: number
    }
    resBody: Task[]
  }
}

server/api/tasks/controller.ts

import { defineController } from './$relay'
import { getTasks } from '$/service/tasks'

export default defineController(() => ({
  get: async ({ query }) => ({
    status: 200,
    body: (await getTasks()).slice(0, query?.limit)
  })
}))
$ curl http://localhost:8080/api/tasks
[{"id":0,"label":"sample task 0","done":false},{"id":1,"label":"sample task 1","done":false},{"id":1,"label":"sample task 2","done":false}]

$ curl http://localhost:8080/api/tasks?limit=1
[{"id":0,"label":"sample task 0","done":false}]

$ curl http://localhost:8080/api/tasks?limit=abc -i
HTTP/1.1 400 Bad Request

JSON body

If no reqFormat is specified, reqBody is parsed as application/json.

server/api/tasks/index.ts

import { Task } from '$/types'

export type Methods = {
  post: {
    reqBody: Pick<Task, 'label'>
    resBody: Task
  }
}

server/api/tasks/controller.ts

import { defineController } from './$relay'
import { createTask } from '$/service/tasks'

export default defineController(() => ({
  post: async ({ body }) => {
    const task = await createTask(body.label)

    return { status: 201, body: task }
  }
}))
$ curl -X POST -H "Content-Type: application/json" -d '{"label":"sample task3"}' http://localhost:8080/api/tasks
{"id":3,"label":"sample task 3","done":false}

$ curl -X POST -H "Content-Type: application/json" -d '{Invalid JSON}' http://localhost:8080/api/tasks -i
HTTP/1.1 400 Bad Request

Custom validation

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

Error handling

Controller error handler

server/api/tasks/controller.ts

import { defineController } from './$relay'
import { createTask } from '$/service/tasks'

export default defineController(() => ({
  post: async ({ body }) => {
    try {
      const task = await createTask(body.label)

      return { status: 201, body: task }
    } catch (e) {
      return { status: 500, body: 'Something broke!' }
    }
  }
}))

The default error handler

https://github.com/fastify/fastify/blob/master/docs/Hooks.md#onerror

server/index.ts

import Fastify from 'fastify'
import server from './$server'

const fastify = Fastify()

server(fastify, { basePath: '/api/v1' })

fastify.addHook('onError', (req, reply, err) => {
  console.error(err.stack)
})
fastify.listen(3000)

FormData

Frourio parses FormData automatically in fastify-multipart.

server/api/user/index.ts

export type Methods = {
  post: {
    reqFormat: FormData
    reqBody: { icon: Blob }
    status: 204
  }
}

Properties of Blob or Blob[] type are converted to Multipart object.

server/api/user/controller.ts

import { defineController } from './$relay'
import { changeIcon } from '$/service/user'

export default defineController(() => ({
  post: async ({ params, body }) => {
    // body.icon is multer object
    await changeIcon(params.userId, body.icon)

    return { status: 204 }
  }
}))

Options

https://github.com/mscdex/busboy#busboy-methods

server/index.ts

import Fastify from 'fastify'
import server from './$server' // '$server.ts' is automatically generated by frourio

const fastify = Fastify()

server(fastify, { basePath: '/api/v1', multipart: { /* limit, ... */} })
fastify.listen(3000)

O/R mapping tool

Prisma

  1. Selecting the DB when installing create-frourio-app

  2. Start the DB

  3. Call the development command

    $ npm run dev
  4. Create schema file server/prisma/schema.prisma

    datasource db {
      provider = "mysql"
      url      = env("DATABASE_URL")
    }
    
    generator client {
      provider = "prisma-client-js"
    }
    
    model Task {
      id    Int     @id @default(autoincrement())
      label String
      done  Boolean @default(false)
    }
  5. Call the migration command

    $ npm run migrate
  6. Migration is done to the DB

TypeORM

  1. Selecting the DB when installing create-frourio-app

  2. Start the DB

  3. Call the development command

    $ npm run dev
  4. Create an Entity file server/entity/Task.ts

    import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
    
    @Entity()
    export class Task {
      @PrimaryGeneratedColumn()
      id: number
    
      @Column({ length: 100 })
      label: string
    
      @Column({ default: false })
      done: boolean
    }
  5. Call the migration command

    $ npm run migration:generate
  6. Migration is done to the DB

CORS / Helmet

$ cd server
$ npm install fastify-cors fastify-helmet

server/index.ts

import Fastify from 'fastify'
import helmet from 'helmet'
import cors from 'fastify-cors'
import server from './$server'

const fastify = Fastify()
fastify.register(helmet)
fastify.register(cors)

server(fastify, { basePath: '/api/v1' })
fastify.listen(3000)

Dependency Injection

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 controller from '$/api/tasks/controller'
import { getTasks } from '$/service/tasks'

test('dependency injection into controller', async () => {
  let printedMessage = ''

  const injectedController = controller.inject({
    getTasks: 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
    }
  })()

  const limit = 3
  const message = 'test message'
  const res = await injectedController.get({
    query: { limit, message },
    body: undefined,
    headers: undefined
  })

  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.

Support

Twitter

License

Frourio is licensed under a MIT License.

frourio's People

Contributors

dependabot[bot] avatar solufa avatar yokinist avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.