├── .env.example ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bin └── server.dart ├── db.sql ├── db └── items-table.sql ├── db_create.sh ├── lib ├── api.dart ├── db.dart └── models │ └── item.dart └── pubspec.yaml /.env.example: -------------------------------------------------------------------------------- 1 | PORT=9001 2 | DB_PORT=5432 3 | DB_HOST=localhost 4 | DB_USER=dart_pg_user 5 | DB_PASS=insecure_password 6 | DB_NAME=dart_pg_example -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[dart]": { 3 | "editor.tabSize": 4, 4 | "editor.insertSpaces": false, 5 | "editor.detectIndentation": false, 6 | }, 7 | "dart.closingLabels": false, 8 | "files.exclude": { 9 | "**/.classpath": true, 10 | "**/.project": true, 11 | "**/.settings": true, 12 | "**/.factorypath": true 13 | } 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version, created by Stagehand 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kenneth Reilly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dart-postgres-example 2 | Example project demonstrating how to build web APIs with [Dart](https://dart.dev), [Shelf](https://pub.dartlang.org/packages/shelf), and [PostgreSQL](https://postgresql.org). 3 | 4 | ## Introduction 5 | This project is an implementation of a very simple REST API (and backing PostgreSQL database schema) that can create and list basic items via HTTP requests. Server port and database connection info are set via environment variables, allowing for easy deployment to app containers. 6 | 7 | ## Requirements 8 | The following tools are required to run this project: 9 | * [Dart](https://dart.dev/get-dart) 10 | * [PostgreSQL](https://www.postgresql.org/download/) 11 | 12 | ## Getting Started 13 | To run and test the server: 14 | * Clone this repo and `cd` into its directory 15 | * Initialize the database with `./db_create.sh` 16 | * Run the server with `$ dart bin/server.dart` 17 | * Create items with `$ curl -X POST localhost:9001/item -d '{"name":"test"}'` 18 | * List items with `$ curl localhost:9001/items` 19 | 20 | ## Contribution 21 | If you notice something out of place or in need of some improvements: issues, pull requests, and other contributions are welcome. 22 | 23 | Thanks for checking out this project! 24 | [@8_bit_hacker](https://twitter.com/8_bit_hacker) 25 | 26 | ___ 27 | Created from templates made available by Stagehand under a BSD-style 28 | [license](https://github.com/dart-lang/stagehand/blob/master/LICENSE). 29 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:pedantic/analysis_options.yaml 5 | 6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 7 | # Uncomment to specify additional rules. 8 | # linter: 9 | # rules: 10 | # - camel_case_types 11 | 12 | analyzer: 13 | # exclude: 14 | # - path/to/excluded/files/** 15 | -------------------------------------------------------------------------------- /bin/server.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:convert'; 3 | import 'package:shelf/shelf.dart'; 4 | import 'package:shelf/shelf_io.dart' as io; 5 | import 'package:dotenv/dotenv.dart' as dotenv; 6 | import 'package:dart_postgres_example/api.dart'; 7 | 8 | abstract class Server { 9 | 10 | static JsonDecoder _decoder = const JsonDecoder(); 11 | static JsonEncoder _encoder = const JsonEncoder(); 12 | 13 | static dynamic _decode(request) async => _decoder.convert(await request.readAsString()); 14 | static dynamic _encode(dynamic data) => _encoder.convert(data); 15 | 16 | static Future _get(Request request) async { 17 | 18 | switch (request.url.toString()) { 19 | 20 | case 'items': 21 | return Response.ok(_encode(await API.index())); 22 | 23 | default: 24 | return Response.notFound('Not Found'); 25 | } 26 | } 27 | 28 | static Future _post(Request request) async { 29 | 30 | switch (request.url.toString()) { 31 | 32 | case 'item': 33 | try { 34 | dynamic data = await _decode(request); 35 | return Response.ok(_encode(await API.addItem(data))); 36 | } 37 | catch(e) { return Response(400); } 38 | break; 39 | 40 | default: 41 | return Response.notFound('Not Found'); 42 | } 43 | } 44 | 45 | static Future _handle(Request request) async { 46 | 47 | switch (request.method) { 48 | 49 | case 'GET': 50 | return _get(request); 51 | 52 | case 'POST': 53 | return _post(request); 54 | 55 | default: 56 | return Response.notFound('Not Found'); 57 | } 58 | } 59 | 60 | static Future start() async { 61 | 62 | String filename = (await File.fromUri(Uri.parse('.env')).exists()) ? '.env' : '.env.example'; 63 | dotenv.load(filename); 64 | 65 | await API.init(dotenv.env); 66 | 67 | dynamic handler = const Pipeline() 68 | .addMiddleware(logRequests()) 69 | .addHandler(_handle); 70 | 71 | int port = int.parse(dotenv.env['PORT']); 72 | HttpServer server = await io.serve(handler, 'localhost', port); 73 | print('Serving at http://${server.address.host}:${server.port}'); 74 | } 75 | } 76 | 77 | main(List args) async => Server.start(); -------------------------------------------------------------------------------- /db.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenreilly/dart-postgres-example/cfa260c7bb47f222f1ca0c746db73f750836f0a4/db.sql -------------------------------------------------------------------------------- /db/items-table.sql: -------------------------------------------------------------------------------- 1 | -- SEQUENCE: public.items_id_seq 2 | 3 | -- DROP SEQUENCE public.items_id_seq; 4 | 5 | CREATE SEQUENCE public.items_id_seq; 6 | 7 | ALTER SEQUENCE public.items_id_seq 8 | OWNER TO dart_pg_user; 9 | 10 | 11 | -- Table: public.items 12 | 13 | -- DROP TABLE public.items; 14 | 15 | CREATE TABLE public.items 16 | ( 17 | id integer NOT NULL DEFAULT nextval('items_id_seq'::regclass), 18 | name character varying(16) COLLATE pg_catalog."default" NOT NULL, 19 | description character varying(128) COLLATE pg_catalog."default", 20 | create_timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 21 | CONSTRAINT items_pkey PRIMARY KEY (id) 22 | ) 23 | WITH ( 24 | OIDS = FALSE 25 | ) 26 | TABLESPACE pg_default; 27 | 28 | ALTER TABLE public.items 29 | OWNER to dart_pg_user; -------------------------------------------------------------------------------- /db_create.sh: -------------------------------------------------------------------------------- 1 | psql postgres -c "CREATE USER dart_pg_user WITH LOGIN SUPERUSER INHERIT CREATEDB CREATEROLE NOREPLICATION PASSWORD 'insecure_password'" 2 | psql postgres -c "CREATE DATABASE dart_pg_example WITH OWNER = dart_pg_user ENCODING = 'UTF8' CONNECTION LIMIT = -1;" 3 | psql dart_pg_example -f db/items-table.sql -------------------------------------------------------------------------------- /lib/api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:dart_postgres_example/db.dart'; 3 | import 'package:dart_postgres_example/models/item.dart'; 4 | 5 | abstract class API { 6 | 7 | static DB _db; 8 | 9 | static init(Map env) async => 10 | _db = await DB.connect(env); 11 | 12 | static Future> index() async { 13 | 14 | List> items = []; 15 | dynamic result = await _db.query('select id, name from items'); 16 | for (final row in result) { items.add(row['items']); } 17 | return items; 18 | } 19 | 20 | static Future> addItem(dynamic data) async { 21 | 22 | Item item = Item.fromDynamicMap(data); 23 | String sql = "insert into items (name, description) VALUES (@name, @description) RETURNING id"; 24 | Map params = { "name": item.name, "description": item.description }; 25 | dynamic result = await _db.query(sql, values: params); 26 | 27 | return { "id": result[0]['items']['id'] }; 28 | } 29 | } -------------------------------------------------------------------------------- /lib/db.dart: -------------------------------------------------------------------------------- 1 | import 'package:postgres/postgres.dart'; 2 | 3 | class DB { 4 | 5 | PostgreSQLConnection _connection; 6 | 7 | static Future connect(Map env) async { 8 | 9 | int _port = 5432; 10 | String _host = env['DB_HOST']; 11 | String _user = env['DB_USER']; 12 | String _pass = env['DB_PASS']; 13 | String _name = env['DB_NAME']; 14 | 15 | DB db = DB(); 16 | db._connection = PostgreSQLConnection(_host, _port, _name, username: _user, password: _pass); 17 | await db._connection.open(); 18 | return db; 19 | } 20 | 21 | Future> query(String sql, { Map values }) async { 22 | 23 | try { 24 | return await _connection.mappedResultsQuery(sql, substitutionValues: values); 25 | } 26 | catch(e) { 27 | return Future.value([]); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /lib/models/item.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | class Item { 4 | 5 | Item({ @required this.name, this.description }); 6 | 7 | String name; 8 | String description; 9 | 10 | static Item fromDynamicMap(Map map) => 11 | Item(name: map['name'], description: map['description']); 12 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_postgres_example 2 | description: A web server built using the shelf package. 3 | # version: 1.0.0 4 | # homepage: https://innovationgroup.tech 5 | # author: Kenneth Reilly 6 | 7 | environment: 8 | sdk: '>=2.1.0 <3.0.0' 9 | 10 | dependencies: 11 | shelf: ^0.7.2 12 | dotenv: ^1.0.0 13 | postgres: ^1.0.2 14 | 15 | dev_dependencies: 16 | pedantic: ^1.0.0 17 | --------------------------------------------------------------------------------