Crea tu propio Express desde cero

Gustavo Morales

@gmoralesc

Gustavo Morales

Software Engineer @  Treasure Data

@gmoralesc

¿Que vamos a hacer?

Emular la funciones básicas de Express JS en Node.JS

y ver qué podemos aprender de ello

// index.js

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello, World!\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

https://nodejs.org/en/about/

// app.js

const express = require('express')

const app = express()

const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

https://expressjs.com/en/starter/hello-world.html

// app.js

const express = require('express');

const app = express();

app.get('/', (req, res) => res.end('Welcome API'));
app.get('/users', (req, res) => res.end('Users list'));

module.exports = app;
no utilizamos el método listen de express 
exportamos la instancia de la aplicación
// index.js

const http = require('http');

const app = require('./app');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer(app);

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
La instancia de express concuerda con el argumento de createServer
utilizamos el servidor http de Node.js

1

Cumplir con los estándares establecidos

// index.js

// const express = require('express');
const express = require('./expresso');

const app = express();

app.get('/', (req, res) => res.end('Welcome API'));
app.get('/users', (req, res) => res.end('Users list'));

module.exports = app;
La funcion de express es un constructor
Los metodos HTTP son funciones de la instancia

2

Ofrecer una API cómoda para el usuario

// expresso.js

module.exports = function expresso() {

  const app = function app(req, res) {

  };

  app.get = (route, callback) => {
  
  };

  return app;
};
Exportamos el constructor
Agregamos el método GET con un monkey patch a la función principal
Creamos la función principal

Incoming request

middleware

middleware

middleware

req, res

res.send()

next()

res.send()

404

GET /users

POST /users

// expresso.js

module.exports = function expresso() {
  const middlewares = [];
  
  const app = function app(req, res) {  

  };

  app.get = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'GET',
    });
  };

  return app;
};
Creamos el Array de Middlewares
Registramos los callbacks al Array de middlewares
// expresso.js
const { parse } = require('url');

module.exports = function expresso() {
  const middlewares = [];
  
  const app = function app(req, res) {
    // Extract URL
    const { method, url } = req;
    const { pathname } = parse(url);
    
    // Match Route
    let index = -1;
    for (let i = 0; i < middlewares.length; i += 1) {
      if (middlewares[i].route === pathname && middlewares[i].method === method) {
        index = i;
        break;
      }
    }

    // Execute Callback
    if (index === -1) {
      res.statusCode = 404;
      res.end('Not middleware Found');
    } else {
      middlewares[index].callback(req, res);
    }
  };

  app.get = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'GET',
    });
  };

  return app;
};
Extraemos el pathname que la petición desea acceder
Buscamos si existe una función middleware que coincida
Devolvemos la respuesta adecuada

3

Una vez entiendes el concepto se hace más sencillo

// index.js

// const express = require('express');
const express = require('./expresso');

const app = express();

app.get('/', (req, res) => res.send('Welcome API'));
app.get('/users', (req, res) => res.send('Users list'));

module.exports = app;
Agregramos el método send
// expresso.js
const { parse } = require('url');

module.exports = function expresso() {
  const middlewares = [];

  function send(content) {
    this.end(content);
  }
  
  const app = function app(req, res) {
    res.send = send;

    // Extract URL
    const { method, url } = req;
    const { pathname } = parse(url);
    
    // Match Route
    let index = -1;
    for (let i = 0; i < middlewares.length; i += 1) {
      if (middlewares[i].route === pathname && middlewares[i].method === method) {
        index = i;
        break;
      }
    }

    // Execute Callback
    if (index === -1) {
      res.statusCode = 404;
      res.end('Not middleware Found');
    } else {
      middlewares[index].callback(req, res);
    }
  };

  app.get = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'GET',
    });
  };

  return app;
};
Agregamos la función send
Hacemos monkey patch, esta vez en el objeto de la respuesta

4

Keep It Simple

// index.js

// const express = require('express');
const express = require('./expresso');

const app = express();

app.get('/', (req, res) => res.send('Welcome API'));
app.get('/users', (req, res) => res.send('Users list'));

