curie-server

A modular Node.js http/1.0 framework

Usage no npm install needed!

<script type="module">
  import curieServer from 'https://cdn.skypack.dev/curie-server';
</script>

README

Curie

A modular Node.js http/1.0 framework

Table of Content

  1. Config
  2. File Structure
    1. Main File
    2. Database File
    3. Listeners
    4. Middleware
    5. Route Parser
    6. Template Files
    7. Single File Alternative
  3. Classes
    1. DBridge
    2. Listener
    3. Middleware
  4. Types
    1. Request
    2. Response
    3. Server Parameters
    4. CallbackReturnType
  5. Helpers
    1. Logging
    2. PreRun commands

Config

Curie-server accepts a number of configuration options of the type:

interface ServerParams {
  routes: string
  routeParser: ClassConstructor<RouteParser>
  public: string
  port: number
  listeners: [string, string | RegExp]
  middleware: [string, string | RegExp]
  database: string
  preRun: string[]
  root: string
}

Keep in mind that all the paths are relative to the main file

File structure

curie-server supports a multi file structure. An example file structure:

|public/
|--css/
|----main.css
|--js/
|----main.js
|routes/
|--index.pug
|listeners/
|--index.list.ts
|middleware/
|--logger.mdw.ts
|index.ts
|database.ts

Main file

The main file is the file, which is responsible for starting your application.

import { initApp } from "curie-server"
initApp({
  port: 8000,
  public: "../public",
  routes: "../routes",
  listeners: ["./listeners", "list.[jt]s"],
  middleware: ["./middleware", "mdw.[jt]s"],
  database: ""
})

Database file

The database file is responcible for connecting with your database. It should export a class which extends the DBridge class. You may create your own class or use the so-called out of the box PotgreSQL DBridge. It should look something like it:

import { PostDBridge, database } from "curie-server"

@database("postgres://postgres:postgres@127.0.0.1:5432/postgres")
export default class extends PostDBridge {}

Listeners

Listeners are responsible for responding to incoming http requests. Both their location and extension are specified in the Server constructor parameters in the main file. A listener should extend the Listener class, implement onGET and/or onPOST method(s), such that they return CallbackReturnType. Example:

import c, { Listener, hookup } from "curie-server";

@hookup("/")
export default class extends Listener {
  async onGET(req: c.Request, res: c.Response): c.CallbackReturnType {
    this.server.routeParser.render(res, "index")
    return [null, false]
  }
}

Middleware

Middleware is responcible for interrupting incoming requests and can even reject them. Middleware should extends the Middleware class and return the CallbackReturnType. It should look something like this:

import { CallbackReturnType, Middleware, withTime, Request, Response, c_log, use } from "curie-server";

@use()
export default class extends Middleware {
  async intercept(req: Request, res: Response) {
    c_log(withTime(`[LOGGER]> ${req.method}: ${req.url || ""}`))
    return [null, true] as CallbackReturnType
  }
}

RouteParser

The RouteParser is responsible for parsing and rendering template files. If you want to use the lamplating language of your choice, you should provide a class that extends the RouteParser.

abstract class RouteParser<RouteType = any> {
  path: string
  routes: LooseObject<RouteType>
  server: Server
  public constructor(path: string, server: Server) {/*...*/}
  abstract compile(route: string): CallbackReturnType
  abstract async compileAll(): Promise<CallbackReturnType>
  abstract async render(res: Response, route: string, locals?: LooseObject): Promise<CallbackReturnType>
}

Out of the box curie-server is providing support for the pug templating lang.

Routes

