Share
Embed
How to Build an API Service in 30 Minutes with Express.js
These are the slides for the talk I gave at O'Reilly Fluent on April 21, 2015.
In this talk I walk through the process of building a full API service with account management, API key management, billing, etc.
The API service being built is 100% open source, and sends SMS messages notifying users about the current price of Bitcoin.
Randall Degges
April 21, 2015
Transcript
60 slides
-
How to Build an API Service in 30 Minutes with
Express. js @rdegges @gostormpath -
var stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); stripe.charges.create({ amount: 2000, // $20 currency:
'usd', source: token, description: 'One time deposit.' }, function(err, charge) { if (err) throw err; console.log('Successfully billed user:', charge.amount); }); -
var twilio = require('twilio')( process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN ); twilio.sendMessage({ to: '+18182179229',
from: '+18882223333', body: 'Heyo!' }, function(err, resp) { if (err) throw err; console.log('SMS message sent!'); }); -
var request = require('request'); request( 'http://api.bitcoincharts.com/v1/weighted_prices.json', function(err, resp, body) {
if (err) throw err; var data = JSON.parse(body); console.log('BTC value in USD:', data.USD['24h']); } ); -
Stormpath • User storage. • User data storage (as JSON).
• Authentication. • Authorization. • API key management. • API authentication. • Password reset. • etc. -
var stormpath = require('express-stormpath'); app.use(stormpath.init(app)); app.get('/', stormpath.loginRequired, function(req, res) {
res.send('Hi ' + req.user.email + '!'); }); -
The Layout . ├── bower.json ├── index.js ├── package.json ├──
routes │ ├── api.js │ ├── private.js │ └── public.js ├── static │ └── css │ └── main.css └── views ├── base.jade ├── dashboard.jade ├── docs.jade ├── index.jade └── pricing.jade Style Stuff Code Stuff Init Stuff HTML Stuff -
{ "name": "btc-sms", "main": "index.js", "version": "0.0.0", "dependencies": { "jquery":
"~2.1.3", "bootstrap": "~3.3.4", "respond": "~1.4.2", "html5shiv": "~3.7.2", "bootswatch": "~3.3.4+1" } } bower.json -
{ "name": "btc-sms", "version": "0.0.0", "main": "index.js", "dependencies": { "async":
"^0.9.0", "body-parser": "^1.12.3", "express": "^4.12.3", "express-stormpath": "^1.0.4", "jade": "^1.9.2", "request": "^2.55.0", "stripe": "^3.3.4", "twilio": "^2.0.0" } } package.json yey ^^ -
extends base block vars - var title = 'Home' block
body .container.index h1.text-center Get BTC Rates via SMS .row .col-xs-12.col-md-offset-2.col-md-8 .jumbotron.text-justify p. #{siteTitle} makes it easy to track the value of Bitcoin via SMS. Each time you hit the API service, we'll SMS you the current Bitcoin price in a user-friendly way. a(href='/register') button.btn.btn-lg.btn-primary.center-block(type='button') Get Started! index.jade -
pricing.jade extends base block vars - var title = 'Pricing'
block body .container.pricing h1.text-center Pricing .row .col-xs-offset-2.col-xs-8.col-md-offset-4.col-md-4.price-box.text-center h2 #{costPerQuery}¢ / query p.text-justify. We believe in simple pricing. Everyone pays the same usage-based feeds regardless of size. p.text-justify.end. <i>Regardless of how many requests you make, BTC exchange rates are updated once per hour.</i> .row .col-xs-offset-2.col-xs-8.col-md-offset-4.col-md-4 a(href='/register') button.btn.btn-lg.btn-primary.center-block(type='button') Get Started! -
docs.jade extends base block vars - var title = 'Docs'
block body .container.docs h1.text-center API Documentation .row .col-xs-12.col-md-offset-2.col-md-8 p.text-justify i. This page contains the documentation for this API service. There is only a single API endpoint available right now, so this document is fairly short. p.text-justify i. Questions? Please email <a href="mailto:[email protected]" >[email protected]</a> for help! h2 REST Endpoints h3 POST /api/message span Description p.description. This API endpoint takes in a phone number, and sends this phone an SMS message with the current Bitcoin exchange rate. span Input .table-box table.table.table-bordered thead tr th Field th Type th Required tbody tr td phoneNumber td String td true span Success Output .table-box table.table.table-bordered thead tr th Field th Type th Example tbody tr td phoneNumber td String td "+18182223333" tr td message td String td "1 Bitcoin is currently worth $225.42 USD." tr td cost td Integer td #{costPerQuery} span Failure Output .table-box table.table.table-bordered thead tr th Field th Type th Example tbody tr td error td String td "We couldn't send the SMS message. Try again soon!" span Example Request pre. $ curl -X POST \ --user 'id:secret' \ --data '{"phoneNumber": "+18182223333"}' \ -H 'Content-Type: application/json' \ 'http://apiservice.com/api/message' -
dashboard.jade extends base block vars - var title = 'Dashboard'
block body .container.dashboard .row.api-keys ul.list-group .col-xs-offset-1.col-xs-10 li.list-group-item.api-key-container .left strong API Key ID: span.api-key-id #{user.apiKeys.items[0].id} .right strong API Key Secret: span.api-key-secret #{user.apiKeys.items[0].secret} .row.widgets .col-md-offset-1.col-md-5 .panel.panel-primary .panel-heading.text-center h3.panel-title Analytics .analytics-content.text-center span.total-queries #{user.customData.totalQueries} br span i. *total queries .col-md-5 .panel.panel-primary .panel-heading.text-center h3.panel-title Billing .billing-content.text-center span.account-balance $#{(user.customData.balance / 100).toFixed(2)} br span i. *current account balance form(action='/dashboard/charge', method='POST') script.stripe-button( src = 'https://checkout.stripe.com/checkout.js', data-email = '#{user.email}', data-key = '#{stripePublishableKey}', data-name = '#{siteTitle}', data-amount = '2000', data-allow-remember-me = 'false' ) -
var async = require('async'); var express = require('express'); var stormpath
= require('express-stormpath'); var apiRoutes = require('./routes/api'); var privateRoutes = require('./routes/private'); var publicRoutes = require('./routes/public'); // Globals var app = express(); // Application settings app.set('view engine', 'jade'); app.set('views', './views'); app.locals.costPerQuery = parseInt(process.env.COST_PER_QUERY); app.locals.siteTitle = 'BTC SMS'; app.locals.stripePublishableKey = process.env.STRIPE_PUBLISHABLE_KEY; #{siteTitle} -
// Middlewares app.use('/static', express.static('./static', { index: false, redirect: false }));
app.use('/static', express.static('./bower_components', { index: false, redirect: false })); -
function(account, req, res, next) { async.parallel([ // Set the user's
default settings. function(cb) { account.customData.balance = 0; account.customData.totalQueries = 0; account.customData.save(function(err) { if (err) return cb(err); cb(); }); }, // Create an API key for this user. function(cb) { account.createApiKey(function(err, key) { if (err) return cb(err); cb(); }); } ], function(err) { if (err) return next(err); next(); }); } persist data -
li.list-group-item.api-key-container .left strong API Key ID: span.api-key-id #{user.apiKeys.items[0].id} .right strong
API Key Secret: span.api-key-secret #{user.apiKeys.items[0].secret} .analytics-content.text-center span.total-queries #{user.customData.totalQueries} API key info query info .billing-content.text-center span.account-balance $#{(user.customData.balance / 100).toFixed(2)} $$$ info -
// Routes app.use('/', publicRoutes); app.use('/api', stormpath.apiAuthenticationRequired, apiRoutes); app.use('/dashboard', stormpath.loginRequired, privateRoutes);
// Server app.listen(process.env.PORT || 3000); security middleware -
var express = require('express'); // Globals var router = express.Router();
// Routes router.get('/', function(req, res) { res.render('index'); }); router.get('/pricing', function(req, res) { res.render('pricing'); }); router.get('/docs', function(req, res) { res.render('docs'); }); // Exports module.exports = router; -
// dashboard.jade form(action='/dashboard/charge', method='POST') script.stripe-button( src = 'https://checkout.stripe.com/checkout.js', data-email =
'#{user.email}', data-key = '#{stripePublishableKey}', data-name = '#{siteTitle}', data-amount = '2000', data-allow-remember-me = 'false' ) -
var bodyParser = require('body-parser'); var express = require('express'); var stormpath
= require('express-stormpath'); var stripe = require('stripe')(process.env. STRIPE_SECRET_KEY); // Globals var router = express.Router(); // Middlewares router.use(bodyParser.urlencoded({ extended: true })); // Routes router.get('/', function(req, res) { res.render('dashboard'); }); router.post('/charge', function(req, res, next) { stripe.charges.create({ amount: 2000, currency: 'usd', source: req.body.stripeToken, description: 'One time deposit for ' + req.user.email + '.' }, function(err, charge) { if (err) return next(err); req.user.customData.balance += charge.amount; req.user.customData.save(function(err) { if (err) return next(err); res.redirect('/dashboard'); }); }); }); // Exports module.exports = router; -
var bodyParser = require('body-parser'); var express = require('express'); var request
= require('request'); var twilio = require('twilio')( process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN ); // Globals var router = express.Router(); var BTC_EXCHANGE_RATE; var COST_PER_QUERY = parseInt(process.env.COST_PER_QUERY); // Middlewares router.use(bodyParser.json()); -
// Functions function getExchangeRates() { request('http://api.bitcoincharts.com/v1/weighted_prices.json', function(err, resp, body) {
if (err || resp.statusCode !== 200) { console.log('Failed to retrieve BTC exchange rates.'); return; } try { var data = JSON.parse(body); BTC_EXCHANGE_RATE = data.USD['24h']; console.log('Updated BTC exchange rate: ' + BTC_EXCHANGE_RATE + '.'); } catch (err) { console.log('Failed to parse BTC exchange rates.'); return; } }); } // Tasks getExchangeRates(); setInterval(getExchangeRates, 60000); -
// Routes router.post('/message', function(req, res) { if (!req.body || !req.body.phoneNumber)
{ return res.status(400).json({ error: 'phoneNumber is required.' }); } else if (!BTC_EXCHANGE_RATE) { return res.status(500).json({ error: "We're having trouble getting the exchange rates right now. Try again soon!" }); } else if (req.user.customData.balance < COST_PER_QUERY) { return res.status(402).json({ error: 'Payment required. You need to deposit funds into your account.' }); } var message = '1 Bitcoin is currently worth $' + BTC_EXCHANGE_RATE + ' USD.'; twilio.sendMessage({ to: req.body.phoneNumber, from: process.env.TWILIO_PHONE_NUMBER, body: message }, function(err, resp) { if (err) return res.status(500).json({ error: "We couldn't send the SMS message. Try again soon!" }); req.user.customData.balance -= COST_PER_QUERY; req.user.customData.totalQueries += 1; req.user.customData.save(); res.json({ phoneNumber: req.body.phoneNumber, message: message, cost: COST_PER_QUERY }); }); }); // Exports module.exports = router;