app.use((req, res) => {
  res.statusCode = 404;
  res.send('Route Not Found');
});

module.exports = app;
Agregamos la opción para agregar middleware sin rutas
// expresso.js
const { parse } = require('url');

module.exports = function expresso() {
  const middlewares = [];

  function send(content) {
    this.end(content);
  }
  
  const app = function app(req, res) {
    res.send = send;

    // Extract URL
    const { method, url } = req;
    const { pathname } = parse(url);
    
    // Match Route
    let index = -1;
    for (let i = 0; i < middlewares.length; i += 1) {
      if (middlewares[i].route) {
        if (middlewares[i].route === pathname && middlewares[i].method === method) {
          index = i;
          break;
        }
      } else {
        index = i;
        break;
      }
    }

    // Execute Callback
    if (index === -1) {
      res.statusCode = 404;
      res.end('Not middleware Found');
    } else {
      middlewares[index].callback(req, res);
    }
  };

  app.get = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'GET',
    });
  };
  
  app.use = (callback) => {
    middlewares.push({
      callback,
    });
  };

  return app;
};
Agregamos el callback al Array de middlewares
// expresso.js
const { parse } = require('url');

module.exports = function expresso() {
  const middlewares = [];

  function send(content) {
    this.end(content);
  }
  
  const app = function app(req, res) {
    res.send = send;

    // Extract URL
    const { method, url } = req;
    const { pathname } = parse(url);
    
    // Match Route
    let index = -1;
    for (let i = 0; i < middlewares.length; i += 1) {
      if (middlewares[i].route) {
        if (middlewares[i].route === pathname && middlewares[i].method === method) {
          index = i;
          break;
        }
      } else {
        index = i;
        break;
      }
    }

    // Execute Callback
    if (index === -1) {
      res.statusCode = 404;
      res.end('Not middleware Found');
    } else {
      middlewares[index].callback(req, res);
    }
  };

  app.get = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'GET',
    });
  };
  
  app.use = (callback) => {
    middlewares.push({
      callback,
    });
  };

  return app;
};
Agregamos el nuevo caso cuando buscamos el middleware

5

Construir sobre lo ya establecido

// index.js

// const express = require('express');
const express = require('./expresso');

const app = express();

app.use((req, res, next) => {
  const id = 123;
  req.id = id;
  next();
});

app.get('/', (req, res) => res.send('Welcome API'));

app.get('/users', (req, res) => {
  const { id } = req;
  res.send(`Userid: ${id}`));
}
        
app.use((req, res) => {
  res.statusCode = 404;
  res.send('Route Not Found');
});

module.exports = app;
Agregamos el soporte para la función next
// expresso.js
const { parse } = require('url');

module.exports = function expresso() {
  const middlewares = [];

  function send(content) {
    this.end(content);
  }
  
  const app = function next(req, res, start = 0) {
    res.send = send;

    // Extract URL
    const { method, url } = req;
    const { pathname } = parse(url);
    
    // Match Route
    let index = -1;
    for (let i = start; i < middlewares.length; i += 1) {
      if (middlewares[i].route) {
        if (middlewares[i].route === pathname && middlewares[i].method === method) {
          index = i;
          break;
        }
      } else {
        index = i;
        break;
      }
    }

    // Execute Callback
    if (index === -1) {
      res.statusCode = 404;
      res.end('Not middleware Found');
    } else {
      middlewares[index].callback(req, res, () => {
        next(req, res, index + 1);
      });
    }
  };

  app.get = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'GET',
    });
  };
  
  app.use = (callback) => {
    middlewares.push({
      callback,
    });
  };

  return app;
};
Renombramos nuestra función principal
Agregamos el callback para continue con la busqueda si el usuario la invoca

6

Usar lo existente

// index.js

// const express = require('express');
const express = require('./expresso');

const app = express();

app.use((req, res, next) => {
  const id = 123;
  req.id = id;
  next();
});

app.get('/', (req, res) => res.send('Welcome API'));