Routes are themplates rendered by the RouteParser. Out of the box you get the PugParser, which compiles .pug files and allows you to query items from the database (template: //# lt;variable_name>: <query>).

//# $posts: SELECT * FROM posts

<!DOCTYPE html>
html(lang="en")
  head 
    // ...
  body
    ul.posts
      for post in posts
        li.posts__post
          h2.posts__post__title= post.title
          p.posts__post__body= post.body

Single file approach

While I highly advise you to take the advatnage of the multi file structure, around which the curie-server was built, you can fit everything into a single file.

import c, { Server, PostDBridge, Listener, Middleware, c_log, withTime, initApp, database, hookup, use } from "curie-server";

(async () => {
  await initApp({
   port: 8000,
   public: "../public",
   routes: "../routes",
   listeners: ["./listeners", "list.[jt]s"],
   middleware: ["./middleware", "mdw.[jt]s"],
   database: ""
  })

  @database("postgres://postgres:postgres@127.0.0.1:5432/postgres")
  class Db extends PostDBridge {}

  @hookup("/")
  class IndexListener extends Listener {
    async onGET(req: c.Request, res: c.Response) {
      await this.render(res, "index")
      return [null, false] as c.CallbackReturnType
    }
  }
  @use()
  class Logger extends Middleware {
    async intercept(req: Request, res: Response) {
      c_log(withTime(`[LOGGER]> ${req.method}: ${req.url || ""}`))
      return [null, true] as c.CallbackReturnType
    }
  }
})()

Classes

DBridge

@database("<connection_uri>")
class MyDBridge extends DBridge<DBInterface, QueryType> {
  async get(query: QueryType): Promise<LooseObject[] | any> {
    // Fetch the query! Good boy!
    return getResponse
  }
  async update(query: QueryType): Promise<UpdateResponse | any> {
    // Update something...
    return updateResponse
  }
  async delete(query: QueryType): Promise<DeleteResponse | any> {
    // Delete something...
    return deletionResponse  
  }
  async create<T extends ClassConstructor>(model: T, data: ConstructorParameters<T>): Promise<CreateResponse | any> {
    // Create something...
    return creationResponse
  }
}

Listener

@hookup("/")
class IndexListener extends Listener {  
  async onGET(
    req?: Request, 
    res?: Response
    ): Promise<CallbackReturnType | undefined> {
      return [null, true]
    }
  async onPOST(
    req?: Request, 
    res?: Response
    ): Promise<CallbackReturnType | undefined> {
      return [null, true]
    }
}

Middleware

@use()
export default class Intercepter extends Middleware {
  async intercept(req?: Request, res?: Response) {
    // Do something
    return [null, true] as CallbackReturnType
  }
}

Interfaces and types

LooseObject

An implementation of a key-value map.

interface LooseObject<T = any> {
  [key: string]: T
}

Request

interface Request extends http.IncomingMessage {
  query: LooseObject<string>
  cookies: cookies
  body: LooseObject
}

Response

interface Response extends http.ServerResponse {
  cookies: cookies
}

ServerParams

It is a configuration object passed to the Server constructor.

interface ServerParams {
  routes: string,
  routeParser: ClassConstructor<RouteParser>
  public: string
  port: number
  listeners: [string, string | RegExp] // [path_to_dir, extension]
  middleware: [string, string | RegExp] // [path_to_dir, extension]
  database: string
  root: string
  preRun: string[]
  [key: string]: any | any[]
}

Server.DEFAULT_SERVER_OPTIONS: ServerParams = {
    routes: "./routes",
    routeParser: PugParser,
    public: "./public",
    port: 8000,
    listeners: ["./", "list.[jt]s"],
    middleware: ["./", "mdw.[jt]s"],
    database: '',
    preRun: [],
    root: path.dirname(require.main.filename)
  }

CallbackReturnType

CallbackReturnType is a value returned by many curie-server functions, but is used especially in the Listener and the Middleware classes. The first part of the tuple is the Error, and the 2nd one is the ShouldContinue boolean, which tells the inner loop whether is should send the Response to the client or continue.

type CallbackReturnType = [Error | null, boolean]

Helpers

Logging

curie-server comes with a plethora of tools to help you log info.

const log = (text: any, color: keyof ChalkColors) =>
  console.log(
    ((chalk[color] as any) as Executable<string>)(
      typeof text === "object" ? JSON.stringify(text) : text
    )
  )
const c_log = (text: string) => log(text, "yellowBright")

const initLogger = 
  (name: string, color: keyof ChalkColors): LoggerFunction 
  => (text: string) 
  => log(withTime(`[${name}]> ${text}`), color)

PreRun commands

initApp accepts a parameter called preRun. It's simply an array of commands to run before the rest of initialization (loading files, setting up events etc.). Errors won't terminate the main process, but only propagate the error message to the console (same with stdout).

initApp({
  // ...
  preRun: [
    "tsc"
  ]
})

In the example, the tsc command will run before loading files to a memory, thus allowing you to compile i.e. typescript files.

CLI

curie-server listens for a console input and evaluates it upon pressing Enter. Errors won't terminate the main process. this refers to the Server instance.