app.get('/users', (req, res) => {
  const { id } = req;
  res.json({
    message: 'User',
    id: req.id,
  });
}
        
app.use((req, res) => {
  res.status(404).json({
    message: 'Route Not Found',
  });
});

module.exports = app;
Agregramos la función json
Agregamos la función status
// expresso.js
const { parse } = require('url');

module.exports = function expresso() {
  const middlewares = [];

  function send(content) {
    this.end(content);
  }
  
  function json(content) {
    this.setHeader('Content-Type', 'application/json');
    this.end(JSON.stringify(content));
  }

  function status(code) {
    this.statusCode = code;
    return this;
  }
  
  const app = function next(req, res, start = 0) {
    res.send = send;
    res.json = json;
    res.status = status;
    
    // Extract URL
    const { method, url } = req;
    const { pathname } = parse(url);
    
    // Match Route
    let index = -1;
    for (let i = start; i < middlewares.length; i += 1) {
      if (middlewares[i].route) {
        if (middlewares[i].route === pathname && middlewares[i].method === method) {
          index = i;
          break;
        }
      } else {
        index = i;
        break;
      }
    }

    // Execute Callback
    if (index === -1) {
      res.statusCode = 404;
      res.end('Not middleware Found');
    } else {
      middlewares[index].callback(req, res, () => {
        next(req, res, index + 1);
      });
    }
  };

  app.get = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'GET',
    });
  };
  
  app.use = (callback) => {
    middlewares.push({
      callback,
    });
  };

  return app;
};
Agregamos la función json
Agregamos la función status, retornando la misma instancia
Aplicamos el monkey patch al objeto de la respuesta

7

Ofrecer versatilidad

// index.js

// const express = require('express');
const express = require('./expresso');

const app = express();

app.use((req, res, next) => {
  const id = 123;
  req.id = id;
  next();
});

app.get('/', (req, res) => res.send('Welcome API'));

app.get('/users', (req, res) => {
  const { id } = req;
  res.send(`Userid: ${id}`));
}
        
app.use((req, res) => {
  res.statusCode = 404;
  res.send('Route Not Found');
});

app.use((err, req, res, next) => {
  const { message = '' } = err;
  res.status(500).json({ message })
})

module.exports = app;
Agregamos el middleware de error de express
// expresso.js
const { parse } = require('url');

module.exports = function expresso() {
  const middlewares = [];

  function send(content) {
    this.end(content);
  }
  
  function json(content) {
    this.setHeader('Content-Type', 'application/json');
    this.end(JSON.stringify(content));
  }

  function status(code) {
    this.statusCode = code;
    return this;
  }
  
  const app = function next(req, res, start = 0, err = undefined) {
    res.send = send;
    res.json = json;
    res.status = status;
    
    // Extract URL
    const { method, url } = req;
    const { pathname } = parse(url);
    
    // Match Route
    let index = -1;
    for (let i = start; i < middlewares.length; i += 1) {
      if (err) {
        if (middlewares[i].callback.length === 4) {
          index = i;
          break;
        }
      } else if (middlewares[i].route) {
        if (middlewares[i].route === pathname && middlewares[i].method === method) {
          index = i;
          break;
        }
      } else {
        index = i;
        break;
      }
    }

    // Execute Callback
    if (index === -1) {
      res.statusCode = 404;
      res.end('Not middleware Found');
    } else {
      middlewares[index].callback(req, res, () => {
        next(req, res, index + 1);
      });
    }
  };

  app.get = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'GET',
    });
  };
  
  app.use = (callback) => {
    middlewares.push({
      callback,
    });
  };

  return app;
};
Modificamos nuevamente nuestro función principal
Agregamos el nuevo caso en la busqueda del middleware
// expresso.js
const { parse } = require('url');

module.exports = function expresso() {
  const middlewares = [];

  function send(content) {
    this.end(content);
  }
  
  function json(content) {
    this.setHeader('Content-Type', 'application/json');
    this.end(JSON.stringify(content));
  }

  function status(code) {
    this.statusCode = code;
    return this;
  }
  
  const app = function next(req, res, start = 0, err = undefined) {
    res.send = send;
    res.json = json;
    res.status = status;
    
    // Extract URL
    const { method, url } = req;
    const { pathname } = parse(url);
    
    // Match Route
    let index = -1;
    for (let i = start; i < middlewares.length; i += 1) {
      if (err) {
        if (middlewares[i].callback.length === 4) {
          index = i;
          break;
        }
      } else if (middlewares[i].route) {
        if (middlewares[i].route === pathname && middlewares[i].method === method) {
          index = i;
          break;
        }
      } else {
        index = i;
        break;
      }
    }

    // Execute Callback
    if (index === -1) {
      res.statusCode = 404;
      res.end('Not middleware Found');
    } else {
      const params = [req, res];

      if (err) {
        params.unshift(err);
      }
      
      middlewares[index].callback(...params, (error = undefined) => {
        next(req, res, index + 1, error);
      });
    }
  };

  app.get = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'GET',
    });
  };
  
  app.use = (callback) => {
    middlewares.push({
      callback,
    });
  };

  return app;
};
Modificamos el orden de los parametros si es necesario para cumplir con la firma de express
Modificamos también nuestro callback

8

Entandarizar y reusar

// index.js

// const express = require('express');
const express = require('./expresso');
const bodyParser = require('body-parser');

const app = express();

// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));

// parse application/json
app.use(bodyParser.json());

app.use((req, res, next) => {
  const id = 123;
  req.id = id;
  next();
});

app.get('/', (req, res) => res.send('Welcome API'));

app.post('/users', (req, res, next) => {
  const { id } = req.body;
  res.json({
    id,
  });
});

app.get('/users', (req, res) => {
  const { id } = req;
  res.send(`Userid: ${id}`));
}
        
app.use((req, res) => {
  res.statusCode = 404;
  res.send('Route Not Found');
});

app.use((err, req, res, next) => {
  const { message = '' } = err;
  res.status(500).json({ message })
})

module.exports = app;
Agregamos body-parser para procesar los parametros
Agregamos el metodo POST
// expresso.js
const { parse } = require('url');

module.exports = function expresso() {
  const middlewares = [];

  function send(content) {
    this.end(content);
  }
  
  function json(content) {
    this.setHeader('Content-Type', 'application/json');
    this.end(JSON.stringify(content));
  }

  function status(code) {
    this.statusCode = code;
    return this;
  }
  
  const app = function next(req, res, start = 0, err = undefined) {
    res.send = send;
    res.json = json;
    res.status = status;
    
    // Extract URL
    const { method, url } = req;
    const { pathname } = parse(url);
    
    // Match Route
    let index = -1;
    for (let i = start; i < middlewares.length; i += 1) {
      if (err) {
        if (middlewares[i].callback.length === 4) {
          index = i;
          break;
        }
      } else if (middlewares[i].route) {
        if (middlewares[i].route === pathname && middlewares[i].method === method) {
          index = i;
          break;
        }
      } else {
        index = i;
        break;
      }
    }

    // Execute Callback
    if (index === -1) {
      res.statusCode = 404;
      res.end('Not middleware Found');
    } else {
      const params = [req, res];

      if (err) {
        params.unshift(err);
      }
      
      middlewares[index].callback(...params, (error = undefined) => {
        next(req, res, index + 1, error);
      });
    }
  };

  app.get = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'GET',
    });
  };
  
  app.post = (route, callback) => {
    middlewares.push({
      route,
      callback,
      method: 'POST',
    });
  };
  
  app.use = (callback) => {
    middlewares.push({
      callback,
    });
  };

  return app;
};
Agregamos soporte para el metodo POST

9

Soportar Third Party

// index.js

// const express = require('express');
const express = require('./expresso');

const app = express();

app.use((req, res, next) => {
  const id = 123;
  req.id = id;
  next();
});

app.get('/', (req, res) => res.send('Welcome API'));

app.get('/users', (req, res) => {
  const { id } = req;
  res.send(`Userid: ${id}`));
}
        
app.use((req, res) => {
  res.statusCode = 404;
  res.send('Route Not Found');
});

app.use((err, req, res, next) => {
  const { message = '' } = err;
  res.status(500).json({ message })
})

module.exports = app;

One more thing

‘Stay hungry. Stay foolish.’

Steve Jobs

Gracias

Made with Slides.com